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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +193 -25
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/runner.mjs +903 -80
  4. package/docs/README.md +32 -54
  5. package/docs/getting-started-own-database.md +40 -8
  6. package/docs/http-mcp.md +200 -0
  7. package/docs/local-mode.md +40 -4
  8. package/docs/mcp-audit.md +0 -4
  9. package/docs/mcp-client-setup.md +40 -1
  10. package/examples/openai-agents-http/README.md +55 -0
  11. package/examples/openai-agents-http/agent.py +90 -0
  12. package/examples/openai-agents-http/requirements.txt +1 -0
  13. package/examples/openai-agents-stdio/README.md +62 -0
  14. package/examples/openai-agents-stdio/agent.py +70 -0
  15. package/examples/openai-agents-stdio/requirements.txt +1 -0
  16. package/package.json +3 -1
  17. package/docs/MCP_RUNNER_IMPLEMENTATION_PLAN.md +0 -187
  18. package/docs/architecture.md +0 -65
  19. package/docs/capability-config.md +0 -180
  20. package/docs/cloud-mode.md +0 -140
  21. package/docs/config-migrations.md +0 -67
  22. package/docs/demo-transcript.md +0 -161
  23. package/docs/dependency-license-inventory.md +0 -35
  24. package/docs/first-10-minutes.md +0 -172
  25. package/docs/licensing.md +0 -38
  26. package/docs/local-ui.md +0 -163
  27. package/docs/mcp-efficiency-benchmark.md +0 -84
  28. package/docs/open-source-feature-inventory.md +0 -254
  29. package/docs/operations.md +0 -38
  30. package/docs/own-db-20-minutes.md +0 -185
  31. package/docs/production-readiness.md +0 -39
  32. package/docs/protocol.md +0 -90
  33. package/docs/roadmap.md +0 -13
  34. package/docs/schema-inspection.md +0 -88
  35. package/docs/shadow-mode.md +0 -67
  36. package/docs/telemetry.md +0 -28
  37. package/docs/threat-model.md +0 -25
  38. 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.4" },
2688
+ { name: "synapsor-runner", version: "0.1.0-alpha.5" },
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,7 +5091,7 @@ 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";
@@ -4874,7 +5102,7 @@ async function startLocalUiServer(options = {}) {
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 = createServer(async (request, response) => {
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) {
@@ -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\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
- const tableName = await askDefault(ask, "Table/view for this capability", optionalArg(args, "--table") ?? tables[0]?.name ?? "");
6196
+ stdout.write("\nStep 2: Create trusted context\n");
6197
+ stdout.write("Choose the source object and trusted scope. Tenant and principal values come from your backend/session, not from the model.\n");
6198
+ const tableName = await askDefault(ask, "Source table/view for this context", optionalArg(args, "--table") ?? tables[0]?.name ?? "");
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, "Tenant/scope column. Leave blank only for a reviewed single-tenant dev source", suggestedTenant ?? "");
6204
+ const tenantAnswer = await askDefault(ask, "Trusted tenant/scope column", suggestedTenant ?? "");
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 mode = await askChoice(ask, "Mode", optionalArg(args, "--mode") ?? "read_only", ["read_only", "shadow", "review"]);
6208
+ const tenantEnv = await askEnvName(ask, "Trusted tenant env var", optionalArg(args, "--tenant-env") ?? "SYNAPSOR_TENANT_ID");
6209
+ const principalEnv = await askEnvName(ask, "Trusted principal env var", optionalArg(args, "--principal-env") ?? "SYNAPSOR_PRINCIPAL");
6210
+ stdout.write("\nStep 3: Create capability\n");
6211
+ stdout.write("Name the semantic tool the model can call. Table, key, visible fields, and mode define what that capability can do.\n");
6212
+ const mode = await askChoice(ask, "Capability mode", optionalArg(args, "--mode") ?? "read_only", ["read_only", "shadow", "review"]);
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, "Read-visible columns", optionalArg(args, "--visible-columns") ?? defaultVisible));
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.4").trim();
7980
+ const runnerVersion = String(parsed.cloud.runner_version || process2.env.npm_package_version || "0.1.0-alpha.5").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,17 +8073,91 @@ 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
- process2.stdout.write([
7849
- "Synapsor Runner quick demo",
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:",
@@ -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
- "Inspect this local fixture:",
7886
- `${cliCommandName()} proposals show latest --store ${quickDemoStorePath}`,
7887
- `${cliCommandName()} evidence show ${seeded.evidence_bundle_id} --store ${quickDemoStorePath}`,
7888
- `${cliCommandName()} activity search --object invoice:INV-3001 --store ${quickDemoStorePath}`,
7889
- `${cliCommandName()} replay show --proposal ${seeded.proposal_id} --store ${quickDemoStorePath}`,
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
- "Approve the fixture outside MCP:",
7892
- `${cliCommandName()} proposals approve latest --yes --store ${quickDemoStorePath}`,
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 demo inspect",
8294
+ "",
8295
+ "demo inspect shows the proposal, evidence, activity search, and replay commands.",
8296
+ "",
8297
+ "If installed globally, use:",
8298
+ "synapsor demo inspect"
8299
+ ]
8300
+ },
8301
+ {
8302
+ title: "Next paths",
8303
+ body: [
8304
+ "Full disposable Postgres demo:",
8305
+ `${cliCommandName()} demo`,
8306
+ "",
8307
+ "Audit risky MCP database tools:",
8308
+ `${cliCommandName()} audit --example dangerous-db-mcp`,
8309
+ "",
8310
+ "Use your own staging DB:",
8311
+ 'export DATABASE_URL="postgres://..."',
8312
+ `${cliCommandName()} inspect --from-env DATABASE_URL`,
8313
+ "",
8314
+ "Done. You just saw Synapsor's core boundary: business tools for the model, approval/writeback outside the model, and replay for inspection."
8315
+ ]
8316
+ }
8317
+ ];
8318
+ }
8319
+ function printStep(title, body, index, total) {
8320
+ const divider = "------------------------------------------------------------";
8321
+ process2.stdout.write([
8322
+ "",
8323
+ divider,
8324
+ `Step ${index}/${total}: ${title}`,
8325
+ divider,
7897
8326
  "",
7898
- "Audit risky MCP database tools:",
7899
- `${cliCommandName()} audit --example dangerous-db-mcp`,
8327
+ ...body,
7900
8328
  ""
7901
8329
  ].join("\n"));
7902
- return 0;
8330
+ }
8331
+ async function waitForEnter(message, options) {
8332
+ if (!options.pause) {
8333
+ process2.stdout.write(`${message}
8334
+ `);
8335
+ return;
8336
+ }
8337
+ const rl = readline.createInterface({ input: process2.stdin, output: process2.stdout });
8338
+ try {
8339
+ await rl.question(`${message} `);
8340
+ } finally {
8341
+ rl.close();
8342
+ }
8343
+ }
8344
+ function quickDemoInspectCommands(useNpx) {
8345
+ const cmd = useNpx ? "npx -y -p @synapsor/runner@alpha synapsor" : cliCommandName();
8346
+ return [
8347
+ {
8348
+ label: "Proposal summary",
8349
+ description: "See what the model asked to change.",
8350
+ command: `${cmd} proposals show latest --store ${quickDemoStorePath}`
8351
+ },
8352
+ {
8353
+ label: "Evidence",
8354
+ description: "Inspect rows and evidence items captured for the proposal.",
8355
+ command: `${cmd} evidence show ev_quick_INV_3001 --store ${quickDemoStorePath}`
8356
+ },
8357
+ {
8358
+ label: "Activity search",
8359
+ description: "Find the local ledger records for invoice INV-3001.",
8360
+ command: `${cmd} activity search --object invoice:INV-3001 --store ${quickDemoStorePath}`
8361
+ },
8362
+ {
8363
+ label: "Replay",
8364
+ description: "Replay the local proposal/evidence/audit story.",
8365
+ command: `${cmd} replay show latest --store ${quickDemoStorePath}`
8366
+ },
8367
+ {
8368
+ label: "Approve outside MCP",
8369
+ description: "Approve the proposal through the local operator boundary.",
8370
+ command: `${cmd} proposals approve latest --yes --store ${quickDemoStorePath}`
8371
+ },
8372
+ {
8373
+ label: "Full Docker-backed demo",
8374
+ description: "Run the disposable Postgres-backed proof.",
8375
+ command: `${cmd} demo`
8376
+ },
8377
+ {
8378
+ label: "Audit risky MCP database tools",
8379
+ description: "Review common dangerous MCP tool shapes.",
8380
+ command: `${cmd} audit --example dangerous-db-mcp`
8381
+ }
8382
+ ];
8383
+ }
8384
+ function formatQuickDemoInspect(commands) {
8385
+ return [
8386
+ "Quick demo inspection",
8387
+ "",
8388
+ "Local ledger:",
8389
+ quickDemoStorePath,
8390
+ "",
8391
+ ...commands.flatMap((item, index) => [
8392
+ `${index + 1}. ${item.label}`,
8393
+ ` ${item.description}`,
8394
+ ` ${item.command}`,
8395
+ ""
8396
+ ])
8397
+ ].join("\n");
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
- for (const event of payload.events) {
9036
- process2.stdout.write(`event ${event.event_id}: ${event.kind} by ${event.actor} at ${event.created_at}
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
- `proposal: ${proposal.proposal_id}`,
10109
- `state: ${proposal.state}`,
10110
- `action: ${proposal.action}`,
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
- "diff:",
10124
- ...Object.keys(changeSet.patch).map((column) => {
10125
- const before = changeSet.before[column];
10126
- const proposed = changeSet.after[column];
10127
- return ` ${column}: ${JSON.stringify(before)} -> ${JSON.stringify(proposed)}`;
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)).slice(0, 12).map(([key, value]) => ` ${key}: ${formatScalar(value)}`);
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
- return [
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} query: ${row.query_fingerprint}`,
10202
- ` proposal: ${row.proposal_id ?? "none"} evidence: ${row.evidence_bundle_id ?? "none"}`
10203
- ].join("\n") + "\n";
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 formatReplayDetail(replay2) {
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.4",
11438
+ runner_version: "0.1.0-alpha.5",
10642
11439
  project_id: "token_scope",
10643
11440
  adapter_id: "mcp.your_adapter",
10644
11441
  source_id: "src_replace_me",
@@ -10747,16 +11544,31 @@ Generate a reviewed Synapsor Runner contract. Defaults to read-only in the wizar
10747
11544
  `,
10748
11545
  mcp: `Usage:
10749
11546
  ${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db
11547
+ ${cmd} mcp serve-http --config ./synapsor.runner.json --store ./.synapsor/local.db --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
10750
11548
  ${cmd} mcp config --absolute-paths --config ./synapsor.runner.json --store ./.synapsor/local.db
10751
11549
  ${cmd} mcp audit --example dangerous-db-mcp
10752
11550
  ${cmd} mcp audit ./tools-list.json
10753
11551
 
11552
+ Use stdio for local MCP clients that launch the runner. Use authenticated HTTP for app/server deployments.
10754
11553
  MCP clients see semantic tools. They do not receive raw SQL, write credentials, approval tools, or commit tools.
10755
11554
  `,
10756
11555
  "mcp serve": `Usage:
10757
11556
  ${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db [--read-only] [--local]
10758
11557
 
10759
- Start the stdio MCP server. Startup logs stay off stdout so the MCP protocol remains clean.
11558
+ Start the stdio MCP server for local MCP clients such as Claude Desktop, Cursor, or local agent tools. Startup logs stay off stdout so the MCP protocol remains clean.
11559
+ `,
11560
+ "mcp serve-http": `Usage:
11561
+ export SYNAPSOR_RUNNER_HTTP_TOKEN=...
11562
+ ${cmd} mcp serve-http --config ./synapsor.runner.json --store ./.synapsor/local.db [--host 127.0.0.1] [--port 8765] [--auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN]
11563
+
11564
+ Start the HTTP MCP server for app/server deployments. Bearer auth is required by default.
11565
+
11566
+ Security:
11567
+ - Defaults to 127.0.0.1:8765.
11568
+ - Refuses to start if the auth token env var is missing.
11569
+ - Use --dev-no-auth only for localhost development.
11570
+ - If binding to 0.0.0.0, use TLS, private networking, authentication, and rate limits.
11571
+ - Optional CORS: --cors-origin http://localhost:3000
10760
11572
  `,
10761
11573
  "mcp config": `Usage:
10762
11574
  ${cmd} mcp config [claude-desktop|cursor|generic|vscode] [--absolute-paths] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
@@ -10795,33 +11607,37 @@ Static MCP/database risk review only. This is not a security guarantee.
10795
11607
  Validate local config, environment bindings, semantic tool boundary, source metadata when reachable, and local store stats. Reports are redacted; do not paste secrets into issues.
10796
11608
  `,
10797
11609
  proposals: `Usage:
10798
- ${cmd} proposals list [--tenant acme] [--capability billing.propose_late_fee_waiver] [--object invoice:INV-3001] [--status applied]
10799
- ${cmd} proposals show latest
10800
- ${cmd} proposals approve latest --yes
10801
- ${cmd} proposals reject latest --reason "..."
11610
+ ${cmd} proposals list [--tenant acme] [--capability billing.propose_late_fee_waiver] [--object invoice:INV-3001] [--status applied]
11611
+ ${cmd} proposals show latest
11612
+ ${cmd} proposals show latest --details
11613
+ ${cmd} proposals approve latest --yes
11614
+ ${cmd} proposals reject latest --reason "..."
10802
11615
 
10803
- Review decisions happen outside the model-facing MCP tool surface.
10804
- `,
11616
+ Review decisions happen outside the model-facing MCP tool surface. Human output is concise by default; use --details for reviewer metadata or --json for complete records.
11617
+ `,
10805
11618
  evidence: `Usage:
10806
- ${cmd} evidence list [--tenant acme] [--capability billing.inspect_invoice] [--object invoice:INV-3001]
10807
- ${cmd} evidence show ev_...
10808
- ${cmd} evidence export ev_... --format json --output evidence.json
11619
+ ${cmd} evidence list [--tenant acme] [--capability billing.inspect_invoice] [--object invoice:INV-3001]
11620
+ ${cmd} evidence show ev_...
11621
+ ${cmd} evidence show ev_... --details
11622
+ ${cmd} evidence export ev_... --format json --output evidence.json
10809
11623
  ${cmd} evidence export ev_... --format markdown --output evidence.md
10810
11624
 
10811
11625
  Inspect captured local evidence bundles and query-audit links without rerunning external DB reads.
10812
11626
  `,
10813
11627
  "query-audit": `Usage:
10814
- ${cmd} query-audit list [--evidence ev_...] [--source app_postgres] [--table invoices]
10815
- ${cmd} query-audit show <audit_id>
10816
- ${cmd} query-audit export <audit_id> --format json --output audit.json
11628
+ ${cmd} query-audit list [--evidence ev_...] [--source app_postgres] [--table invoices]
11629
+ ${cmd} query-audit show <audit_id>
11630
+ ${cmd} query-audit show <audit_id> --details
11631
+ ${cmd} query-audit export <audit_id> --format json --output audit.json
10817
11632
 
10818
11633
  Inspect local query fingerprints, table names, row counts, and redacted-parameter metadata.
10819
11634
  `,
10820
11635
  receipts: `Usage:
10821
- ${cmd} receipts list [--proposal wrp_...] [--status applied]
10822
- ${cmd} receipts show <receipt_id>
11636
+ ${cmd} receipts list [--proposal wrp_...] [--status applied]
11637
+ ${cmd} receipts show <receipt_id>
11638
+ ${cmd} receipts show <receipt_id> --details
10823
11639
 
10824
- Inspect guarded writeback receipts recorded by the trusted runner path.
11640
+ Inspect guarded writeback receipts recorded by the trusted runner path. Use --details for idempotency keys, receipt hashes, and runner metadata.
10825
11641
  `,
10826
11642
  apply: `Usage:
10827
11643
  ${cmd} apply latest [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
@@ -10831,18 +11647,20 @@ Apply an approved proposal through guarded writeback. Requires a trusted write c
10831
11647
  `,
10832
11648
  replay: `Usage:
10833
11649
  ${cmd} replay list [--tenant acme] [--object invoice:INV-3001]
10834
- ${cmd} replay show latest
10835
- ${cmd} replay show --proposal wrp_...
11650
+ ${cmd} replay show latest
11651
+ ${cmd} replay show latest --details
11652
+ ${cmd} replay show --proposal wrp_...
10836
11653
  ${cmd} replay show --replay replay_wrp_...
10837
11654
  ${cmd} replay show --evidence ev_...
10838
11655
  ${cmd} replay export --proposal wrp_... --format json --output replay.json
10839
11656
  ${cmd} replay export --proposal wrp_... --format markdown --output replay.md
10840
11657
 
10841
- Show evidence, proposal events, receipts, and replay state without rerunning side effects.
11658
+ Show evidence, proposal events, receipts, and replay state without rerunning side effects. Human output is concise by default; use --details for reviewer metadata or --json for complete records.
10842
11659
  `,
10843
11660
  activity: `Usage:
10844
- ${cmd} activity search --tenant acme --object invoice:INV-3001
10845
- ${cmd} activity search --capability billing.propose_late_fee_waiver --from 2026-06-01 --to 2026-06-23
11661
+ ${cmd} activity search --tenant acme --object invoice:INV-3001
11662
+ ${cmd} activity search --tenant acme --object invoice:INV-3001 --details
11663
+ ${cmd} activity search --capability billing.propose_late_fee_waiver --from 2026-06-01 --to 2026-06-23
10846
11664
 
10847
11665
  Search the local SQLite evidence/replay ledger across proposals, evidence, query audit, receipts, and replay records.
10848
11666
  `,
@@ -10857,9 +11675,14 @@ Local store maintenance only. Prune defaults to dry-run and never touches your s
10857
11675
  demo: `Usage:
10858
11676
  ${cmd} demo [--force]
10859
11677
  ${cmd} demo --quick
11678
+ ${cmd} demo --quick --guided
11679
+ ${cmd} demo --quick --no-interactive
11680
+ ${cmd} demo --quick --details
11681
+ ${cmd} demo inspect
11682
+ ${cmd} demo inspect --npx
10860
11683
 
10861
11684
  Start a disposable local Postgres demo and write ./synapsor.runner.json for the first-run flow.
10862
- Use --quick for a fixture-only 15-second explanation and local ledger seed with no Docker startup.
11685
+ Use --quick for a fixture-only guided walkthrough and local ledger seed with no Docker startup. Use demo inspect to print follow-up commands for the quick-demo fixture.
10863
11686
  `,
10864
11687
  ui: `Usage:
10865
11688
  ${cmd} ui [--tour] [--config synapsor.runner.json] [--store ./.synapsor/local.db]