@synapsor/client 0.1.5 → 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 +20 -0
- package/bin/synapsor.mjs +261 -0
- package/package.json +1 -1
- package/synapsor.mjs +162 -0
package/README.md
CHANGED
|
@@ -181,6 +181,26 @@ The worker validates Synapsor-provided mapping metadata, only writes allowlisted
|
|
|
181
181
|
columns, adds tenant and primary-key guards, checks optional conflict columns,
|
|
182
182
|
and reports `applied`, `conflict`, or `failed` back to Synapsor idempotently.
|
|
183
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
|
+
|
|
184
204
|
## API Surface
|
|
185
205
|
|
|
186
206
|
- `execute(sql)` and `query(sql)`
|
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."],
|
|
@@ -176,6 +178,13 @@ function boolFlag(args, name) {
|
|
|
176
178
|
return args.includes(name);
|
|
177
179
|
}
|
|
178
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
|
+
|
|
179
188
|
function resolveSecretRef(value) {
|
|
180
189
|
const text = String(value || "").trim();
|
|
181
190
|
if (text.startsWith("env:")) {
|
|
@@ -221,6 +230,250 @@ async function readStdinSecret() {
|
|
|
221
230
|
return value.trim();
|
|
222
231
|
}
|
|
223
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
|
+
|
|
224
477
|
async function main(argv = process.argv.slice(2)) {
|
|
225
478
|
const { globals, args } = parse(argv);
|
|
226
479
|
const [cmd, sub, third, ...rest] = args;
|
|
@@ -375,6 +628,11 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
375
628
|
}
|
|
376
629
|
}
|
|
377
630
|
|
|
631
|
+
if (cmd === "mcp" && sub === "audit") {
|
|
632
|
+
const targetInfo = await loadMcpAuditTarget(third, rest);
|
|
633
|
+
return printMcpAudit(buildMcpAuditReport(targetInfo), globals);
|
|
634
|
+
}
|
|
635
|
+
|
|
378
636
|
if (cmd === "sql") {
|
|
379
637
|
const db = globals.db || "";
|
|
380
638
|
let sql = args.slice(1).join(" ").trim();
|
|
@@ -438,6 +696,9 @@ Usage:
|
|
|
438
696
|
synapsor sources show app_postgres --project <project>
|
|
439
697
|
synapsor sources doctor app_postgres --project <project>
|
|
440
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
|
|
441
702
|
synapsor sql --db <database> "SELECT 1;"
|
|
442
703
|
synapsor sql --db <database> --file ./query.sql
|
|
443
704
|
synapsor invoke <capability> --db <database> --json '{"ticket_id":"TICK-1001"}'
|
package/package.json
CHANGED
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,
|
|
@@ -644,6 +655,10 @@ export class Synapsor {
|
|
|
644
655
|
});
|
|
645
656
|
}
|
|
646
657
|
|
|
658
|
+
async listAdapterTools(adapter, options = {}) {
|
|
659
|
+
return this.adapterTools(adapter, options);
|
|
660
|
+
}
|
|
661
|
+
|
|
647
662
|
async callAdapterTool(adapter, tool, {
|
|
648
663
|
arguments: args = {},
|
|
649
664
|
runId = undefined,
|
|
@@ -669,6 +684,138 @@ export class Synapsor {
|
|
|
669
684
|
return this.request("POST", "/v1/agent/adapters/call-tool", payload);
|
|
670
685
|
}
|
|
671
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
|
+
|
|
672
819
|
async traceAgentRun(runId, { session = undefined } = {}) {
|
|
673
820
|
return this.request("POST", "/v1/agent/runs/trace", {
|
|
674
821
|
run_id: runId,
|
|
@@ -1053,6 +1200,10 @@ export class Synapsor {
|
|
|
1053
1200
|
return this.request("POST", "/v1/agent/runs/replay", payload);
|
|
1054
1201
|
}
|
|
1055
1202
|
|
|
1203
|
+
async getReplay(runId, options = {}) {
|
|
1204
|
+
return this.replayAgentRun(runId, options);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1056
1207
|
async proposalLifecycle(action, proposal, { session = undefined, promoteBranch = undefined, promote_branch = undefined, targetBranch = undefined, target_branch = undefined, settlementPolicy = undefined, settlement_policy = undefined } = {}) {
|
|
1057
1208
|
const payload = {
|
|
1058
1209
|
proposal,
|
|
@@ -1479,6 +1630,17 @@ function identifier(value) {
|
|
|
1479
1630
|
return value;
|
|
1480
1631
|
}
|
|
1481
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
|
+
|
|
1482
1644
|
function repoRoot() {
|
|
1483
1645
|
return resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
1484
1646
|
}
|