@synapsor/runner 0.1.0-alpha.4 → 0.1.0-alpha.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 +211 -43
- package/dist/cli.d.ts.map +1 -1
- package/dist/runner.mjs +912 -90
- package/docs/README.md +32 -54
- package/docs/getting-started-own-database.md +68 -36
- package/docs/http-mcp.md +200 -0
- package/docs/local-mode.md +71 -35
- package/docs/mcp-audit.md +11 -15
- package/docs/mcp-client-setup.md +48 -9
- package/docs/recipes.md +6 -6
- package/docs/security-boundary.md +1 -1
- package/docs/troubleshooting-first-run.md +6 -6
- package/docs/writeback-executors.md +1 -1
- package/examples/openai-agents-http/README.md +55 -0
- package/examples/openai-agents-http/agent.py +90 -0
- package/examples/openai-agents-http/requirements.txt +1 -0
- package/examples/openai-agents-stdio/README.md +62 -0
- package/examples/openai-agents-stdio/agent.py +70 -0
- package/examples/openai-agents-stdio/requirements.txt +1 -0
- package/examples/reference-support-billing-app/README.md +17 -17
- package/package.json +3 -2
- package/docs/MCP_RUNNER_IMPLEMENTATION_PLAN.md +0 -187
- package/docs/architecture.md +0 -65
- package/docs/capability-config.md +0 -180
- package/docs/cloud-mode.md +0 -140
- package/docs/config-migrations.md +0 -67
- package/docs/demo-transcript.md +0 -161
- package/docs/dependency-license-inventory.md +0 -35
- package/docs/first-10-minutes.md +0 -172
- package/docs/licensing.md +0 -38
- package/docs/local-ui.md +0 -163
- package/docs/mcp-efficiency-benchmark.md +0 -84
- package/docs/open-source-feature-inventory.md +0 -254
- package/docs/operations.md +0 -38
- package/docs/own-db-20-minutes.md +0 -185
- package/docs/production-readiness.md +0 -39
- package/docs/protocol.md +0 -90
- package/docs/roadmap.md +0 -13
- package/docs/schema-inspection.md +0 -88
- package/docs/shadow-mode.md +0 -67
- package/docs/telemetry.md +0 -28
- package/docs/threat-model.md +0 -25
- package/docs/trusted-context.md +0 -70
package/dist/runner.mjs
CHANGED
|
@@ -1180,6 +1180,7 @@ function isRecord(value) {
|
|
|
1180
1180
|
// packages/mcp-server/src/index.ts
|
|
1181
1181
|
import crypto from "node:crypto";
|
|
1182
1182
|
import fs from "node:fs";
|
|
1183
|
+
import { createServer } from "node:http";
|
|
1183
1184
|
import path from "node:path";
|
|
1184
1185
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1185
1186
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -2684,7 +2685,7 @@ function createMcpRuntime(config, options = {}) {
|
|
|
2684
2685
|
}
|
|
2685
2686
|
function createSynapsorMcpServer(runtime) {
|
|
2686
2687
|
const server = new McpServer(
|
|
2687
|
-
{ name: "synapsor-runner", version: "0.1.0-alpha.
|
|
2688
|
+
{ name: "synapsor-runner", version: "0.1.0-alpha.6" },
|
|
2688
2689
|
{ capabilities: { tools: {}, resources: {} } }
|
|
2689
2690
|
);
|
|
2690
2691
|
if (runtime.config.mode === "cloud") {
|
|
@@ -2778,6 +2779,233 @@ async function serveStdio(options = {}) {
|
|
|
2778
2779
|
process.once("SIGTERM", close);
|
|
2779
2780
|
});
|
|
2780
2781
|
}
|
|
2782
|
+
async function startHttpMcpServer(options = {}) {
|
|
2783
|
+
const host = options.host ?? "127.0.0.1";
|
|
2784
|
+
const port = options.port ?? 8765;
|
|
2785
|
+
const authTokenEnv = options.authTokenEnv ?? "SYNAPSOR_RUNNER_HTTP_TOKEN";
|
|
2786
|
+
const env = options.env ?? process.env;
|
|
2787
|
+
const devNoAuth = options.devNoAuth === true;
|
|
2788
|
+
if (devNoAuth && !isLoopbackHost(host)) {
|
|
2789
|
+
throw new McpRuntimeError("HTTP_DEV_NO_AUTH_UNSAFE_HOST", "--dev-no-auth is only allowed with localhost or 127.0.0.1.");
|
|
2790
|
+
}
|
|
2791
|
+
const authToken = devNoAuth ? void 0 : env[authTokenEnv];
|
|
2792
|
+
if (!devNoAuth && !authToken) {
|
|
2793
|
+
throw new McpRuntimeError("HTTP_AUTH_TOKEN_MISSING", `${authTokenEnv} is not set. HTTP MCP requires bearer auth by default.`);
|
|
2794
|
+
}
|
|
2795
|
+
const config = options.config ?? loadRuntimeConfigFromFile(options.configPath);
|
|
2796
|
+
const cloudTools = config.mode === "cloud" ? await fetchCloudToolMetadata(config, env) : void 0;
|
|
2797
|
+
const runtime = createMcpRuntime(config, {
|
|
2798
|
+
env,
|
|
2799
|
+
storePath: options.storePath,
|
|
2800
|
+
readRow: options.readRow,
|
|
2801
|
+
cloudTools
|
|
2802
|
+
});
|
|
2803
|
+
const server = createServer((request, response) => {
|
|
2804
|
+
void handleHttpMcpRequest({
|
|
2805
|
+
request,
|
|
2806
|
+
response,
|
|
2807
|
+
runtime,
|
|
2808
|
+
authToken,
|
|
2809
|
+
devNoAuth,
|
|
2810
|
+
corsOrigin: options.corsOrigin
|
|
2811
|
+
});
|
|
2812
|
+
});
|
|
2813
|
+
try {
|
|
2814
|
+
await new Promise((resolve, reject) => {
|
|
2815
|
+
server.once("error", reject);
|
|
2816
|
+
server.listen(port, host, () => {
|
|
2817
|
+
server.off("error", reject);
|
|
2818
|
+
resolve();
|
|
2819
|
+
});
|
|
2820
|
+
});
|
|
2821
|
+
} catch (error) {
|
|
2822
|
+
runtime.close();
|
|
2823
|
+
throw error;
|
|
2824
|
+
}
|
|
2825
|
+
const address = server.address();
|
|
2826
|
+
const actualHost = address.address === "::" ? host : address.address;
|
|
2827
|
+
const actualPort = address.port;
|
|
2828
|
+
const url = `http://${actualHost}:${actualPort}/mcp`;
|
|
2829
|
+
if (options.log !== false) {
|
|
2830
|
+
const log = options.log ?? process.stderr;
|
|
2831
|
+
log.write(`Synapsor Runner HTTP MCP listening on ${url}
|
|
2832
|
+
`);
|
|
2833
|
+
log.write(devNoAuth ? "Auth: disabled for localhost development only\n" : `Auth: bearer token from ${authTokenEnv}
|
|
2834
|
+
`);
|
|
2835
|
+
log.write(`Config: ${options.configPath ?? "synapsor.runner.json"}
|
|
2836
|
+
`);
|
|
2837
|
+
log.write(`Store: ${options.storePath ?? config.storage?.sqlite_path ?? "./.synapsor/local.db"}
|
|
2838
|
+
`);
|
|
2839
|
+
}
|
|
2840
|
+
return {
|
|
2841
|
+
host: actualHost,
|
|
2842
|
+
port: actualPort,
|
|
2843
|
+
url,
|
|
2844
|
+
close: () => closeHttpServer(server, runtime)
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2847
|
+
async function handleHttpMcpRequest(input) {
|
|
2848
|
+
const { request, response, runtime, authToken, devNoAuth, corsOrigin } = input;
|
|
2849
|
+
try {
|
|
2850
|
+
setCommonHttpHeaders(response, corsOrigin);
|
|
2851
|
+
if (request.method === "OPTIONS" && corsOrigin) {
|
|
2852
|
+
response.statusCode = 204;
|
|
2853
|
+
response.end();
|
|
2854
|
+
return;
|
|
2855
|
+
}
|
|
2856
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
2857
|
+
if (request.method === "GET" && url.pathname === "/healthz") {
|
|
2858
|
+
writeJson(response, 200, {
|
|
2859
|
+
ok: true,
|
|
2860
|
+
transport: "http",
|
|
2861
|
+
tools: runtime.listTools().length,
|
|
2862
|
+
mode: runtime.config.mode
|
|
2863
|
+
});
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
if (url.pathname !== "/mcp") {
|
|
2867
|
+
writeJson(response, 404, { ok: false, error: "not_found" });
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
if (request.method !== "POST") {
|
|
2871
|
+
writeJson(response, 405, { ok: false, error: "method_not_allowed" });
|
|
2872
|
+
return;
|
|
2873
|
+
}
|
|
2874
|
+
if (!devNoAuth && !validBearerToken(request.headers.authorization, authToken ?? "")) {
|
|
2875
|
+
writeJson(response, 401, { ok: false, error: "unauthorized" });
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
const body = await readRequestBody(request);
|
|
2879
|
+
const payload = JSON.parse(body);
|
|
2880
|
+
if (!isRecord3(payload)) {
|
|
2881
|
+
writeJson(response, 400, jsonRpcError(null, -32600, "JSON-RPC request must be an object."));
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
const id = payload.id ?? null;
|
|
2885
|
+
const method = typeof payload.method === "string" ? payload.method : void 0;
|
|
2886
|
+
if (!method) {
|
|
2887
|
+
writeJson(response, 400, jsonRpcError(id, -32600, "JSON-RPC method is required."));
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2890
|
+
const result = await handleHttpJsonRpcMethod(runtime, method, isRecord3(payload.params) ? payload.params : {});
|
|
2891
|
+
writeJson(response, 200, {
|
|
2892
|
+
jsonrpc: "2.0",
|
|
2893
|
+
id,
|
|
2894
|
+
result: sanitizeHttpPayload(result, authToken)
|
|
2895
|
+
});
|
|
2896
|
+
} catch (error) {
|
|
2897
|
+
const message = sanitizeHttpError(error, authToken);
|
|
2898
|
+
writeJson(response, 200, jsonRpcError(null, -32e3, message));
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
async function handleHttpJsonRpcMethod(runtime, method, params) {
|
|
2902
|
+
if (method === "tools/list") {
|
|
2903
|
+
return {
|
|
2904
|
+
tools: runtime.listTools().map(httpToolMetadata)
|
|
2905
|
+
};
|
|
2906
|
+
}
|
|
2907
|
+
if (method === "tools/call") {
|
|
2908
|
+
const name = typeof params.name === "string" ? params.name : void 0;
|
|
2909
|
+
if (!name) throw new McpRuntimeError("HTTP_TOOL_NAME_REQUIRED", "tools/call requires params.name.");
|
|
2910
|
+
const args = isRecord3(params.arguments) ? params.arguments : isRecord3(params.args) ? params.args : {};
|
|
2911
|
+
return await toolCallResult(runtime, name, args);
|
|
2912
|
+
}
|
|
2913
|
+
if (method === "resources/read") {
|
|
2914
|
+
const uri = typeof params.uri === "string" ? params.uri : void 0;
|
|
2915
|
+
if (!uri) throw new McpRuntimeError("HTTP_RESOURCE_URI_REQUIRED", "resources/read requires params.uri.");
|
|
2916
|
+
return resourceResult(uri, runtime.readResource);
|
|
2917
|
+
}
|
|
2918
|
+
throw new McpRuntimeError("HTTP_JSONRPC_METHOD_UNSUPPORTED", `Unsupported MCP HTTP method: ${method}`);
|
|
2919
|
+
}
|
|
2920
|
+
function httpToolMetadata(tool) {
|
|
2921
|
+
return {
|
|
2922
|
+
name: tool.name,
|
|
2923
|
+
title: tool.title,
|
|
2924
|
+
description: tool.description,
|
|
2925
|
+
inputSchema: tool.input_schema,
|
|
2926
|
+
annotations: {
|
|
2927
|
+
...tool.annotations,
|
|
2928
|
+
raw_sql_exposed: false,
|
|
2929
|
+
approval_or_commit_tool: false
|
|
2930
|
+
},
|
|
2931
|
+
_meta: {
|
|
2932
|
+
"synapsor.raw_sql_exposed": false,
|
|
2933
|
+
"synapsor.approval_tool": false,
|
|
2934
|
+
"synapsor.database_credentials_exposed": false,
|
|
2935
|
+
"synapsor.model_controlled_tenant_authority": false
|
|
2936
|
+
}
|
|
2937
|
+
};
|
|
2938
|
+
}
|
|
2939
|
+
function validBearerToken(header, expected) {
|
|
2940
|
+
if (!header?.startsWith("Bearer ")) return false;
|
|
2941
|
+
const actual = header.slice("Bearer ".length);
|
|
2942
|
+
const actualBuffer = Buffer.from(actual);
|
|
2943
|
+
const expectedBuffer = Buffer.from(expected);
|
|
2944
|
+
return actualBuffer.length === expectedBuffer.length && crypto.timingSafeEqual(actualBuffer, expectedBuffer);
|
|
2945
|
+
}
|
|
2946
|
+
function setCommonHttpHeaders(response, corsOrigin) {
|
|
2947
|
+
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
2948
|
+
if (corsOrigin) {
|
|
2949
|
+
response.setHeader("access-control-allow-origin", corsOrigin);
|
|
2950
|
+
response.setHeader("access-control-allow-methods", "POST, GET, OPTIONS");
|
|
2951
|
+
response.setHeader("access-control-allow-headers", "authorization, content-type");
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
function writeJson(response, statusCode, payload) {
|
|
2955
|
+
response.statusCode = statusCode;
|
|
2956
|
+
response.end(`${JSON.stringify(payload, null, 2)}
|
|
2957
|
+
`);
|
|
2958
|
+
}
|
|
2959
|
+
async function readRequestBody(request) {
|
|
2960
|
+
const chunks = [];
|
|
2961
|
+
let bytes = 0;
|
|
2962
|
+
for await (const chunk of request) {
|
|
2963
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
2964
|
+
bytes += buffer.length;
|
|
2965
|
+
if (bytes > 1024 * 1024) {
|
|
2966
|
+
throw new McpRuntimeError("HTTP_BODY_TOO_LARGE", "HTTP MCP request body exceeds 1 MiB.");
|
|
2967
|
+
}
|
|
2968
|
+
chunks.push(buffer);
|
|
2969
|
+
}
|
|
2970
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
2971
|
+
}
|
|
2972
|
+
function jsonRpcError(id, code, message) {
|
|
2973
|
+
return {
|
|
2974
|
+
jsonrpc: "2.0",
|
|
2975
|
+
id,
|
|
2976
|
+
error: { code, message }
|
|
2977
|
+
};
|
|
2978
|
+
}
|
|
2979
|
+
function sanitizeHttpError(error, authToken) {
|
|
2980
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
2981
|
+
return sanitizeHttpString(raw, authToken);
|
|
2982
|
+
}
|
|
2983
|
+
function sanitizeHttpPayload(value, authToken) {
|
|
2984
|
+
if (typeof value === "string") return sanitizeHttpString(value, authToken);
|
|
2985
|
+
if (Array.isArray(value)) return value.map((item) => sanitizeHttpPayload(item, authToken));
|
|
2986
|
+
if (isRecord3(value)) {
|
|
2987
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitizeHttpPayload(item, authToken)]));
|
|
2988
|
+
}
|
|
2989
|
+
return value;
|
|
2990
|
+
}
|
|
2991
|
+
function sanitizeHttpString(value, authToken) {
|
|
2992
|
+
let redacted = value.replace(/(?:postgres(?:ql)?|mysql):\/\/[^\s"']+/gi, "[redacted-database-url]");
|
|
2993
|
+
if (authToken) redacted = redacted.split(authToken).join("[redacted-token]");
|
|
2994
|
+
return redacted;
|
|
2995
|
+
}
|
|
2996
|
+
function isLoopbackHost(host) {
|
|
2997
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
2998
|
+
}
|
|
2999
|
+
async function closeHttpServer(server, runtime) {
|
|
3000
|
+
await new Promise((resolve, reject) => {
|
|
3001
|
+
server.close((error) => {
|
|
3002
|
+
if (error) reject(error);
|
|
3003
|
+
else resolve();
|
|
3004
|
+
});
|
|
3005
|
+
}).finally(() => {
|
|
3006
|
+
runtime.close();
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
2781
3009
|
async function toolCallResult(runtime, toolName, args) {
|
|
2782
3010
|
try {
|
|
2783
3011
|
const structuredContent = await runtime.callTool(toolName, args);
|
|
@@ -4863,18 +5091,18 @@ async function startPolling(config, adapters2, signal) {
|
|
|
4863
5091
|
// apps/runner/src/local-ui.ts
|
|
4864
5092
|
import crypto4 from "node:crypto";
|
|
4865
5093
|
import fs2 from "node:fs/promises";
|
|
4866
|
-
import { createServer } from "node:http";
|
|
5094
|
+
import { createServer as createServer2 } from "node:http";
|
|
4867
5095
|
import path2 from "node:path";
|
|
4868
5096
|
async function startLocalUiServer(options = {}) {
|
|
4869
5097
|
const host = options.host ?? "127.0.0.1";
|
|
4870
5098
|
if (!isLocalHost(host) && options.allowRemoteBind !== true) {
|
|
4871
|
-
throw new Error("synapsor ui binds to localhost by default. Use --allow-remote-bind only for an intentional trusted local-network demo.");
|
|
5099
|
+
throw new Error("synapsor-runner ui binds to localhost by default. Use --allow-remote-bind only for an intentional trusted local-network demo.");
|
|
4872
5100
|
}
|
|
4873
5101
|
const configPath = path2.resolve(options.configPath ?? "synapsor.runner.json");
|
|
4874
5102
|
const storePath = path2.resolve(options.storePath ?? "./.synapsor/local.db");
|
|
4875
5103
|
const token = options.token ?? crypto4.randomBytes(24).toString("base64url");
|
|
4876
5104
|
const csrfToken = options.csrfToken ?? crypto4.randomBytes(24).toString("base64url");
|
|
4877
|
-
const server =
|
|
5105
|
+
const server = createServer2(async (request, response) => {
|
|
4878
5106
|
try {
|
|
4879
5107
|
await handleRequest({ request, response, configPath, storePath, token, csrfToken, tour: options.tour === true });
|
|
4880
5108
|
} catch (error) {
|
|
@@ -5450,7 +5678,7 @@ function shellQuote(value) {
|
|
|
5450
5678
|
return /^[A-Za-z0-9_./:@=-]+$/.test(text) ? text : "'" + text.replace(/'/g, "'\\\\''") + "'";
|
|
5451
5679
|
}
|
|
5452
5680
|
function trustedApplyCommand(proposalId) {
|
|
5453
|
-
return "synapsor apply " + shellQuote(proposalId) + " --config " + shellQuote(configPath) + " --store " + shellQuote(storePath);
|
|
5681
|
+
return "synapsor-runner apply " + shellQuote(proposalId) + " --config " + shellQuote(configPath) + " --store " + shellQuote(storePath);
|
|
5454
5682
|
}
|
|
5455
5683
|
async function loadSummary() {
|
|
5456
5684
|
const payload = await api("/api/summary");
|
|
@@ -5482,7 +5710,7 @@ async function loadProposals() {
|
|
|
5482
5710
|
const payload = await api("/api/proposals");
|
|
5483
5711
|
const root = byId("proposals"); root.replaceChildren(text("h2", "Proposals"));
|
|
5484
5712
|
if (payload.proposals.length === 0) {
|
|
5485
|
-
root.append(text("p", "No proposals in the local store yet. Run synapsor mcp serve and have an agent propose a change."));
|
|
5713
|
+
root.append(text("p", "No proposals in the local store yet. Run synapsor-runner mcp serve and have an agent propose a change."));
|
|
5486
5714
|
state.firstId = null;
|
|
5487
5715
|
return;
|
|
5488
5716
|
}
|
|
@@ -5831,10 +6059,10 @@ async function main(argv) {
|
|
|
5831
6059
|
return 0;
|
|
5832
6060
|
}
|
|
5833
6061
|
if (!isKnownTopLevelCommand(command)) {
|
|
5834
|
-
process2.stderr.write(`Unknown command:
|
|
6062
|
+
process2.stderr.write(`Unknown command: ${cliCommandName()} ${command}
|
|
5835
6063
|
|
|
5836
6064
|
Try:
|
|
5837
|
-
|
|
6065
|
+
${cliCommandName()} --help
|
|
5838
6066
|
`);
|
|
5839
6067
|
return 2;
|
|
5840
6068
|
}
|
|
@@ -5872,10 +6100,10 @@ synapsor --help
|
|
|
5872
6100
|
if (command === "store") return storeCommand(rest);
|
|
5873
6101
|
if (command === "shadow") return shadow(rest);
|
|
5874
6102
|
if (command === "ui") return ui(rest);
|
|
5875
|
-
process2.stderr.write(`Unknown command:
|
|
6103
|
+
process2.stderr.write(`Unknown command: ${cliCommandName()} ${command}
|
|
5876
6104
|
|
|
5877
6105
|
Try:
|
|
5878
|
-
|
|
6106
|
+
${cliCommandName()} --help
|
|
5879
6107
|
`);
|
|
5880
6108
|
return 2;
|
|
5881
6109
|
}
|
|
@@ -5940,7 +6168,9 @@ async function runInitWizard(args, options = {}) {
|
|
|
5940
6168
|
const ask = options.ask ?? askTtyQuestion;
|
|
5941
6169
|
const stdout = options.stdout ?? process2.stdout;
|
|
5942
6170
|
stdout.write("Synapsor Runner guided init\n");
|
|
5943
|
-
stdout.write("Use a staging or disposable Postgres/MySQL database first. The wizard stores environment-variable names, not credentials.\n
|
|
6171
|
+
stdout.write("Use a staging or disposable Postgres/MySQL database first. The wizard stores environment-variable names, not credentials.\n");
|
|
6172
|
+
stdout.write("Flow: inspect database -> create trusted context -> create capability -> expose MCP tool.\n\n");
|
|
6173
|
+
stdout.write("Step 1: Inspect database metadata\n");
|
|
5944
6174
|
const engineInput = await askChoice(ask, "Engine", optionalArg(args, "--engine") ?? "auto", ["postgres", "mysql", "auto"]);
|
|
5945
6175
|
const databaseInput = databaseInputFromArgs(args);
|
|
5946
6176
|
if (databaseInput.inlineUrl) {
|
|
@@ -5963,21 +6193,27 @@ async function runInitWizard(args, options = {}) {
|
|
|
5963
6193
|
stdout.write(` - ${table2.schema}.${table2.name} (${table2.type}, pk=${table2.primary_key.join(",") || "none"}, tenant=${table2.suggestions.tenant_columns.join(",") || "none"})
|
|
5964
6194
|
`);
|
|
5965
6195
|
}
|
|
5966
|
-
|
|
6196
|
+
stdout.write("\nStep 2: Create trusted context\n");
|
|
6197
|
+
stdout.write("Choose the source object and trusted scope. Tenant and principal values come from your backend/session, not from the model.\n");
|
|
6198
|
+
const tableName = await askDefault(ask, "Source table/view for this context", optionalArg(args, "--table") ?? tables[0]?.name ?? "");
|
|
5967
6199
|
const table = findInspectionTable(inspection, tableName, schema);
|
|
5968
6200
|
if (!table) throw new Error(`table not found in inspection: ${schema}.${tableName}`);
|
|
5969
6201
|
const columns = table.columns.map((column) => column.name);
|
|
5970
6202
|
const primaryKey = await askColumn(ask, "Primary-key column", optionalArg(args, "--primary-key") ?? table.primary_key[0] ?? inferPrimaryKeyCandidate(table), columns);
|
|
5971
6203
|
const suggestedTenant = optionalArg(args, "--tenant-key") ?? table.suggestions.tenant_columns[0];
|
|
5972
|
-
const tenantAnswer = await askDefault(ask, "
|
|
6204
|
+
const tenantAnswer = await askDefault(ask, "Trusted tenant/scope column", suggestedTenant ?? "");
|
|
5973
6205
|
const singleTenantDev = !tenantAnswer && (await askDefault(ask, "No tenant column selected. Type yes to mark this as a single-tenant dev source", "no")).toLowerCase() === "yes";
|
|
5974
6206
|
if (!tenantAnswer && !singleTenantDev) throw new Error("tenant/scope column is required unless single-tenant dev source is explicitly confirmed");
|
|
5975
6207
|
if (tenantAnswer && !columns.includes(tenantAnswer)) throw new Error(`tenant column ${tenantAnswer} does not exist on ${table.schema}.${table.name}`);
|
|
5976
|
-
const
|
|
6208
|
+
const tenantEnv = await askEnvName(ask, "Trusted tenant env var", optionalArg(args, "--tenant-env") ?? "SYNAPSOR_TENANT_ID");
|
|
6209
|
+
const principalEnv = await askEnvName(ask, "Trusted principal env var", optionalArg(args, "--principal-env") ?? "SYNAPSOR_PRINCIPAL");
|
|
6210
|
+
stdout.write("\nStep 3: Create capability\n");
|
|
6211
|
+
stdout.write("Name the semantic tool the model can call. Table, key, visible fields, and mode define what that capability can do.\n");
|
|
6212
|
+
const mode = await askChoice(ask, "Capability mode", optionalArg(args, "--mode") ?? "read_only", ["read_only", "shadow", "review"]);
|
|
5977
6213
|
const conflictAnswer = mode === "read_only" ? optionalArg(args, "--conflict-column") ?? "" : await askDefault(ask, "Conflict/version column", optionalArg(args, "--conflict-column") ?? table.suggestions.conflict_columns[0] ?? "");
|
|
5978
6214
|
if (conflictAnswer && !columns.includes(conflictAnswer)) throw new Error(`conflict column ${conflictAnswer} does not exist on ${table.schema}.${table.name}`);
|
|
5979
6215
|
const defaultVisible = table.suggestions.default_visible_columns.join(",");
|
|
5980
|
-
let visibleColumns = parseColumnList(await askDefault(ask, "
|
|
6216
|
+
let visibleColumns = parseColumnList(await askDefault(ask, "Capability read-visible columns", optionalArg(args, "--visible-columns") ?? defaultVisible));
|
|
5981
6217
|
ensureColumnsExist(visibleColumns, columns, "visible");
|
|
5982
6218
|
if (mode !== "read_only" && !conflictAnswer) {
|
|
5983
6219
|
const weak = await askDefault(ask, "No conflict/version column selected. Type yes to continue with a weak guard", "no");
|
|
@@ -6045,8 +6281,6 @@ async function runInitWizard(args, options = {}) {
|
|
|
6045
6281
|
const namespace = await askDefault(ask, "Capability namespace", optionalArg(args, "--namespace") ?? recipeSpec?.namespace ?? "source");
|
|
6046
6282
|
const objectName = await askDefault(ask, "Business object name", optionalArg(args, "--object-name") ?? recipeSpec?.object_name ?? safeObjectName(table.name));
|
|
6047
6283
|
const lookupArg = await askDefault(ask, "Model-visible object id argument", optionalArg(args, "--lookup-arg") ?? recipeSpec?.lookup_arg ?? `${objectName}_id`);
|
|
6048
|
-
const tenantEnv = await askEnvName(ask, "Trusted tenant env var", optionalArg(args, "--tenant-env") ?? "SYNAPSOR_TENANT_ID");
|
|
6049
|
-
const principalEnv = await askEnvName(ask, "Trusted principal env var", optionalArg(args, "--principal-env") ?? "SYNAPSOR_PRINCIPAL");
|
|
6050
6284
|
const writeUrlEnv = mode === "review" ? await askEnvName(ask, "Write URL env var for trusted apply path", optionalArg(args, "--write-url-env") ?? "SYNAPSOR_DATABASE_WRITE_URL") : optionalArg(args, "--write-url-env") ?? "SYNAPSOR_DATABASE_WRITE_URL";
|
|
6051
6285
|
const approvalRole = mode === "read_only" ? "local_reviewer" : await askDefault(ask, "Required approval role", optionalArg(args, "--approval-role") ?? recipeSpec?.approval?.required_role ?? "local_reviewer");
|
|
6052
6286
|
const spec = {
|
|
@@ -6084,6 +6318,8 @@ async function runInitWizard(args, options = {}) {
|
|
|
6084
6318
|
const generated = generateRunnerConfigFromSpec(spec);
|
|
6085
6319
|
const tools2 = generated.config.capabilities.map((capability) => `${capability.name} (${capability.kind})`);
|
|
6086
6320
|
stdout.write("\nPreview:\n");
|
|
6321
|
+
stdout.write(` trusted context: tenant from ${tenantEnv}${singleTenantDev ? " (single-tenant dev source)" : tenantAnswer ? ` via ${tenantAnswer}` : ""}; principal from ${principalEnv}
|
|
6322
|
+
`);
|
|
6087
6323
|
stdout.write(` source: ${inspection.engine} ${table.schema}.${table.name}
|
|
6088
6324
|
`);
|
|
6089
6325
|
stdout.write(` mode: ${mode}
|
|
@@ -7741,7 +7977,7 @@ async function cloudConnect(args) {
|
|
|
7741
7977
|
return 1;
|
|
7742
7978
|
}
|
|
7743
7979
|
const runnerId = String(parsed.cloud.runner_id || process2.env.SYNAPSOR_RUNNER_ID || "synapsor_runner_local").trim();
|
|
7744
|
-
const runnerVersion = String(parsed.cloud.runner_version || process2.env.npm_package_version || "0.1.0-alpha.
|
|
7980
|
+
const runnerVersion = String(parsed.cloud.runner_version || process2.env.npm_package_version || "0.1.0-alpha.6").trim();
|
|
7745
7981
|
const engines = normalizeEngines(parsed.cloud.engines);
|
|
7746
7982
|
const capabilities = normalizeCapabilities(parsed.cloud.capabilities);
|
|
7747
7983
|
const client = new ControlPlaneClient({
|
|
@@ -7791,6 +8027,7 @@ async function cloudConnect(args) {
|
|
|
7791
8027
|
async function mcp(args) {
|
|
7792
8028
|
const [subcommand, ...rest] = args;
|
|
7793
8029
|
if (subcommand === "serve") return mcpServe(rest);
|
|
8030
|
+
if (subcommand === "serve-http") return mcpServeHttp(rest);
|
|
7794
8031
|
if (subcommand === "audit") return mcpAudit(rest);
|
|
7795
8032
|
if (subcommand === "config") return mcpConfig(rest);
|
|
7796
8033
|
if (subcommand === "configure") return mcpConfigure(rest);
|
|
@@ -7836,24 +8073,98 @@ async function onboard(args) {
|
|
|
7836
8073
|
}
|
|
7837
8074
|
async function demo(args) {
|
|
7838
8075
|
const [subcommand] = args;
|
|
8076
|
+
if (subcommand === "inspect") return demoInspect(args.slice(1));
|
|
7839
8077
|
if (subcommand && !subcommand.startsWith("-") && subcommand !== "reference-support-billing") {
|
|
7840
8078
|
usage(["demo"]);
|
|
7841
8079
|
return 2;
|
|
7842
8080
|
}
|
|
7843
|
-
if (args.includes("--quick")) return quickDemo();
|
|
8081
|
+
if (args.includes("--quick")) return quickDemo(args);
|
|
7844
8082
|
return prepareReferenceDemo(args);
|
|
7845
8083
|
}
|
|
7846
|
-
async function quickDemo() {
|
|
8084
|
+
async function quickDemo(args) {
|
|
8085
|
+
const allowed = /* @__PURE__ */ new Set(["--quick", "--guided", "--no-interactive", "--details", "--json"]);
|
|
8086
|
+
assertKnownOptions(args, allowed, "demo --quick");
|
|
7847
8087
|
const seeded = await seedQuickDemoStore(quickDemoStorePath);
|
|
7848
|
-
|
|
7849
|
-
|
|
8088
|
+
const summary = quickDemoSummary(seeded);
|
|
8089
|
+
if (args.includes("--json")) {
|
|
8090
|
+
process2.stdout.write(`${JSON.stringify(summary, null, 2)}
|
|
8091
|
+
`);
|
|
8092
|
+
return 0;
|
|
8093
|
+
}
|
|
8094
|
+
if (args.includes("--details")) {
|
|
8095
|
+
process2.stdout.write(formatQuickDemoDetails(seeded));
|
|
8096
|
+
return 0;
|
|
8097
|
+
}
|
|
8098
|
+
const canPauseForInput = Boolean(process2.stdin.isTTY && process2.stdout.isTTY && !process2.env.CI && !process2.env.VITEST);
|
|
8099
|
+
const forceConcise = args.includes("--no-interactive");
|
|
8100
|
+
const forceGuided = args.includes("--guided") && !forceConcise;
|
|
8101
|
+
const shouldGuide = forceGuided || canPauseForInput && !forceConcise;
|
|
8102
|
+
if (shouldGuide) {
|
|
8103
|
+
await runGuidedQuickDemo(seeded, { pause: canPauseForInput });
|
|
8104
|
+
return 0;
|
|
8105
|
+
}
|
|
8106
|
+
process2.stdout.write(formatQuickDemoConcise(seeded));
|
|
8107
|
+
return 0;
|
|
8108
|
+
}
|
|
8109
|
+
async function demoInspect(args) {
|
|
8110
|
+
const allowed = /* @__PURE__ */ new Set(["--npx", "--json"]);
|
|
8111
|
+
assertKnownOptions(args, allowed, "demo inspect");
|
|
8112
|
+
const seeded = await seedQuickDemoStore(quickDemoStorePath);
|
|
8113
|
+
const commands = quickDemoInspectCommands(args.includes("--npx"));
|
|
8114
|
+
if (args.includes("--json")) {
|
|
8115
|
+
process2.stdout.write(`${JSON.stringify({ ...quickDemoSummary(seeded), commands }, null, 2)}
|
|
8116
|
+
`);
|
|
8117
|
+
return 0;
|
|
8118
|
+
}
|
|
8119
|
+
process2.stdout.write(formatQuickDemoInspect(commands));
|
|
8120
|
+
return 0;
|
|
8121
|
+
}
|
|
8122
|
+
function quickDemoSummary(seeded) {
|
|
8123
|
+
return {
|
|
8124
|
+
mode: "fixture_only",
|
|
8125
|
+
store: quickDemoStorePath,
|
|
8126
|
+
proposal_id: seeded.proposal_id,
|
|
8127
|
+
evidence_bundle_id: seeded.evidence_bundle_id,
|
|
8128
|
+
replay_id: seeded.replay_id,
|
|
8129
|
+
model_tool: "billing.propose_late_fee_waiver",
|
|
8130
|
+
business_object: "invoice:INV-3001",
|
|
8131
|
+
proposed_change: { late_fee_cents: { before: 5500, after: 0 } },
|
|
8132
|
+
source_database_changed: false,
|
|
8133
|
+
approval: "required outside MCP"
|
|
8134
|
+
};
|
|
8135
|
+
}
|
|
8136
|
+
function formatQuickDemoConcise(seeded) {
|
|
8137
|
+
void seeded;
|
|
8138
|
+
return [
|
|
8139
|
+
"Synapsor quick demo complete.",
|
|
8140
|
+
"",
|
|
8141
|
+
"The model asked to waive a late fee:",
|
|
8142
|
+
'billing.propose_late_fee_waiver(invoice_id="INV-3001")',
|
|
8143
|
+
"",
|
|
8144
|
+
"Result:",
|
|
8145
|
+
"* proposal created",
|
|
8146
|
+
"* source DB changed: no",
|
|
8147
|
+
"* approval required outside MCP",
|
|
8148
|
+
"* evidence + replay saved locally",
|
|
8149
|
+
"",
|
|
8150
|
+
"Local ledger:",
|
|
8151
|
+
quickDemoStorePath,
|
|
8152
|
+
"",
|
|
8153
|
+
"Next:",
|
|
8154
|
+
`${cliCommandName()} demo inspect`,
|
|
8155
|
+
""
|
|
8156
|
+
].join("\n");
|
|
8157
|
+
}
|
|
8158
|
+
function formatQuickDemoDetails(seeded) {
|
|
8159
|
+
return [
|
|
8160
|
+
"Synapsor Runner quick demo is ready.",
|
|
7850
8161
|
"",
|
|
7851
8162
|
"This is a fixture-only first look. It does not start Docker, connect a database,",
|
|
7852
8163
|
"or require an MCP client. It writes an inspectable local ledger fixture to:",
|
|
7853
8164
|
quickDemoStorePath,
|
|
7854
8165
|
"",
|
|
7855
8166
|
"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",
|
|
8167
|
+
"follow-up commands with: npx -y -p @synapsor/runner@alpha synapsor-runner",
|
|
7857
8168
|
"",
|
|
7858
8169
|
"Raw MCP shape:",
|
|
7859
8170
|
"execute_sql(sql: string)",
|
|
@@ -7881,25 +8192,209 @@ async function quickDemo() {
|
|
|
7881
8192
|
"",
|
|
7882
8193
|
"Replay:",
|
|
7883
8194
|
`${seeded.replay_id} captures the local proposal, evidence handle, query audit, and events.`,
|
|
8195
|
+
`Proposal id: ${seeded.proposal_id}`,
|
|
8196
|
+
`Evidence id: ${seeded.evidence_bundle_id}`,
|
|
8197
|
+
"",
|
|
8198
|
+
"What this proves:",
|
|
8199
|
+
"* The model gets a business tool, not raw SQL.",
|
|
8200
|
+
"* The model created a proposal, not a write.",
|
|
8201
|
+
"* Source DB changed: no.",
|
|
8202
|
+
"* You can inspect evidence and replay locally.",
|
|
7884
8203
|
"",
|
|
7885
|
-
"
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
`${cliCommandName()} replay show --proposal ${seeded.proposal_id} --store ${quickDemoStorePath}`,
|
|
8204
|
+
"Try:",
|
|
8205
|
+
`1. ${cliCommandName()} proposals show latest --store ${quickDemoStorePath}`,
|
|
8206
|
+
`2. ${cliCommandName()} activity search --object invoice:INV-3001 --store ${quickDemoStorePath}`,
|
|
8207
|
+
`3. ${cliCommandName()} replay show latest --store ${quickDemoStorePath}`,
|
|
7890
8208
|
"",
|
|
7891
|
-
"
|
|
7892
|
-
`${cliCommandName()}
|
|
7893
|
-
`${cliCommandName()} replay show latest --store ${quickDemoStorePath}`,
|
|
8209
|
+
"For full reviewer detail:",
|
|
8210
|
+
`${cliCommandName()} replay show latest --details --store ${quickDemoStorePath}`,
|
|
7894
8211
|
"",
|
|
7895
8212
|
"For real guarded writeback against disposable Postgres:",
|
|
7896
8213
|
`${cliCommandName()} demo`,
|
|
8214
|
+
""
|
|
8215
|
+
].join("\n");
|
|
8216
|
+
}
|
|
8217
|
+
async function runGuidedQuickDemo(seeded, options) {
|
|
8218
|
+
const screens = quickDemoGuidedScreens(seeded);
|
|
8219
|
+
for (const [index, screen] of screens.entries()) {
|
|
8220
|
+
printStep(screen.title, screen.body, index + 1, screens.length);
|
|
8221
|
+
if (index < screens.length - 1) {
|
|
8222
|
+
await waitForEnter("Press Enter to continue...", options);
|
|
8223
|
+
}
|
|
8224
|
+
}
|
|
8225
|
+
}
|
|
8226
|
+
function quickDemoGuidedScreens(seeded) {
|
|
8227
|
+
return [
|
|
8228
|
+
{
|
|
8229
|
+
title: "Synapsor Runner quick demo",
|
|
8230
|
+
body: [
|
|
8231
|
+
"This teaches the Synapsor safety model without Docker, a database, or an MCP client.",
|
|
8232
|
+
"",
|
|
8233
|
+
"It also creates a local fixture ledger you can inspect."
|
|
8234
|
+
]
|
|
8235
|
+
},
|
|
8236
|
+
{
|
|
8237
|
+
title: "The risky default",
|
|
8238
|
+
body: [
|
|
8239
|
+
"Many database MCP demos expose this:",
|
|
8240
|
+
"",
|
|
8241
|
+
"execute_sql(sql: string)",
|
|
8242
|
+
"",
|
|
8243
|
+
"That means the model can receive database authority directly."
|
|
8244
|
+
]
|
|
8245
|
+
},
|
|
8246
|
+
{
|
|
8247
|
+
title: "The Synapsor boundary",
|
|
8248
|
+
body: [
|
|
8249
|
+
"Synapsor gives the model business tools instead:",
|
|
8250
|
+
"",
|
|
8251
|
+
"billing.inspect_invoice(invoice_id)",
|
|
8252
|
+
"billing.propose_late_fee_waiver(invoice_id, reason)",
|
|
8253
|
+
"",
|
|
8254
|
+
"The model can ask for a business change.",
|
|
8255
|
+
"It cannot commit the write."
|
|
8256
|
+
]
|
|
8257
|
+
},
|
|
8258
|
+
{
|
|
8259
|
+
title: "What the agent requested",
|
|
8260
|
+
body: [
|
|
8261
|
+
'billing.propose_late_fee_waiver(invoice_id="INV-3001")',
|
|
8262
|
+
"",
|
|
8263
|
+
"Proposed change:",
|
|
8264
|
+
"late_fee_cents: 5500 -> 0",
|
|
8265
|
+
"",
|
|
8266
|
+
"Source DB changed:",
|
|
8267
|
+
"no"
|
|
8268
|
+
]
|
|
8269
|
+
},
|
|
8270
|
+
{
|
|
8271
|
+
title: "What Synapsor saved",
|
|
8272
|
+
body: [
|
|
8273
|
+
"Synapsor saved:",
|
|
8274
|
+
"",
|
|
8275
|
+
"- proposal: what the model requested",
|
|
8276
|
+
"- evidence: what data supported it",
|
|
8277
|
+
"- query audit: what was read",
|
|
8278
|
+
"- replay: what happened later",
|
|
8279
|
+
"",
|
|
8280
|
+
`Proposal: ${seeded.proposal_id}`,
|
|
8281
|
+
`Evidence: ${seeded.evidence_bundle_id}`,
|
|
8282
|
+
`Replay: ${seeded.replay_id}`,
|
|
8283
|
+
"",
|
|
8284
|
+
"Local ledger:",
|
|
8285
|
+
quickDemoStorePath
|
|
8286
|
+
]
|
|
8287
|
+
},
|
|
8288
|
+
{
|
|
8289
|
+
title: "Inspect it",
|
|
8290
|
+
body: [
|
|
8291
|
+
"Run this next:",
|
|
8292
|
+
"",
|
|
8293
|
+
"npx -y -p @synapsor/runner@alpha synapsor-runner demo inspect",
|
|
8294
|
+
"",
|
|
8295
|
+
"demo inspect shows the proposal, evidence, activity search, and replay commands.",
|
|
8296
|
+
"",
|
|
8297
|
+
"If installed globally, use:",
|
|
8298
|
+
"synapsor-runner demo inspect"
|
|
8299
|
+
]
|
|
8300
|
+
},
|
|
8301
|
+
{
|
|
8302
|
+
title: "Next paths",
|
|
8303
|
+
body: [
|
|
8304
|
+
"Full disposable Postgres demo:",
|
|
8305
|
+
`${cliCommandName()} demo`,
|
|
8306
|
+
"",
|
|
8307
|
+
"Audit risky MCP database tools:",
|
|
8308
|
+
`${cliCommandName()} audit --example dangerous-db-mcp`,
|
|
8309
|
+
"",
|
|
8310
|
+
"Use your own staging DB:",
|
|
8311
|
+
'export DATABASE_URL="postgres://..."',
|
|
8312
|
+
`${cliCommandName()} inspect --from-env DATABASE_URL`,
|
|
8313
|
+
"",
|
|
8314
|
+
"Done. You just saw Synapsor's core boundary: business tools for the model, approval/writeback outside the model, and replay for inspection."
|
|
8315
|
+
]
|
|
8316
|
+
}
|
|
8317
|
+
];
|
|
8318
|
+
}
|
|
8319
|
+
function printStep(title, body, index, total) {
|
|
8320
|
+
const divider = "------------------------------------------------------------";
|
|
8321
|
+
process2.stdout.write([
|
|
8322
|
+
"",
|
|
8323
|
+
divider,
|
|
8324
|
+
`Step ${index}/${total}: ${title}`,
|
|
8325
|
+
divider,
|
|
7897
8326
|
"",
|
|
7898
|
-
|
|
7899
|
-
`${cliCommandName()} audit --example dangerous-db-mcp`,
|
|
8327
|
+
...body,
|
|
7900
8328
|
""
|
|
7901
8329
|
].join("\n"));
|
|
7902
|
-
|
|
8330
|
+
}
|
|
8331
|
+
async function waitForEnter(message, options) {
|
|
8332
|
+
if (!options.pause) {
|
|
8333
|
+
process2.stdout.write(`${message}
|
|
8334
|
+
`);
|
|
8335
|
+
return;
|
|
8336
|
+
}
|
|
8337
|
+
const rl = readline.createInterface({ input: process2.stdin, output: process2.stdout });
|
|
8338
|
+
try {
|
|
8339
|
+
await rl.question(`${message} `);
|
|
8340
|
+
} finally {
|
|
8341
|
+
rl.close();
|
|
8342
|
+
}
|
|
8343
|
+
}
|
|
8344
|
+
function quickDemoInspectCommands(useNpx) {
|
|
8345
|
+
const cmd = useNpx ? "npx -y -p @synapsor/runner@alpha synapsor-runner" : cliCommandName();
|
|
8346
|
+
return [
|
|
8347
|
+
{
|
|
8348
|
+
label: "Proposal summary",
|
|
8349
|
+
description: "See what the model asked to change.",
|
|
8350
|
+
command: `${cmd} proposals show latest --store ${quickDemoStorePath}`
|
|
8351
|
+
},
|
|
8352
|
+
{
|
|
8353
|
+
label: "Evidence",
|
|
8354
|
+
description: "Inspect rows and evidence items captured for the proposal.",
|
|
8355
|
+
command: `${cmd} evidence show ev_quick_INV_3001 --store ${quickDemoStorePath}`
|
|
8356
|
+
},
|
|
8357
|
+
{
|
|
8358
|
+
label: "Activity search",
|
|
8359
|
+
description: "Find the local ledger records for invoice INV-3001.",
|
|
8360
|
+
command: `${cmd} activity search --object invoice:INV-3001 --store ${quickDemoStorePath}`
|
|
8361
|
+
},
|
|
8362
|
+
{
|
|
8363
|
+
label: "Replay",
|
|
8364
|
+
description: "Replay the local proposal/evidence/audit story.",
|
|
8365
|
+
command: `${cmd} replay show latest --store ${quickDemoStorePath}`
|
|
8366
|
+
},
|
|
8367
|
+
{
|
|
8368
|
+
label: "Approve outside MCP",
|
|
8369
|
+
description: "Approve the proposal through the local operator boundary.",
|
|
8370
|
+
command: `${cmd} proposals approve latest --yes --store ${quickDemoStorePath}`
|
|
8371
|
+
},
|
|
8372
|
+
{
|
|
8373
|
+
label: "Full Docker-backed demo",
|
|
8374
|
+
description: "Run the disposable Postgres-backed proof.",
|
|
8375
|
+
command: `${cmd} demo`
|
|
8376
|
+
},
|
|
8377
|
+
{
|
|
8378
|
+
label: "Audit risky MCP database tools",
|
|
8379
|
+
description: "Review common dangerous MCP tool shapes.",
|
|
8380
|
+
command: `${cmd} audit --example dangerous-db-mcp`
|
|
8381
|
+
}
|
|
8382
|
+
];
|
|
8383
|
+
}
|
|
8384
|
+
function formatQuickDemoInspect(commands) {
|
|
8385
|
+
return [
|
|
8386
|
+
"Quick demo inspection",
|
|
8387
|
+
"",
|
|
8388
|
+
"Local ledger:",
|
|
8389
|
+
quickDemoStorePath,
|
|
8390
|
+
"",
|
|
8391
|
+
...commands.flatMap((item, index) => [
|
|
8392
|
+
`${index + 1}. ${item.label}`,
|
|
8393
|
+
` ${item.description}`,
|
|
8394
|
+
` ${item.command}`,
|
|
8395
|
+
""
|
|
8396
|
+
])
|
|
8397
|
+
].join("\n");
|
|
7903
8398
|
}
|
|
7904
8399
|
async function seedQuickDemoStore(storePath) {
|
|
7905
8400
|
const resolved = path3.resolve(storePath);
|
|
@@ -8062,6 +8557,41 @@ async function mcpServe(args) {
|
|
|
8062
8557
|
});
|
|
8063
8558
|
return 0;
|
|
8064
8559
|
}
|
|
8560
|
+
async function mcpServeHttp(args) {
|
|
8561
|
+
const configPath = optionalArg(args, "--config") ?? process2.env.SYNAPSOR_MCP_CONFIG;
|
|
8562
|
+
const readOnly = args.includes("--read-only");
|
|
8563
|
+
const config = readOnly ? { ...await readRuntimeConfig(configPath ?? defaultConfigPath), mode: "read_only" } : void 0;
|
|
8564
|
+
const host = optionalArg(args, "--host") ?? "127.0.0.1";
|
|
8565
|
+
const port = Number(optionalArg(args, "--port") ?? "8765");
|
|
8566
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
8567
|
+
throw new Error("--port must be an integer from 1 to 65535");
|
|
8568
|
+
}
|
|
8569
|
+
if (host === "0.0.0.0") {
|
|
8570
|
+
process2.stderr.write("Warning: binding Synapsor Runner HTTP MCP to 0.0.0.0 exposes model-facing tools on the network. Use TLS, private networking, authentication, and rate limits.\n");
|
|
8571
|
+
}
|
|
8572
|
+
const server = await startHttpMcpServer({
|
|
8573
|
+
configPath,
|
|
8574
|
+
config,
|
|
8575
|
+
storePath: optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE,
|
|
8576
|
+
host,
|
|
8577
|
+
port,
|
|
8578
|
+
authTokenEnv: optionalArg(args, "--auth-token-env") ?? "SYNAPSOR_RUNNER_HTTP_TOKEN",
|
|
8579
|
+
devNoAuth: args.includes("--dev-no-auth"),
|
|
8580
|
+
corsOrigin: optionalArg(args, "--cors-origin")
|
|
8581
|
+
});
|
|
8582
|
+
process2.stderr.write("Press Ctrl+C to stop.\n");
|
|
8583
|
+
await new Promise((resolve) => {
|
|
8584
|
+
const stop = async () => {
|
|
8585
|
+
process2.off("SIGINT", stop);
|
|
8586
|
+
process2.off("SIGTERM", stop);
|
|
8587
|
+
await server.close();
|
|
8588
|
+
resolve();
|
|
8589
|
+
};
|
|
8590
|
+
process2.once("SIGINT", stop);
|
|
8591
|
+
process2.once("SIGTERM", stop);
|
|
8592
|
+
});
|
|
8593
|
+
return 0;
|
|
8594
|
+
}
|
|
8065
8595
|
async function mcpAudit(args) {
|
|
8066
8596
|
const format = optionalArg(args, "--format") ?? (args.includes("--json") ? "json" : "text");
|
|
8067
8597
|
if (!["text", "json", "markdown"].includes(format)) {
|
|
@@ -9018,6 +9548,7 @@ async function proposalsList(args) {
|
|
|
9018
9548
|
}
|
|
9019
9549
|
}
|
|
9020
9550
|
async function proposalsShow(args) {
|
|
9551
|
+
assertKnownOptions(args, showAllowedOptions, "proposals show");
|
|
9021
9552
|
const proposalId = positional(args, 0);
|
|
9022
9553
|
if (!proposalId) throw new Error("proposals show requires <proposal_id>");
|
|
9023
9554
|
const store = await openLocalStore(args);
|
|
@@ -9030,12 +9561,12 @@ async function proposalsShow(args) {
|
|
|
9030
9561
|
if (args.includes("--json")) {
|
|
9031
9562
|
process2.stdout.write(`${JSON.stringify(payload, null, 2)}
|
|
9032
9563
|
`);
|
|
9033
|
-
} else {
|
|
9564
|
+
} else if (showDetails(args)) {
|
|
9034
9565
|
process2.stdout.write(formatProposalDetail(proposal, evidence2?.items.length));
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
|
|
9566
|
+
process2.stdout.write(formatProposalEventDetail(payload.events));
|
|
9567
|
+
if (args.includes("--debug")) process2.stdout.write(formatProposalDebug(proposal, optionalArg(args, "--store")));
|
|
9568
|
+
} else {
|
|
9569
|
+
process2.stdout.write(formatProposalFirstLook(proposal, evidence2?.items.length, proposalId, storeOptionSuffix(args)));
|
|
9039
9570
|
}
|
|
9040
9571
|
return 0;
|
|
9041
9572
|
} finally {
|
|
@@ -9151,7 +9682,8 @@ async function evidenceShow(args) {
|
|
|
9151
9682
|
if (!evidence2) throw new Error(`evidence bundle not found: ${evidenceId}`);
|
|
9152
9683
|
if (args.includes("--json")) process2.stdout.write(`${JSON.stringify(evidence2, null, 2)}
|
|
9153
9684
|
`);
|
|
9154
|
-
else process2.stdout.write(formatEvidenceDetail(evidence2));
|
|
9685
|
+
else if (showDetails(args)) process2.stdout.write(formatEvidenceDetail(evidence2));
|
|
9686
|
+
else process2.stdout.write(formatEvidenceFirstLook(evidence2, storeOptionSuffix(args)));
|
|
9155
9687
|
return 0;
|
|
9156
9688
|
} finally {
|
|
9157
9689
|
store.close();
|
|
@@ -9187,7 +9719,7 @@ async function queryAuditList(args) {
|
|
|
9187
9719
|
if (args.includes("--json")) process2.stdout.write(`${JSON.stringify({ query_audit: rows }, null, 2)}
|
|
9188
9720
|
`);
|
|
9189
9721
|
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));
|
|
9722
|
+
else for (const row of rows) process2.stdout.write(formatQueryAuditSummary(row, showDetails(args), storeOptionSuffix(args)));
|
|
9191
9723
|
return 0;
|
|
9192
9724
|
} finally {
|
|
9193
9725
|
store.close();
|
|
@@ -9203,7 +9735,7 @@ async function queryAuditShow(args) {
|
|
|
9203
9735
|
if (!row) throw new Error(`query audit record not found: ${auditId}`);
|
|
9204
9736
|
if (args.includes("--json")) process2.stdout.write(`${JSON.stringify(row, null, 2)}
|
|
9205
9737
|
`);
|
|
9206
|
-
else process2.stdout.write(formatQueryAuditDetail(row));
|
|
9738
|
+
else process2.stdout.write(showDetails(args) ? formatQueryAuditDetail(row) : formatQueryAuditFirstLook(row, storeOptionSuffix(args)));
|
|
9207
9739
|
return 0;
|
|
9208
9740
|
} finally {
|
|
9209
9741
|
store.close();
|
|
@@ -9254,7 +9786,7 @@ async function receiptsShow(args) {
|
|
|
9254
9786
|
if (!receipt) throw new Error(`writeback receipt not found: ${receiptId}`);
|
|
9255
9787
|
if (args.includes("--json")) process2.stdout.write(`${JSON.stringify(receipt, null, 2)}
|
|
9256
9788
|
`);
|
|
9257
|
-
else process2.stdout.write(formatReceiptDetail(receipt));
|
|
9789
|
+
else process2.stdout.write(showDetails(args) ? formatReceiptDetail(receipt) : formatReceiptFirstLook(receipt, storeOptionSuffix(args)));
|
|
9258
9790
|
return 0;
|
|
9259
9791
|
} finally {
|
|
9260
9792
|
store.close();
|
|
@@ -9295,8 +9827,11 @@ async function replayShow(args) {
|
|
|
9295
9827
|
if (args.includes("--json")) {
|
|
9296
9828
|
process2.stdout.write(`${JSON.stringify(replayRecord, null, 2)}
|
|
9297
9829
|
`);
|
|
9298
|
-
} else {
|
|
9830
|
+
} else if (showDetails(args)) {
|
|
9299
9831
|
process2.stdout.write(formatReplayDetail(replayRecord));
|
|
9832
|
+
if (args.includes("--debug")) process2.stdout.write(formatReplayDebug(replayRecord, optionalArg(args, "--store")));
|
|
9833
|
+
} else {
|
|
9834
|
+
process2.stdout.write(formatReplayFirstLook(replayRecord, storeOptionSuffix(args)));
|
|
9300
9835
|
}
|
|
9301
9836
|
return 0;
|
|
9302
9837
|
} finally {
|
|
@@ -9363,7 +9898,8 @@ async function activitySearch(args) {
|
|
|
9363
9898
|
process2.stdout.write(`Found ${sorted.length} local interaction${sorted.length === 1 ? "" : "s"}
|
|
9364
9899
|
|
|
9365
9900
|
`);
|
|
9366
|
-
sorted.forEach((item, index) => process2.stdout.write(formatActivityItem(item, index + 1)));
|
|
9901
|
+
sorted.forEach((item, index) => process2.stdout.write(formatActivityItem(item, index + 1, showDetails(args))));
|
|
9902
|
+
process2.stdout.write(formatActivityNext(sorted, storeOptionSuffix(args)));
|
|
9367
9903
|
}
|
|
9368
9904
|
return 0;
|
|
9369
9905
|
} finally {
|
|
@@ -9418,7 +9954,7 @@ async function storePrune(args) {
|
|
|
9418
9954
|
store.close();
|
|
9419
9955
|
}
|
|
9420
9956
|
}
|
|
9421
|
-
var commonReadOptions = /* @__PURE__ */ new Set(["--store", "--json"]);
|
|
9957
|
+
var commonReadOptions = /* @__PURE__ */ new Set(["--store", "--json", "--details", "--debug"]);
|
|
9422
9958
|
var showAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions]);
|
|
9423
9959
|
var exportAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions, "--output", "--out", "--format", "--evidence", "--audit"]);
|
|
9424
9960
|
var proposalListAllowedOptions = /* @__PURE__ */ new Set([
|
|
@@ -9956,11 +10492,13 @@ function firstPositional(args) {
|
|
|
9956
10492
|
"--approval-role",
|
|
9957
10493
|
"--actor",
|
|
9958
10494
|
"--action",
|
|
10495
|
+
"--auth-token-env",
|
|
9959
10496
|
"--audit",
|
|
9960
10497
|
"--bearer-env",
|
|
9961
10498
|
"--capability",
|
|
9962
10499
|
"--config",
|
|
9963
10500
|
"--conflict-column",
|
|
10501
|
+
"--cors-origin",
|
|
9964
10502
|
"--database-url-env",
|
|
9965
10503
|
"--destination",
|
|
9966
10504
|
"--engine",
|
|
@@ -10098,6 +10636,38 @@ function formatProposalSummary(proposal) {
|
|
|
10098
10636
|
` tenant: ${proposal.tenant_id} source changed: ${proposal.source_database_mutated ? "yes" : "no"}`
|
|
10099
10637
|
].join("\n") + "\n";
|
|
10100
10638
|
}
|
|
10639
|
+
function formatProposalFirstLook(proposal, storedEvidenceItemCount, proposalRef, storeSuffix) {
|
|
10640
|
+
const evidenceItems = storedEvidenceItemCount ?? proposal.change_set.evidence.items?.length ?? 0;
|
|
10641
|
+
return [
|
|
10642
|
+
`Proposal ${proposal.proposal_id}`,
|
|
10643
|
+
`Status: ${humanStatus(proposal.state)}`,
|
|
10644
|
+
"",
|
|
10645
|
+
"Agent requested:",
|
|
10646
|
+
proposal.action,
|
|
10647
|
+
"",
|
|
10648
|
+
"Business object:",
|
|
10649
|
+
`${proposal.business_object} ${proposal.object_id}`,
|
|
10650
|
+
"",
|
|
10651
|
+
"Proposed change:",
|
|
10652
|
+
...formatChangeLines(proposal).map((line) => line.replace(/^ /, "")),
|
|
10653
|
+
"",
|
|
10654
|
+
"Source DB changed:",
|
|
10655
|
+
proposal.source_database_mutated ? "yes" : "no",
|
|
10656
|
+
"",
|
|
10657
|
+
"Approval:",
|
|
10658
|
+
approvalBoundary(proposal),
|
|
10659
|
+
"",
|
|
10660
|
+
"Evidence:",
|
|
10661
|
+
`${proposal.change_set.evidence.bundle_id}${evidenceItems > 0 ? ` (${plural(evidenceItems, "item")})` : ""}`,
|
|
10662
|
+
"",
|
|
10663
|
+
"Next:",
|
|
10664
|
+
...proposalNextCommands(proposal, proposalRef, storeSuffix).map((command) => `${command}`),
|
|
10665
|
+
"",
|
|
10666
|
+
"More detail:",
|
|
10667
|
+
`${cliCommandName()} proposals show ${proposalRef} --details${storeSuffix}`,
|
|
10668
|
+
""
|
|
10669
|
+
].join("\n");
|
|
10670
|
+
}
|
|
10101
10671
|
function formatProposalDetail(proposal, storedEvidenceItemCount) {
|
|
10102
10672
|
const changeSet = proposal.change_set;
|
|
10103
10673
|
const conflictGuard = changeSet.guards.expected_version;
|
|
@@ -10105,13 +10675,15 @@ function formatProposalDetail(proposal, storedEvidenceItemCount) {
|
|
|
10105
10675
|
const approvalStatus = currentApprovalStatus(proposal);
|
|
10106
10676
|
const writebackStatus = currentWritebackStatus(proposal);
|
|
10107
10677
|
return [
|
|
10108
|
-
`
|
|
10109
|
-
|
|
10110
|
-
|
|
10678
|
+
`Proposal details: ${proposal.proposal_id}`,
|
|
10679
|
+
"",
|
|
10680
|
+
"Review details:",
|
|
10111
10681
|
`principal: ${changeSet.principal.id} (${changeSet.principal.source})`,
|
|
10112
10682
|
`tenant: ${proposal.tenant_id}`,
|
|
10113
10683
|
`target: ${proposal.source_kind}:${proposal.source_id}/${proposal.source_schema}.${proposal.source_table}/${proposal.object_id}`,
|
|
10114
10684
|
`primary key: ${changeSet.source.primary_key.column}=${formatScalar(changeSet.source.primary_key.value)}`,
|
|
10685
|
+
`status: ${proposal.state}`,
|
|
10686
|
+
`action: ${proposal.action}`,
|
|
10115
10687
|
`approval: ${approvalStatus}${changeSet.approval.required_role ? ` required role ${changeSet.approval.required_role}` : ""}`,
|
|
10116
10688
|
`proposal hash: ${proposal.proposal_hash}`,
|
|
10117
10689
|
`proposal version: ${proposal.proposal_version}`,
|
|
@@ -10120,14 +10692,29 @@ function formatProposalDetail(proposal, storedEvidenceItemCount) {
|
|
|
10120
10692
|
`evidence: ${changeSet.evidence.bundle_id} query ${changeSet.evidence.query_fingerprint} items ${evidenceItems}`,
|
|
10121
10693
|
`writeback: ${writebackStatus} via ${changeSet.writeback.mode}`,
|
|
10122
10694
|
`source database changed: ${proposal.source_database_mutated ? "yes" : "no"}`,
|
|
10123
|
-
"
|
|
10124
|
-
|
|
10125
|
-
|
|
10126
|
-
|
|
10127
|
-
|
|
10128
|
-
|
|
10695
|
+
"",
|
|
10696
|
+
"Diff:",
|
|
10697
|
+
...formatChangeLines(proposal)
|
|
10698
|
+
].join("\n") + "\n";
|
|
10699
|
+
}
|
|
10700
|
+
function formatProposalEventDetail(events) {
|
|
10701
|
+
if (events.length === 0) return "Events:\n none\n";
|
|
10702
|
+
return [
|
|
10703
|
+
"Events:",
|
|
10704
|
+
...events.map((event) => ` event ${event.event_id}: ${event.kind} by ${event.actor} at ${event.created_at}`)
|
|
10129
10705
|
].join("\n") + "\n";
|
|
10130
10706
|
}
|
|
10707
|
+
function formatProposalDebug(proposal, storePath) {
|
|
10708
|
+
return [
|
|
10709
|
+
"Debug:",
|
|
10710
|
+
`store: ${storePath ?? process2.env.SYNAPSOR_LOCAL_STORE ?? "./.synapsor/local.db"}`,
|
|
10711
|
+
`interaction id: ${proposal.interaction_id ?? "none"}`,
|
|
10712
|
+
`tool call id: ${proposal.tool_call_id ?? "none"}`,
|
|
10713
|
+
`source kind: ${proposal.source_kind}`,
|
|
10714
|
+
`writeback mode: ${proposal.change_set.writeback.mode}`,
|
|
10715
|
+
""
|
|
10716
|
+
].join("\n");
|
|
10717
|
+
}
|
|
10131
10718
|
function formatEvidenceSummary(evidence2) {
|
|
10132
10719
|
return [
|
|
10133
10720
|
`${evidence2.created_at} ${evidence2.evidence_bundle_id}`,
|
|
@@ -10135,6 +10722,35 @@ function formatEvidenceSummary(evidence2) {
|
|
|
10135
10722
|
` source: ${evidence2.source_id ?? "unknown"}/${evidence2.source_table ?? "unknown"} object: ${evidence2.business_object ?? "object"}:${evidence2.object_id ?? "unknown"}`
|
|
10136
10723
|
].join("\n") + "\n";
|
|
10137
10724
|
}
|
|
10725
|
+
function formatEvidenceFirstLook(evidence2, storeSuffix) {
|
|
10726
|
+
const object = evidence2.business_object && evidence2.object_id ? `${evidence2.business_object} ${evidence2.object_id}` : "not linked";
|
|
10727
|
+
const lines = [
|
|
10728
|
+
`Evidence ${evidence2.evidence_bundle_id}`,
|
|
10729
|
+
"",
|
|
10730
|
+
"Used for:",
|
|
10731
|
+
evidence2.capability ?? "unknown capability",
|
|
10732
|
+
object,
|
|
10733
|
+
"",
|
|
10734
|
+
"Captured:",
|
|
10735
|
+
plural(evidence2.items.length, "evidence item"),
|
|
10736
|
+
plural(evidence2.query_audit.length, "query audit record"),
|
|
10737
|
+
"",
|
|
10738
|
+
"Source:",
|
|
10739
|
+
`${evidence2.source_id ?? "unknown"} / ${evidence2.source_table ?? "unknown"}`,
|
|
10740
|
+
"",
|
|
10741
|
+
"Rows:",
|
|
10742
|
+
...evidence2.items.flatMap((item, index) => formatEvidenceItem(item, index + 1)),
|
|
10743
|
+
"",
|
|
10744
|
+
"Next:",
|
|
10745
|
+
` ${cliCommandName()} query-audit list --evidence ${evidence2.evidence_bundle_id}${storeSuffix}`,
|
|
10746
|
+
...evidence2.proposal_id ? [` ${cliCommandName()} replay show --proposal ${evidence2.proposal_id}${storeSuffix}`] : [],
|
|
10747
|
+
"",
|
|
10748
|
+
"More detail:",
|
|
10749
|
+
` ${cliCommandName()} evidence show ${evidence2.evidence_bundle_id} --details${storeSuffix}`
|
|
10750
|
+
];
|
|
10751
|
+
return `${lines.join("\n")}
|
|
10752
|
+
`;
|
|
10753
|
+
}
|
|
10138
10754
|
function formatEvidenceDetail(evidence2) {
|
|
10139
10755
|
const audit2 = evidence2.query_audit[0];
|
|
10140
10756
|
const lines = [
|
|
@@ -10166,9 +10782,16 @@ function formatEvidenceItem(item, index) {
|
|
|
10166
10782
|
const title = stringField(payload, "kind") ?? "item";
|
|
10167
10783
|
const primaryKey = isRecord6(payload.primary_key) ? payload.primary_key : void 0;
|
|
10168
10784
|
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)).
|
|
10785
|
+
const rows = Object.entries(visibleRow).filter(([key]) => !["kind", "source_id", "table", "primary_key", "tenant"].includes(key)).flatMap(([key, value]) => formatEvidenceFieldLines(key, value)).slice(0, 12);
|
|
10170
10786
|
return [heading, ...rows.length ? rows : [" (no scalar preview fields)"]];
|
|
10171
10787
|
}
|
|
10788
|
+
function formatEvidenceFieldLines(key, value) {
|
|
10789
|
+
if (isRecord6(value)) {
|
|
10790
|
+
const nested = Object.entries(value).filter(([, nestedValue]) => nestedValue === null || ["string", "number", "boolean"].includes(typeof nestedValue)).slice(0, 6).map(([nestedKey, nestedValue]) => ` ${key}.${nestedKey}: ${formatScalar(nestedValue)}`);
|
|
10791
|
+
return nested.length ? nested : [` ${key}: [object]`];
|
|
10792
|
+
}
|
|
10793
|
+
return [` ${key}: ${formatScalar(value)}`];
|
|
10794
|
+
}
|
|
10172
10795
|
function formatEvidenceMarkdown(evidence2) {
|
|
10173
10796
|
return [
|
|
10174
10797
|
`# Evidence ${evidence2.evidence_bundle_id}`,
|
|
@@ -10195,12 +10818,34 @@ function formatEvidenceMarkdown(evidence2) {
|
|
|
10195
10818
|
"```"
|
|
10196
10819
|
].join("\n") + "\n";
|
|
10197
10820
|
}
|
|
10198
|
-
function formatQueryAuditSummary(row) {
|
|
10199
|
-
|
|
10821
|
+
function formatQueryAuditSummary(row, details = false, storeSuffix = "") {
|
|
10822
|
+
const lines = [
|
|
10200
10823
|
`${row.created_at} audit ${row.audit_id}`,
|
|
10201
|
-
` source: ${row.source_id}/${row.table_name} rows: ${row.row_count}
|
|
10202
|
-
` proposal: ${row.proposal_id ?? "none"} evidence: ${row.evidence_bundle_id ?? "none"}
|
|
10203
|
-
]
|
|
10824
|
+
` source: ${row.source_id}/${row.table_name} rows: ${row.row_count}`,
|
|
10825
|
+
` proposal: ${row.proposal_id ?? "none"} evidence: ${row.evidence_bundle_id ?? "none"}`,
|
|
10826
|
+
...details ? [` query fingerprint: ${row.query_fingerprint}`] : [],
|
|
10827
|
+
` detail: ${cliCommandName()} query-audit show ${row.audit_id}${details ? "" : " --details"}${storeSuffix}`
|
|
10828
|
+
];
|
|
10829
|
+
return lines.join("\n") + "\n";
|
|
10830
|
+
}
|
|
10831
|
+
function formatQueryAuditFirstLook(row, storeSuffix) {
|
|
10832
|
+
return [
|
|
10833
|
+
`Query audit ${row.audit_id}`,
|
|
10834
|
+
"",
|
|
10835
|
+
"Read:",
|
|
10836
|
+
`${row.source_id}/${row.table_name}`,
|
|
10837
|
+
"",
|
|
10838
|
+
"Rows returned:",
|
|
10839
|
+
String(row.row_count ?? "unknown"),
|
|
10840
|
+
"",
|
|
10841
|
+
"Linked records:",
|
|
10842
|
+
`proposal: ${row.proposal_id ?? "none"}`,
|
|
10843
|
+
`evidence: ${row.evidence_bundle_id ?? "none"}`,
|
|
10844
|
+
"",
|
|
10845
|
+
"More detail:",
|
|
10846
|
+
`${cliCommandName()} query-audit show ${row.audit_id} --details${storeSuffix}`,
|
|
10847
|
+
""
|
|
10848
|
+
].join("\n");
|
|
10204
10849
|
}
|
|
10205
10850
|
function formatQueryAuditDetail(row) {
|
|
10206
10851
|
const payload = isRecord6(row.payload) ? row.payload : {};
|
|
@@ -10228,6 +10873,33 @@ function formatReceiptSummary(receipt) {
|
|
|
10228
10873
|
` idempotency: ${receipt.idempotency_key} source changed: ${receipt.source_database_mutated ? "yes" : "no"}`
|
|
10229
10874
|
].join("\n") + "\n";
|
|
10230
10875
|
}
|
|
10876
|
+
function formatReceiptFirstLook(receipt, storeSuffix) {
|
|
10877
|
+
const checks = receipt.status === "applied" ? ["primary key matched", "tenant guard matched", "allowed columns only", "conflict guard passed"] : receipt.status === "conflict" ? ["primary key matched", "tenant guard matched", "conflict guard blocked stale write"] : ["guarded writeback did not apply"];
|
|
10878
|
+
return [
|
|
10879
|
+
`Receipt ${formatReceiptId(receipt.receipt_id)}`,
|
|
10880
|
+
`Status: ${humanStatus(receipt.status)}`,
|
|
10881
|
+
"",
|
|
10882
|
+
"Proposal:",
|
|
10883
|
+
receipt.proposal_id,
|
|
10884
|
+
"",
|
|
10885
|
+
"Writeback:",
|
|
10886
|
+
"guarded single-row update",
|
|
10887
|
+
"",
|
|
10888
|
+
"Checks:",
|
|
10889
|
+
...checks.map((check) => `${check}`),
|
|
10890
|
+
`affected rows: ${receipt.receipt.rows_affected}`,
|
|
10891
|
+
"",
|
|
10892
|
+
"Source DB changed:",
|
|
10893
|
+
receipt.source_database_mutated ? "yes" : "no",
|
|
10894
|
+
"",
|
|
10895
|
+
"Next:",
|
|
10896
|
+
`${cliCommandName()} replay show --proposal ${receipt.proposal_id}${storeSuffix}`,
|
|
10897
|
+
"",
|
|
10898
|
+
"More detail:",
|
|
10899
|
+
`${cliCommandName()} receipts show ${receipt.receipt_id} --details${storeSuffix}`,
|
|
10900
|
+
""
|
|
10901
|
+
].join("\n");
|
|
10902
|
+
}
|
|
10231
10903
|
function formatReceiptDetail(receipt) {
|
|
10232
10904
|
return [
|
|
10233
10905
|
`Receipt: ${receipt.receipt_id}`,
|
|
@@ -10253,13 +10925,53 @@ function formatReplaySummary(row) {
|
|
|
10253
10925
|
` tenant: ${row.tenant_id} capability: ${row.capability} object: ${row.business_object}:${row.object_id}`
|
|
10254
10926
|
].join("\n") + "\n";
|
|
10255
10927
|
}
|
|
10256
|
-
function
|
|
10928
|
+
function formatReplayFirstLook(replay2, storeSuffix) {
|
|
10929
|
+
const proposal = replay2.proposal;
|
|
10257
10930
|
const evidenceItems = replay2.evidence.reduce((count, item) => {
|
|
10258
10931
|
const evidence2 = item;
|
|
10259
10932
|
return count + (Array.isArray(evidence2.items) ? evidence2.items.length : 0);
|
|
10260
10933
|
}, 0);
|
|
10934
|
+
const latestReceipt = replay2.receipts.at(-1);
|
|
10935
|
+
const writebackStatus = latestReceipt ? humanStatus(latestReceipt.status) : humanStatus(currentWritebackStatus(proposal));
|
|
10936
|
+
const approvalLine = proposal.state === "pending_review" ? "Approval is still pending" : `Proposal is ${humanStatus(proposal.state)}`;
|
|
10261
10937
|
return [
|
|
10262
10938
|
`Replay ${replay2.replay_id}`,
|
|
10939
|
+
"",
|
|
10940
|
+
"What happened:",
|
|
10941
|
+
`1. Agent called ${proposal.action}`,
|
|
10942
|
+
`2. Runner read ${proposal.business_object} ${proposal.object_id} under tenant ${proposal.tenant_id}`,
|
|
10943
|
+
`3. Runner created evidence bundle ${proposal.change_set.evidence.bundle_id}`,
|
|
10944
|
+
"4. Runner created a proposal",
|
|
10945
|
+
`5. Source DB changed: ${proposal.source_database_mutated ? "yes" : "no"}`,
|
|
10946
|
+
`6. ${approvalLine}`,
|
|
10947
|
+
"",
|
|
10948
|
+
"Proposed change:",
|
|
10949
|
+
...formatChangeLines(proposal).map((line) => line.replace(/^ /, "")),
|
|
10950
|
+
"",
|
|
10951
|
+
"Evidence:",
|
|
10952
|
+
plural(replay2.query_audit.length, "query audit record"),
|
|
10953
|
+
plural(evidenceItems, "evidence item"),
|
|
10954
|
+
"",
|
|
10955
|
+
"Writeback:",
|
|
10956
|
+
writebackStatus,
|
|
10957
|
+
...latestReceipt ? [`source DB changed after writeback: ${latestReceipt.source_database_mutated ? "yes" : "no"}`] : [],
|
|
10958
|
+
"",
|
|
10959
|
+
"Next:",
|
|
10960
|
+
` ${cliCommandName()} evidence show ${proposal.change_set.evidence.bundle_id}${storeSuffix}`,
|
|
10961
|
+
...proposal.state === "pending_review" ? [` ${cliCommandName()} proposals approve ${proposal.proposal_id} --yes${storeSuffix}`] : [],
|
|
10962
|
+
"",
|
|
10963
|
+
"More detail:",
|
|
10964
|
+
` ${cliCommandName()} replay show --proposal ${proposal.proposal_id} --details${storeSuffix}`,
|
|
10965
|
+
""
|
|
10966
|
+
].join("\n");
|
|
10967
|
+
}
|
|
10968
|
+
function formatReplayDetail(replay2) {
|
|
10969
|
+
const evidenceItems = replay2.evidence.reduce((count, item) => {
|
|
10970
|
+
const evidence2 = item;
|
|
10971
|
+
return count + (Array.isArray(evidence2.items) ? evidence2.items.length : 0);
|
|
10972
|
+
}, 0);
|
|
10973
|
+
return [
|
|
10974
|
+
`Replay details ${replay2.replay_id}`,
|
|
10263
10975
|
formatProposalDetail(replay2.proposal, evidenceItems).trimEnd(),
|
|
10264
10976
|
`events: ${replay2.events.length}`,
|
|
10265
10977
|
...replay2.events.map((event) => ` ${event.kind} by ${event.actor} at ${event.created_at}`),
|
|
@@ -10271,6 +10983,16 @@ function formatReplayDetail(replay2) {
|
|
|
10271
10983
|
...replay2.query_audit.map((record) => ` audit ${record.audit_id}: ${record.source_id}/${record.table_name} rows ${record.row_count}`)
|
|
10272
10984
|
].join("\n") + "\n";
|
|
10273
10985
|
}
|
|
10986
|
+
function formatReplayDebug(replay2, storePath) {
|
|
10987
|
+
return [
|
|
10988
|
+
"Debug:",
|
|
10989
|
+
`store: ${storePath ?? process2.env.SYNAPSOR_LOCAL_STORE ?? "./.synapsor/local.db"}`,
|
|
10990
|
+
`generated at: ${replay2.generated_at}`,
|
|
10991
|
+
`event ids: ${replay2.events.map((event) => event.event_id).join(", ") || "none"}`,
|
|
10992
|
+
`receipt ids: ${replay2.receipts.map((receipt) => receipt.receipt_id).join(", ") || "none"}`,
|
|
10993
|
+
""
|
|
10994
|
+
].join("\n");
|
|
10995
|
+
}
|
|
10274
10996
|
function formatReplayMarkdown(replay2) {
|
|
10275
10997
|
const proposal = replay2.proposal;
|
|
10276
10998
|
const principal = proposal.change_set.principal.id;
|
|
@@ -10402,7 +11124,7 @@ function activityFromReceipt(receipt) {
|
|
|
10402
11124
|
source_database_mutated: receipt.source_database_mutated
|
|
10403
11125
|
};
|
|
10404
11126
|
}
|
|
10405
|
-
function formatActivityItem(item, index) {
|
|
11127
|
+
function formatActivityItem(item, index, details = false) {
|
|
10406
11128
|
const lines = [
|
|
10407
11129
|
`${index}. ${item.created_at}`,
|
|
10408
11130
|
` kind: ${item.kind}`,
|
|
@@ -10412,14 +11134,35 @@ function formatActivityItem(item, index) {
|
|
|
10412
11134
|
...item.proposal ? [` proposal: ${item.proposal}`] : [],
|
|
10413
11135
|
...item.evidence ? [` evidence: ${item.evidence}`] : [],
|
|
10414
11136
|
...item.query_audit ? [` query audit: ${item.query_audit}`] : [],
|
|
10415
|
-
...item.query_fingerprint ? [` query fingerprint: ${item.query_fingerprint}`] : [],
|
|
11137
|
+
...details && item.query_fingerprint ? [` query fingerprint: ${item.query_fingerprint}`] : [],
|
|
10416
11138
|
...item.receipt ? [` receipt: ${item.receipt}`] : [],
|
|
10417
|
-
...item.status ? [` status: ${item.status}`] : [],
|
|
11139
|
+
...item.status ? [` status: ${humanStatus(String(item.status))}`] : [],
|
|
10418
11140
|
...item.replay ? [` replay: ${item.replay}`] : [],
|
|
10419
11141
|
""
|
|
10420
11142
|
];
|
|
10421
11143
|
return lines.join("\n");
|
|
10422
11144
|
}
|
|
11145
|
+
function formatActivityNext(items, storeSuffix) {
|
|
11146
|
+
const first = items[0];
|
|
11147
|
+
if (!first) return "";
|
|
11148
|
+
const proposal = stringField(first, "proposal");
|
|
11149
|
+
const replayId = stringField(first, "replay");
|
|
11150
|
+
const evidence2 = stringField(first, "evidence");
|
|
11151
|
+
const lines = ["Next:"];
|
|
11152
|
+
if (proposal) {
|
|
11153
|
+
lines.push(`${cliCommandName()} proposals show ${proposal}${storeSuffix}`);
|
|
11154
|
+
lines.push(`${cliCommandName()} replay show --proposal ${proposal}${storeSuffix}`);
|
|
11155
|
+
} else if (replayId) {
|
|
11156
|
+
lines.push(`${cliCommandName()} replay show --replay ${replayId}${storeSuffix}`);
|
|
11157
|
+
} else if (evidence2) {
|
|
11158
|
+
lines.push(`${cliCommandName()} evidence show ${evidence2}${storeSuffix}`);
|
|
11159
|
+
} else {
|
|
11160
|
+
lines.push(`${cliCommandName()} activity search --details${storeSuffix}`);
|
|
11161
|
+
}
|
|
11162
|
+
lines.push("");
|
|
11163
|
+
return `${lines.join("\n")}
|
|
11164
|
+
`;
|
|
11165
|
+
}
|
|
10423
11166
|
function formatStoreStats(stats) {
|
|
10424
11167
|
return [
|
|
10425
11168
|
`Local store: ${stats.path}`,
|
|
@@ -10521,6 +11264,60 @@ function currentWritebackStatus(proposal) {
|
|
|
10521
11264
|
if (proposal.state === "failed") return "failed";
|
|
10522
11265
|
return proposal.change_set.writeback.status;
|
|
10523
11266
|
}
|
|
11267
|
+
function showDetails(args) {
|
|
11268
|
+
return args.includes("--details") || args.includes("--debug");
|
|
11269
|
+
}
|
|
11270
|
+
function storeOptionSuffix(args) {
|
|
11271
|
+
const storePath = optionalArg(args, "--store");
|
|
11272
|
+
return storePath ? ` --store ${storePath}` : "";
|
|
11273
|
+
}
|
|
11274
|
+
function humanStatus(value) {
|
|
11275
|
+
const normalized = value.replace(/_/g, " ");
|
|
11276
|
+
if (normalized === "pending review") return "pending review";
|
|
11277
|
+
if (normalized === "not applied") return "not applied";
|
|
11278
|
+
return normalized;
|
|
11279
|
+
}
|
|
11280
|
+
function plural(count, label) {
|
|
11281
|
+
return `${count} ${label}${count === 1 ? "" : "s"}`;
|
|
11282
|
+
}
|
|
11283
|
+
function formatReceiptId(receiptId) {
|
|
11284
|
+
return `rct_${String(receiptId).padStart(6, "0")}`;
|
|
11285
|
+
}
|
|
11286
|
+
function approvalBoundary(proposal) {
|
|
11287
|
+
if (proposal.state === "pending_review") return "required outside MCP";
|
|
11288
|
+
if (proposal.state === "approved" || proposal.state === "pending_worker") return "approved outside MCP; waiting for trusted worker";
|
|
11289
|
+
if (proposal.state === "applied") return "approved outside MCP; writeback applied";
|
|
11290
|
+
if (proposal.state === "conflict") return "approved outside MCP; writeback blocked by conflict guard";
|
|
11291
|
+
if (proposal.state === "failed") return "approved outside MCP; writeback failed safely";
|
|
11292
|
+
if (proposal.state === "rejected") return "rejected outside MCP";
|
|
11293
|
+
return humanStatus(proposal.state);
|
|
11294
|
+
}
|
|
11295
|
+
function proposalNextCommands(proposal, proposalRef, storeSuffix) {
|
|
11296
|
+
if (proposal.state === "pending_review") {
|
|
11297
|
+
return [
|
|
11298
|
+
`${cliCommandName()} proposals approve ${proposalRef} --yes${storeSuffix}`,
|
|
11299
|
+
`${cliCommandName()} replay show ${proposalRef === "latest" ? "latest" : `--proposal ${proposal.proposal_id}`}${storeSuffix}`
|
|
11300
|
+
];
|
|
11301
|
+
}
|
|
11302
|
+
if (proposal.state === "approved" || proposal.state === "pending_worker") {
|
|
11303
|
+
return [
|
|
11304
|
+
`${cliCommandName()} replay show --proposal ${proposal.proposal_id}${storeSuffix}`
|
|
11305
|
+
];
|
|
11306
|
+
}
|
|
11307
|
+
return [
|
|
11308
|
+
`${cliCommandName()} replay show --proposal ${proposal.proposal_id}${storeSuffix}`
|
|
11309
|
+
];
|
|
11310
|
+
}
|
|
11311
|
+
function formatChangeLines(proposal) {
|
|
11312
|
+
const changeSet = proposal.change_set;
|
|
11313
|
+
const columns = Object.keys(changeSet.patch);
|
|
11314
|
+
if (columns.length === 0) return [" (no changed columns)"];
|
|
11315
|
+
return columns.map((column) => {
|
|
11316
|
+
const before = changeSet.before[column];
|
|
11317
|
+
const proposed = changeSet.after[column];
|
|
11318
|
+
return ` ${column}: ${formatScalar(before)} -> ${formatScalar(proposed)}`;
|
|
11319
|
+
});
|
|
11320
|
+
}
|
|
10524
11321
|
function formatShadowComparison(comparison) {
|
|
10525
11322
|
return [
|
|
10526
11323
|
`shadow comparison: ${comparison.proposal_id}`,
|
|
@@ -10638,7 +11435,7 @@ function starterCloudConfig() {
|
|
|
10638
11435
|
base_url_env: "SYNAPSOR_CLOUD_BASE_URL",
|
|
10639
11436
|
runner_token_env: "SYNAPSOR_RUNNER_TOKEN",
|
|
10640
11437
|
runner_id: "synapsor_runner_local",
|
|
10641
|
-
runner_version: "0.1.0-alpha.
|
|
11438
|
+
runner_version: "0.1.0-alpha.6",
|
|
10642
11439
|
project_id: "token_scope",
|
|
10643
11440
|
adapter_id: "mcp.your_adapter",
|
|
10644
11441
|
source_id: "src_replace_me",
|
|
@@ -10694,8 +11491,7 @@ function isKnownTopLevelCommand(command) {
|
|
|
10694
11491
|
}
|
|
10695
11492
|
function cliCommandName() {
|
|
10696
11493
|
if (process2.env.SYNAPSOR_RUNNER_COMMAND_NAME) return process2.env.SYNAPSOR_RUNNER_COMMAND_NAME;
|
|
10697
|
-
|
|
10698
|
-
return invoked === "synapsor-runner" ? "synapsor-runner" : "synapsor";
|
|
11494
|
+
return "synapsor-runner";
|
|
10699
11495
|
}
|
|
10700
11496
|
function usage(args = []) {
|
|
10701
11497
|
const [command, subcommand] = args;
|
|
@@ -10747,16 +11543,31 @@ Generate a reviewed Synapsor Runner contract. Defaults to read-only in the wizar
|
|
|
10747
11543
|
`,
|
|
10748
11544
|
mcp: `Usage:
|
|
10749
11545
|
${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
11546
|
+
${cmd} mcp serve-http --config ./synapsor.runner.json --store ./.synapsor/local.db --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
|
|
10750
11547
|
${cmd} mcp config --absolute-paths --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
10751
11548
|
${cmd} mcp audit --example dangerous-db-mcp
|
|
10752
11549
|
${cmd} mcp audit ./tools-list.json
|
|
10753
11550
|
|
|
11551
|
+
Use stdio for local MCP clients that launch the runner. Use authenticated HTTP for app/server deployments.
|
|
10754
11552
|
MCP clients see semantic tools. They do not receive raw SQL, write credentials, approval tools, or commit tools.
|
|
10755
11553
|
`,
|
|
10756
11554
|
"mcp serve": `Usage:
|
|
10757
11555
|
${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db [--read-only] [--local]
|
|
10758
11556
|
|
|
10759
|
-
Start the stdio MCP server. Startup logs stay off stdout so the MCP protocol remains clean.
|
|
11557
|
+
Start the stdio MCP server for local MCP clients such as Claude Desktop, Cursor, or local agent tools. Startup logs stay off stdout so the MCP protocol remains clean.
|
|
11558
|
+
`,
|
|
11559
|
+
"mcp serve-http": `Usage:
|
|
11560
|
+
export SYNAPSOR_RUNNER_HTTP_TOKEN=...
|
|
11561
|
+
${cmd} mcp serve-http --config ./synapsor.runner.json --store ./.synapsor/local.db [--host 127.0.0.1] [--port 8765] [--auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN]
|
|
11562
|
+
|
|
11563
|
+
Start the HTTP MCP server for app/server deployments. Bearer auth is required by default.
|
|
11564
|
+
|
|
11565
|
+
Security:
|
|
11566
|
+
- Defaults to 127.0.0.1:8765.
|
|
11567
|
+
- Refuses to start if the auth token env var is missing.
|
|
11568
|
+
- Use --dev-no-auth only for localhost development.
|
|
11569
|
+
- If binding to 0.0.0.0, use TLS, private networking, authentication, and rate limits.
|
|
11570
|
+
- Optional CORS: --cors-origin http://localhost:3000
|
|
10760
11571
|
`,
|
|
10761
11572
|
"mcp config": `Usage:
|
|
10762
11573
|
${cmd} mcp config [claude-desktop|cursor|generic|vscode] [--absolute-paths] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
|
|
@@ -10795,33 +11606,37 @@ Static MCP/database risk review only. This is not a security guarantee.
|
|
|
10795
11606
|
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.
|
|
10796
11607
|
`,
|
|
10797
11608
|
proposals: `Usage:
|
|
10798
|
-
|
|
10799
|
-
|
|
10800
|
-
|
|
10801
|
-
|
|
11609
|
+
${cmd} proposals list [--tenant acme] [--capability billing.propose_late_fee_waiver] [--object invoice:INV-3001] [--status applied]
|
|
11610
|
+
${cmd} proposals show latest
|
|
11611
|
+
${cmd} proposals show latest --details
|
|
11612
|
+
${cmd} proposals approve latest --yes
|
|
11613
|
+
${cmd} proposals reject latest --reason "..."
|
|
10802
11614
|
|
|
10803
|
-
Review decisions happen outside the model-facing MCP tool surface.
|
|
10804
|
-
`,
|
|
11615
|
+
Review decisions happen outside the model-facing MCP tool surface. Human output is concise by default; use --details for reviewer metadata or --json for complete records.
|
|
11616
|
+
`,
|
|
10805
11617
|
evidence: `Usage:
|
|
10806
|
-
|
|
10807
|
-
|
|
10808
|
-
|
|
11618
|
+
${cmd} evidence list [--tenant acme] [--capability billing.inspect_invoice] [--object invoice:INV-3001]
|
|
11619
|
+
${cmd} evidence show ev_...
|
|
11620
|
+
${cmd} evidence show ev_... --details
|
|
11621
|
+
${cmd} evidence export ev_... --format json --output evidence.json
|
|
10809
11622
|
${cmd} evidence export ev_... --format markdown --output evidence.md
|
|
10810
11623
|
|
|
10811
11624
|
Inspect captured local evidence bundles and query-audit links without rerunning external DB reads.
|
|
10812
11625
|
`,
|
|
10813
11626
|
"query-audit": `Usage:
|
|
10814
|
-
|
|
10815
|
-
|
|
10816
|
-
|
|
11627
|
+
${cmd} query-audit list [--evidence ev_...] [--source app_postgres] [--table invoices]
|
|
11628
|
+
${cmd} query-audit show <audit_id>
|
|
11629
|
+
${cmd} query-audit show <audit_id> --details
|
|
11630
|
+
${cmd} query-audit export <audit_id> --format json --output audit.json
|
|
10817
11631
|
|
|
10818
11632
|
Inspect local query fingerprints, table names, row counts, and redacted-parameter metadata.
|
|
10819
11633
|
`,
|
|
10820
11634
|
receipts: `Usage:
|
|
10821
|
-
|
|
10822
|
-
|
|
11635
|
+
${cmd} receipts list [--proposal wrp_...] [--status applied]
|
|
11636
|
+
${cmd} receipts show <receipt_id>
|
|
11637
|
+
${cmd} receipts show <receipt_id> --details
|
|
10823
11638
|
|
|
10824
|
-
Inspect guarded writeback receipts recorded by the trusted runner path.
|
|
11639
|
+
Inspect guarded writeback receipts recorded by the trusted runner path. Use --details for idempotency keys, receipt hashes, and runner metadata.
|
|
10825
11640
|
`,
|
|
10826
11641
|
apply: `Usage:
|
|
10827
11642
|
${cmd} apply latest [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
|
|
@@ -10831,18 +11646,20 @@ Apply an approved proposal through guarded writeback. Requires a trusted write c
|
|
|
10831
11646
|
`,
|
|
10832
11647
|
replay: `Usage:
|
|
10833
11648
|
${cmd} replay list [--tenant acme] [--object invoice:INV-3001]
|
|
10834
|
-
|
|
10835
|
-
|
|
11649
|
+
${cmd} replay show latest
|
|
11650
|
+
${cmd} replay show latest --details
|
|
11651
|
+
${cmd} replay show --proposal wrp_...
|
|
10836
11652
|
${cmd} replay show --replay replay_wrp_...
|
|
10837
11653
|
${cmd} replay show --evidence ev_...
|
|
10838
11654
|
${cmd} replay export --proposal wrp_... --format json --output replay.json
|
|
10839
11655
|
${cmd} replay export --proposal wrp_... --format markdown --output replay.md
|
|
10840
11656
|
|
|
10841
|
-
Show evidence, proposal events, receipts, and replay state without rerunning side effects.
|
|
11657
|
+
Show evidence, proposal events, receipts, and replay state without rerunning side effects. Human output is concise by default; use --details for reviewer metadata or --json for complete records.
|
|
10842
11658
|
`,
|
|
10843
11659
|
activity: `Usage:
|
|
10844
|
-
|
|
10845
|
-
|
|
11660
|
+
${cmd} activity search --tenant acme --object invoice:INV-3001
|
|
11661
|
+
${cmd} activity search --tenant acme --object invoice:INV-3001 --details
|
|
11662
|
+
${cmd} activity search --capability billing.propose_late_fee_waiver --from 2026-06-01 --to 2026-06-23
|
|
10846
11663
|
|
|
10847
11664
|
Search the local SQLite evidence/replay ledger across proposals, evidence, query audit, receipts, and replay records.
|
|
10848
11665
|
`,
|
|
@@ -10857,9 +11674,14 @@ Local store maintenance only. Prune defaults to dry-run and never touches your s
|
|
|
10857
11674
|
demo: `Usage:
|
|
10858
11675
|
${cmd} demo [--force]
|
|
10859
11676
|
${cmd} demo --quick
|
|
11677
|
+
${cmd} demo --quick --guided
|
|
11678
|
+
${cmd} demo --quick --no-interactive
|
|
11679
|
+
${cmd} demo --quick --details
|
|
11680
|
+
${cmd} demo inspect
|
|
11681
|
+
${cmd} demo inspect --npx
|
|
10860
11682
|
|
|
10861
11683
|
Start a disposable local Postgres demo and write ./synapsor.runner.json for the first-run flow.
|
|
10862
|
-
Use --quick for a fixture-only
|
|
11684
|
+
Use --quick for a fixture-only guided walkthrough and local ledger seed with no Docker startup. Use demo inspect to print follow-up commands for the quick-demo fixture.
|
|
10863
11685
|
`,
|
|
10864
11686
|
ui: `Usage:
|
|
10865
11687
|
${cmd} ui [--tour] [--config synapsor.runner.json] [--store ./.synapsor/local.db]
|