@synapsor/runner 0.1.0-alpha.10 → 0.1.0-alpha.13

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 (54) hide show
  1. package/README.md +203 -21
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/runner.mjs +1103 -115
  4. package/docs/README.md +38 -0
  5. package/docs/app-owned-executors.md +26 -0
  6. package/docs/capability-authoring.md +265 -0
  7. package/docs/cloud-mode.md +24 -0
  8. package/docs/current-scope.md +24 -0
  9. package/docs/dependency-license-inventory.md +35 -0
  10. package/docs/doctor.md +98 -0
  11. package/docs/handler-helper.md +200 -0
  12. package/docs/http-mcp.md +35 -1
  13. package/docs/licensing.md +36 -0
  14. package/docs/local-mode.md +13 -2
  15. package/docs/mcp-client-setup.md +39 -0
  16. package/docs/openai-agents-sdk.md +57 -0
  17. package/docs/release-notes.md +76 -2
  18. package/docs/release-policy.md +86 -0
  19. package/docs/result-envelope-v2.md +148 -0
  20. package/docs/rfcs/001-result-envelope-v2.md +143 -0
  21. package/docs/rfcs/002-app-owned-handler-helper.md +161 -0
  22. package/docs/rfcs/003-integrator-feedback-teardown.md +97 -0
  23. package/docs/store-lifecycle.md +83 -0
  24. package/docs/use-your-own-database.md +18 -0
  25. package/docs/writeback-executors.md +29 -0
  26. package/examples/app-owned-writeback/README.md +1 -0
  27. package/examples/mcp-postgres-billing-app-handler/README.md +86 -0
  28. package/examples/mcp-postgres-billing-app-handler/app-handler.mjs +125 -0
  29. package/examples/mcp-postgres-billing-app-handler/docker-compose.yml +13 -0
  30. package/examples/mcp-postgres-billing-app-handler/schema.sql +59 -0
  31. package/examples/mcp-postgres-billing-app-handler/scripts/run-demo.sh +100 -0
  32. package/examples/mcp-postgres-billing-app-handler/seed.sql +39 -0
  33. package/examples/mcp-postgres-billing-app-handler/synapsor-handler.mjs +437 -0
  34. package/examples/mcp-postgres-billing-app-handler/synapsor.runner.json +158 -0
  35. package/examples/openai-agents-http/README.md +10 -2
  36. package/examples/openai-agents-stdio/README.md +8 -4
  37. package/examples/openai-agents-stdio/agent.py +2 -0
  38. package/fixtures/benchmark/mcp-efficiency.json +53 -0
  39. package/fixtures/benchmark/mcp-efficiency.txt +25 -0
  40. package/fixtures/protocol/MANIFEST.json +54 -0
  41. package/fixtures/protocol/change-set.late-fee-waiver.v1.json +72 -0
  42. package/fixtures/protocol/execution-receipt.applied.v1.json +14 -0
  43. package/fixtures/protocol/execution-receipt.conflict.v1.json +15 -0
  44. package/fixtures/protocol/runner-registration.v1.json +22 -0
  45. package/fixtures/protocol/writeback-job.late-fee-waiver.v1.json +44 -0
  46. package/package.json +4 -1
  47. package/schemas/change-set.v1.schema.json +140 -0
  48. package/schemas/execution-receipt.v1.schema.json +34 -0
  49. package/schemas/onboarding-selection.v1.schema.json +125 -0
  50. package/schemas/runner-registration.v1.schema.json +48 -0
  51. package/schemas/synapsor.app-handler-receipt.v1.json +39 -0
  52. package/schemas/synapsor.app-handler-request.v1.json +119 -0
  53. package/schemas/synapsor.runner.schema.json +412 -0
  54. package/schemas/writeback-job.v1.schema.json +121 -0
package/dist/runner.mjs CHANGED
@@ -444,7 +444,7 @@ function sleep(ms) {
444
444
  }
445
445
 
446
446
  // packages/config/src/index.ts
447
- var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["version", "mode", "storage", "sources", "trusted_context", "contexts", "executors", "capabilities", "cloud", "strict"]);
447
+ var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["version", "mode", "storage", "sources", "trusted_context", "contexts", "executors", "capabilities", "cloud", "strict", "result_format"]);
448
448
  var STORAGE_KEYS = /* @__PURE__ */ new Set(["sqlite_path"]);
449
449
  var CLOUD_KEYS = /* @__PURE__ */ new Set(["base_url_env", "runner_token_env", "runner_id", "runner_version", "project_id", "adapter_id", "source_id", "engines", "capabilities", "session"]);
450
450
  var SOURCE_KEYS = /* @__PURE__ */ new Set([
@@ -456,11 +456,13 @@ var SOURCE_KEYS = /* @__PURE__ */ new Set([
456
456
  ]);
457
457
  var TRUSTED_CONTEXT_KEYS = /* @__PURE__ */ new Set(["provider", "values"]);
458
458
  var CONTEXT_KEYS = TRUSTED_CONTEXT_KEYS;
459
- var EXECUTOR_KEYS = /* @__PURE__ */ new Set(["type", "url_env", "method", "auth", "timeout_ms", "command_env"]);
459
+ var EXECUTOR_KEYS = /* @__PURE__ */ new Set(["type", "url_env", "method", "auth", "signing_secret_env", "timeout_ms", "command_env"]);
460
460
  var EXECUTOR_AUTH_KEYS = /* @__PURE__ */ new Set(["type", "token_env"]);
461
461
  var CAPABILITY_KEYS = /* @__PURE__ */ new Set([
462
462
  "name",
463
463
  "kind",
464
+ "description",
465
+ "returns_hint",
464
466
  "source",
465
467
  "context",
466
468
  "executor",
@@ -480,7 +482,7 @@ var CAPABILITY_KEYS = /* @__PURE__ */ new Set([
480
482
  ]);
481
483
  var TARGET_KEYS = /* @__PURE__ */ new Set(["schema", "table", "primary_key", "tenant_key", "single_tenant_dev"]);
482
484
  var LOOKUP_KEYS = /* @__PURE__ */ new Set(["id_from_arg"]);
483
- var ARG_KEYS = /* @__PURE__ */ new Set(["type", "required", "max_length", "minimum", "maximum", "enum"]);
485
+ var ARG_KEYS = /* @__PURE__ */ new Set(["type", "description", "required", "max_length", "minimum", "maximum", "enum"]);
484
486
  var PATCH_BINDING_KEYS = /* @__PURE__ */ new Set(["fixed", "from_arg"]);
485
487
  var NUMERIC_BOUND_KEYS = /* @__PURE__ */ new Set(["minimum", "maximum"]);
486
488
  var TRANSITION_GUARD_KEYS = /* @__PURE__ */ new Set(["from_column", "allowed"]);
@@ -549,6 +551,9 @@ function validateRunnerCapabilityConfig(input) {
549
551
  if (input.version !== 1) {
550
552
  errors.push({ path: "$.version", code: "UNSUPPORTED_CONFIG_VERSION", message: "Runner config version must be 1." });
551
553
  }
554
+ if (input.result_format !== void 0 && input.result_format !== 1 && input.result_format !== 2) {
555
+ errors.push({ path: "$.result_format", code: "INVALID_RESULT_FORMAT", message: "result_format must be 1 or 2." });
556
+ }
552
557
  if (!isRunnerMode(input.mode)) {
553
558
  errors.push({ path: "$.mode", code: "INVALID_MODE", message: "mode must be read_only, shadow, review, or cloud." });
554
559
  }
@@ -797,6 +802,9 @@ function validateExecutors(value, mode, strict, errors) {
797
802
  errors.push({ path: `${path4}.method`, code: "INVALID_HANDLER_METHOD", message: "http_handler.method must be POST, PUT, or PATCH." });
798
803
  }
799
804
  validateExecutorAuth(executor.auth, `${path4}.auth`, strict, errors);
805
+ if (executor.signing_secret_env !== void 0 && !isEnvName(executor.signing_secret_env)) {
806
+ errors.push({ path: `${path4}.signing_secret_env`, code: "HANDLER_SIGNING_SECRET_ENV_INVALID", message: "http_handler.signing_secret_env must name an environment variable containing the HMAC signing secret." });
807
+ }
800
808
  if (executor.timeout_ms !== void 0 && !isPositiveInteger(executor.timeout_ms)) {
801
809
  errors.push({ path: `${path4}.timeout_ms`, code: "INVALID_HANDLER_TIMEOUT", message: "http_handler.timeout_ms must be a positive integer." });
802
810
  }
@@ -852,6 +860,12 @@ function validateCapability(value, index, sourceNames, contextNames, executorNam
852
860
  if (!isCapabilityKind(value.kind)) {
853
861
  errors.push({ path: `${path4}.kind`, code: "INVALID_CAPABILITY_KIND", message: "kind must be read or proposal." });
854
862
  }
863
+ if (value.description !== void 0 && !isNonEmptyString(value.description)) {
864
+ errors.push({ path: `${path4}.description`, code: "INVALID_CAPABILITY_DESCRIPTION", message: "description must be a non-empty string when provided." });
865
+ }
866
+ if (value.returns_hint !== void 0 && !isNonEmptyString(value.returns_hint)) {
867
+ errors.push({ path: `${path4}.returns_hint`, code: "INVALID_RETURNS_HINT", message: "returns_hint must be a non-empty string when provided." });
868
+ }
855
869
  if (!isNonEmptyString(value.source) || !sourceNames.has(value.source)) {
856
870
  errors.push({ path: `${path4}.source`, code: "UNKNOWN_SOURCE", message: "Capability source must reference a configured source." });
857
871
  }
@@ -926,6 +940,9 @@ function validateArgs(value, path4, strict, errors) {
926
940
  if (!["string", "number", "boolean"].includes(String(arg.type))) {
927
941
  errors.push({ path: `${argPath}.type`, code: "INVALID_ARG_TYPE", message: "Argument type must be string, number, or boolean." });
928
942
  }
943
+ if (arg.description !== void 0 && !isNonEmptyString(arg.description)) {
944
+ errors.push({ path: `${argPath}.description`, code: "INVALID_ARG_DESCRIPTION", message: "Argument description must be a non-empty string when provided." });
945
+ }
929
946
  if (arg.max_length !== void 0 && !isPositiveInteger(arg.max_length)) {
930
947
  errors.push({ path: `${argPath}.max_length`, code: "INVALID_MAX_LENGTH", message: "max_length must be a positive integer." });
931
948
  }
@@ -2073,6 +2090,11 @@ var ProposalStore = class {
2073
2090
  const rows = this.db.prepare("SELECT * FROM proposal_events WHERE proposal_id = ? ORDER BY event_id ASC").all(proposalId);
2074
2091
  return rows.map(rowToEvent).filter((event) => event !== void 0);
2075
2092
  }
2093
+ listEvents(filters = {}) {
2094
+ const query = buildEventQuery(filters);
2095
+ const rows = this.db.prepare(query.sql).all(...query.params);
2096
+ return rows.map(rowToEvent).filter((event) => event !== void 0);
2097
+ }
2076
2098
  receipts(proposalId) {
2077
2099
  const rows = this.db.prepare("SELECT * FROM writeback_receipts WHERE proposal_id = ? ORDER BY receipt_id ASC").all(proposalId);
2078
2100
  return rows.map(rowToReceipt).filter((receipt) => receipt !== void 0);
@@ -2387,6 +2409,15 @@ function buildReceiptQuery(filters) {
2387
2409
  addTimeRange(clauses, params, "created_at", filters.from, filters.to);
2388
2410
  return finishQuery("SELECT * FROM writeback_receipts", clauses, params, filters.limit);
2389
2411
  }
2412
+ function buildEventQuery(filters) {
2413
+ const clauses = [];
2414
+ const params = [];
2415
+ addEqual(clauses, params, "proposal_id", filters.proposal);
2416
+ addEqual(clauses, params, "kind", filters.kind);
2417
+ addEqual(clauses, params, "actor", filters.actor);
2418
+ addTimeRange(clauses, params, "created_at", filters.from, filters.to);
2419
+ return finishQuery("SELECT * FROM proposal_events", clauses, params, filters.limit);
2420
+ }
2390
2421
  function addEqual(clauses, params, column, value) {
2391
2422
  if (!value) return;
2392
2423
  clauses.push(`${column} = ?`);
@@ -2670,74 +2701,118 @@ function loadRuntimeConfigFromFile(configPath = process.env.SYNAPSOR_MCP_CONFIG
2670
2701
  function createMcpRuntime(config, options = {}) {
2671
2702
  assertValidRunnerCapabilityConfig(config);
2672
2703
  const env = options.env ?? process.env;
2673
- const store = options.store ?? new ProposalStore(options.storePath ?? config.storage?.sqlite_path ?? "./.synapsor/local.db");
2704
+ const storePath = options.storePath ?? config.storage?.sqlite_path ?? "./.synapsor/local.db";
2705
+ const ownsStore = !options.store;
2706
+ const store = options.store ?? new ProposalStore(storePath);
2674
2707
  const readRow = options.readRow ?? readCurrentRow;
2675
2708
  const cloudClient = options.controlPlaneClient ?? (config.mode === "cloud" ? createCloudClient(config, env) : void 0);
2676
2709
  const cloudTools = options.cloudTools ?? [];
2710
+ const resultFormat = options.resultFormat ?? config.result_format ?? 1;
2711
+ const assertStoreAvailable = () => {
2712
+ if (ownsStore) assertPersistentStoreAvailable(storePath);
2713
+ };
2677
2714
  return {
2678
2715
  config,
2679
2716
  store,
2680
2717
  listTools: () => config.mode === "cloud" ? cloudTools : listedLocalCapabilities(config).map((capability) => toolMetadata(capability)),
2681
- callTool: async (name, args) => callConfiguredTool({ config, env, store, readRow, cloudClient, name, args }),
2682
- readResource: (uri) => readLocalResource(store, uri),
2718
+ callTool: async (name, args) => {
2719
+ if (resultFormat === 2) {
2720
+ try {
2721
+ assertStoreAvailable();
2722
+ return await callConfiguredToolV2({ config, env, store, readRow, cloudClient, name, args });
2723
+ } catch (error) {
2724
+ const capability = config.mode === "cloud" ? void 0 : localCapabilities(config).find((item) => item.name === name);
2725
+ return errorEnvelopeFromError(error, capability, name);
2726
+ }
2727
+ }
2728
+ assertStoreAvailable();
2729
+ return callConfiguredTool({ config, env, store, readRow, cloudClient, name, args });
2730
+ },
2731
+ readResource: (uri) => {
2732
+ assertStoreAvailable();
2733
+ return readLocalResource(store, uri);
2734
+ },
2683
2735
  close: () => {
2684
2736
  if (!options.store) store.close();
2685
2737
  }
2686
2738
  };
2687
2739
  }
2688
- function createSynapsorMcpServer(runtime) {
2740
+ function assertPersistentStoreAvailable(storePath) {
2741
+ if (storePath === ":memory:") return;
2742
+ if (fs.existsSync(storePath)) return;
2743
+ throw new McpRuntimeError(
2744
+ "LOCAL_STORE_UNAVAILABLE",
2745
+ "The local Synapsor store is temporarily unavailable. Restart the runner or recreate the store before retrying."
2746
+ );
2747
+ }
2748
+ function createSynapsorMcpServer(runtime, options = {}) {
2689
2749
  const server = new McpServer(
2690
- { name: "synapsor-runner", version: "0.1.0-alpha.10" },
2750
+ { name: "synapsor-runner", version: "0.1.0-alpha.13" },
2691
2751
  { capabilities: { tools: {}, resources: {} } }
2692
2752
  );
2753
+ const toolNameStyle = options.toolNameStyle ?? "canonical";
2693
2754
  if (runtime.config.mode === "cloud") {
2694
- for (const tool of runtime.listTools()) {
2695
- server.registerTool(
2696
- tool.name,
2697
- {
2698
- title: tool.title,
2699
- description: tool.description,
2700
- inputSchema: zodInputShapeFromJsonSchema(tool.input_schema),
2701
- annotations: {
2702
- readOnlyHint: Boolean(tool.annotations.readOnlyHint),
2703
- destructiveHint: false,
2704
- idempotentHint: Boolean(tool.annotations.idempotentHint),
2705
- openWorldHint: false
2755
+ const tools2 = runtime.listTools();
2756
+ const exposedNames = toolNameExposureMap(tools2.map((tool) => tool.name), toolNameStyle);
2757
+ for (const tool of tools2) {
2758
+ for (const exposedName of exposedNames.get(tool.name) ?? [tool.name]) {
2759
+ server.registerTool(
2760
+ exposedName,
2761
+ {
2762
+ title: tool.title,
2763
+ description: toolDescriptionWithCanonical(tool.description, tool.name, exposedName),
2764
+ inputSchema: zodInputShapeFromJsonSchema(tool.input_schema),
2765
+ annotations: {
2766
+ readOnlyHint: Boolean(tool.annotations.readOnlyHint),
2767
+ destructiveHint: false,
2768
+ idempotentHint: Boolean(tool.annotations.idempotentHint),
2769
+ openWorldHint: false
2770
+ },
2771
+ _meta: {
2772
+ ...tool.annotations,
2773
+ "synapsor.cloud_delegated": true,
2774
+ "synapsor.canonical_tool_name": tool.name,
2775
+ "synapsor.exposed_tool_name": exposedName,
2776
+ "synapsor.tool_name_style": toolNameStyle,
2777
+ "synapsor.raw_sql_exposed": false,
2778
+ "synapsor.approval_tool": false
2779
+ }
2706
2780
  },
2707
- _meta: {
2708
- ...tool.annotations,
2709
- "synapsor.cloud_delegated": true,
2710
- "synapsor.raw_sql_exposed": false,
2711
- "synapsor.approval_tool": false
2712
- }
2713
- },
2714
- async (args) => toolCallResult(runtime, tool.name, args)
2715
- );
2781
+ async (args) => toolCallResult(runtime, tool.name, args)
2782
+ );
2783
+ }
2716
2784
  }
2717
2785
  } else {
2718
- for (const capability of listedLocalCapabilities(runtime.config)) {
2719
- server.registerTool(
2720
- capability.name,
2721
- {
2722
- title: capability.name,
2723
- description: capabilityDescription(capability),
2724
- inputSchema: zodInputShape(capability),
2725
- annotations: {
2726
- readOnlyHint: capability.kind === "read",
2727
- destructiveHint: false,
2728
- idempotentHint: capability.kind === "read",
2729
- openWorldHint: false
2786
+ const capabilities = listedLocalCapabilities(runtime.config);
2787
+ const exposedNames = toolNameExposureMap(capabilities.map((capability) => capability.name), toolNameStyle);
2788
+ for (const capability of capabilities) {
2789
+ for (const exposedName of exposedNames.get(capability.name) ?? [capability.name]) {
2790
+ server.registerTool(
2791
+ exposedName,
2792
+ {
2793
+ title: capability.name,
2794
+ description: capabilityDescription(capability, exposedName),
2795
+ inputSchema: zodInputShape(capability),
2796
+ annotations: {
2797
+ readOnlyHint: capability.kind === "read",
2798
+ destructiveHint: false,
2799
+ idempotentHint: capability.kind === "read",
2800
+ openWorldHint: false
2801
+ },
2802
+ _meta: {
2803
+ "synapsor.kind": capability.kind,
2804
+ "synapsor.source": capability.source,
2805
+ "synapsor.target": `${capability.target.schema}.${capability.target.table}`,
2806
+ "synapsor.canonical_tool_name": capability.name,
2807
+ "synapsor.exposed_tool_name": exposedName,
2808
+ "synapsor.tool_name_style": toolNameStyle,
2809
+ "synapsor.raw_sql_exposed": false,
2810
+ "synapsor.approval_tool": false
2811
+ }
2730
2812
  },
2731
- _meta: {
2732
- "synapsor.kind": capability.kind,
2733
- "synapsor.source": capability.source,
2734
- "synapsor.target": `${capability.target.schema}.${capability.target.table}`,
2735
- "synapsor.raw_sql_exposed": false,
2736
- "synapsor.approval_tool": false
2737
- }
2738
- },
2739
- async (args) => toolCallResult(runtime, capability.name, args)
2740
- );
2813
+ async (args) => toolCallResult(runtime, capability.name, args)
2814
+ );
2815
+ }
2741
2816
  }
2742
2817
  }
2743
2818
  server.registerResource(
@@ -2763,8 +2838,8 @@ function createSynapsorMcpServer(runtime) {
2763
2838
  async function serveStdio(options = {}) {
2764
2839
  const config = options.config ?? loadRuntimeConfigFromFile(options.configPath);
2765
2840
  const cloudTools = config.mode === "cloud" ? await fetchCloudToolMetadata(config, process.env) : void 0;
2766
- const runtime = createMcpRuntime(config, { storePath: options.storePath, cloudTools });
2767
- const server = createSynapsorMcpServer(runtime);
2841
+ const runtime = createMcpRuntime(config, { storePath: options.storePath, resultFormat: options.resultFormat, cloudTools });
2842
+ const server = createSynapsorMcpServer(runtime, { toolNameStyle: options.toolNameStyle });
2768
2843
  const transport = new StdioServerTransport();
2769
2844
  await server.connect(transport);
2770
2845
  await new Promise((resolve) => {
@@ -2799,6 +2874,7 @@ async function startHttpMcpServer(options = {}) {
2799
2874
  const runtime = createMcpRuntime(config, {
2800
2875
  env,
2801
2876
  storePath: options.storePath,
2877
+ resultFormat: options.resultFormat,
2802
2878
  readRow: options.readRow,
2803
2879
  cloudTools
2804
2880
  });
@@ -2872,6 +2948,8 @@ async function startStreamableHttpMcpServer(options = {}) {
2872
2948
  readRow: options.readRow,
2873
2949
  cloudTools,
2874
2950
  env,
2951
+ toolNameStyle: options.toolNameStyle,
2952
+ resultFormat: options.resultFormat,
2875
2953
  authToken,
2876
2954
  devNoAuth,
2877
2955
  corsOrigin: options.corsOrigin,
@@ -2914,7 +2992,7 @@ async function startStreamableHttpMcpServer(options = {}) {
2914
2992
  };
2915
2993
  }
2916
2994
  async function handleStreamableHttpMcpRequest(input) {
2917
- const { request, response, config, storePath, readRow, cloudTools, env, authToken, devNoAuth, corsOrigin, sessions, openSessions } = input;
2995
+ const { request, response, config, storePath, readRow, cloudTools, env, toolNameStyle, resultFormat, authToken, devNoAuth, corsOrigin, sessions, openSessions } = input;
2918
2996
  try {
2919
2997
  setCorsHeaders(response, corsOrigin);
2920
2998
  if (request.method === "OPTIONS" && corsOrigin) {
@@ -2976,13 +3054,13 @@ async function handleStreamableHttpMcpRequest(input) {
2976
3054
  }
2977
3055
  }
2978
3056
  });
2979
- const runtime = createMcpRuntime(config, { env, storePath, readRow, cloudTools });
3057
+ const runtime = createMcpRuntime(config, { env, storePath, resultFormat, readRow, cloudTools });
2980
3058
  session = { transport, runtime };
2981
3059
  openSessions.add(session);
2982
3060
  transport.onclose = () => {
2983
3061
  if (session) disposeStreamableSession(session, sessions, openSessions);
2984
3062
  };
2985
- await createSynapsorMcpServer(runtime).connect(transport);
3063
+ await createSynapsorMcpServer(runtime, { toolNameStyle }).connect(transport);
2986
3064
  await transport.handleRequest(request, response, parsedBody);
2987
3065
  } catch (error) {
2988
3066
  const message = sanitizeHttpError(error, authToken);
@@ -3103,6 +3181,50 @@ function requestIdFromPayload(payload) {
3103
3181
  }
3104
3182
  return isRecord3(payload) ? payload.id ?? null : null;
3105
3183
  }
3184
+ function openaiToolNameAlias(canonicalName) {
3185
+ const sanitized = canonicalName.replace(/[^A-Za-z0-9_-]+/g, "__").replace(/_{3,}/g, "__").replace(/^_+|_+$/g, "");
3186
+ const base = sanitized.length > 0 ? sanitized : `tool_${shortToolHash(canonicalName)}`;
3187
+ if (base.length <= 64) return base;
3188
+ const suffix = shortToolHash(canonicalName);
3189
+ return `${base.slice(0, Math.max(1, 63 - suffix.length)).replace(/_+$/g, "")}_${suffix}`;
3190
+ }
3191
+ function toolNameExposures(canonicalNames, style) {
3192
+ const exposedNames = toolNameExposureMap(canonicalNames, style);
3193
+ return canonicalNames.flatMap((canonicalName) => {
3194
+ return (exposedNames.get(canonicalName) ?? [canonicalName]).map((exposedName) => ({
3195
+ canonicalName,
3196
+ exposedName,
3197
+ isAlias: exposedName !== canonicalName,
3198
+ style
3199
+ }));
3200
+ });
3201
+ }
3202
+ function toolNameExposureMap(canonicalNames, style) {
3203
+ const exposedByCanonical = /* @__PURE__ */ new Map();
3204
+ const canonicalByExposed = /* @__PURE__ */ new Map();
3205
+ if (style === "both") {
3206
+ for (const canonical of canonicalNames) canonicalByExposed.set(canonical, canonical);
3207
+ }
3208
+ for (const canonical of canonicalNames) {
3209
+ const names = /* @__PURE__ */ new Set();
3210
+ if (style === "canonical" || style === "both") names.add(canonical);
3211
+ if (style === "openai" || style === "both") {
3212
+ let alias = openaiToolNameAlias(canonical);
3213
+ const existing = canonicalByExposed.get(alias);
3214
+ if (existing && existing !== canonical) {
3215
+ const suffix = shortToolHash(canonical);
3216
+ alias = `${alias.slice(0, Math.max(1, 63 - suffix.length)).replace(/_+$/g, "")}_${suffix}`;
3217
+ }
3218
+ canonicalByExposed.set(alias, canonical);
3219
+ names.add(alias);
3220
+ }
3221
+ exposedByCanonical.set(canonical, [...names]);
3222
+ }
3223
+ return exposedByCanonical;
3224
+ }
3225
+ function shortToolHash(value) {
3226
+ return crypto.createHash("sha256").update(value).digest("hex").slice(0, 8);
3227
+ }
3106
3228
  function setCorsHeaders(response, corsOrigin) {
3107
3229
  if (corsOrigin) {
3108
3230
  response.setHeader("access-control-allow-origin", corsOrigin);
@@ -3270,6 +3392,11 @@ function cloudToolMetadata(tool) {
3270
3392
  }
3271
3393
  };
3272
3394
  }
3395
+ function toolDescriptionWithCanonical(description, canonicalName, exposedName) {
3396
+ if (!exposedName || exposedName === canonicalName) return description;
3397
+ return `Canonical Synapsor capability: ${canonicalName}.
3398
+ ${description}`;
3399
+ }
3273
3400
  function zodInputShapeFromJsonSchema(schema) {
3274
3401
  const properties = isRecord3(schema.properties) ? schema.properties : {};
3275
3402
  const required = Array.isArray(schema.required) ? new Set(schema.required.map(String)) : /* @__PURE__ */ new Set();
@@ -3447,6 +3574,135 @@ async function callConfiguredTool(input) {
3447
3574
  source_database_mutated: false
3448
3575
  };
3449
3576
  }
3577
+ async function callConfiguredToolV2(input) {
3578
+ const capability = input.config.mode === "cloud" ? void 0 : localCapabilities(input.config).find((item) => item.name === input.name);
3579
+ try {
3580
+ const legacy = await callConfiguredTool(input);
3581
+ return resultEnvelopeFromLegacy(legacy, capability, input.name);
3582
+ } catch (error) {
3583
+ return errorEnvelopeFromError(error, capability, input.name);
3584
+ }
3585
+ }
3586
+ function resultEnvelopeFromLegacy(legacy, capability, canonicalName) {
3587
+ const action = typeof legacy.action === "string" ? legacy.action : canonicalName;
3588
+ const kind = capability?.kind ?? (typeof legacy.proposal_id === "string" ? "proposal" : "read");
3589
+ const evidenceBundleId = typeof legacy.evidence_bundle_id === "string" ? legacy.evidence_bundle_id : void 0;
3590
+ const sourceChanged = Boolean(legacy.source_database_changed ?? legacy.source_database_mutated ?? false);
3591
+ const context = isRecord3(legacy.trusted_context) ? legacy.trusted_context : void 0;
3592
+ const target2 = isRecord3(legacy.target) ? legacy.target : void 0;
3593
+ if (kind === "proposal") {
3594
+ const proposalId = typeof legacy.proposal_id === "string" ? legacy.proposal_id : "wrp_unknown";
3595
+ const targetType = typeof target2?.type === "string" ? target2.type : capability?.target.table ?? "object";
3596
+ const targetId = target2?.id !== void 0 ? String(target2.id) : "unknown";
3597
+ const executor = writebackExecutorName(legacy.writeback);
3598
+ const writebackMode = executor && executor !== "sql_update" && executor !== "trusted_worker_required" ? "app_handler" : "direct_update";
3599
+ return {
3600
+ ok: true,
3601
+ summary: `Created proposal ${proposalId} for ${targetType} ${targetId}. Source database changed: no.`,
3602
+ action,
3603
+ kind,
3604
+ data: null,
3605
+ proposal: {
3606
+ id: proposalId,
3607
+ state: typeof legacy.status === "string" ? legacy.status : "review_required",
3608
+ target: `${targetType}:${targetId}`,
3609
+ diff: isRecord3(legacy.diff) ? legacy.diff : {},
3610
+ approval_required: legacy.approval_required !== false,
3611
+ writeback: {
3612
+ mode: writebackMode,
3613
+ applied: false
3614
+ },
3615
+ next: "A human must approve outside this model-facing tool surface; nothing is committed yet."
3616
+ },
3617
+ error: null,
3618
+ evidence: evidenceBundleId ? evidenceHandle(evidenceBundleId) : null,
3619
+ source_database_changed: sourceChanged,
3620
+ _meta: {
3621
+ tenant_id: typeof target2?.tenant_id === "string" ? target2.tenant_id : void 0,
3622
+ principal: typeof context?.principal === "string" ? context.principal : void 0,
3623
+ provenance: typeof context?.provenance === "string" ? context.provenance : void 0,
3624
+ canonical_capability: action
3625
+ }
3626
+ };
3627
+ }
3628
+ const businessObject = isRecord3(legacy.business_object) ? legacy.business_object : void 0;
3629
+ const objectType = typeof businessObject?.type === "string" ? businessObject.type : capability?.target.table ?? "record";
3630
+ const objectId = businessObject?.id !== void 0 ? String(businessObject.id) : String(legacy.action ?? action);
3631
+ return {
3632
+ ok: true,
3633
+ summary: `Read ${objectType} ${objectId} through ${action}. Source database changed: no.`,
3634
+ action,
3635
+ kind: "read",
3636
+ data: isRecord3(legacy.data) ? legacy.data : {},
3637
+ proposal: null,
3638
+ error: null,
3639
+ evidence: evidenceBundleId ? evidenceHandle(evidenceBundleId) : null,
3640
+ source_database_changed: sourceChanged,
3641
+ _meta: {
3642
+ tenant_id: typeof context?.tenant_id === "string" ? context.tenant_id : void 0,
3643
+ principal: typeof context?.principal === "string" ? context.principal : void 0,
3644
+ provenance: typeof context?.provenance === "string" ? context.provenance : void 0,
3645
+ canonical_capability: action
3646
+ }
3647
+ };
3648
+ }
3649
+ function writebackExecutorName(value) {
3650
+ if (!isRecord3(value)) return void 0;
3651
+ return typeof value.executor === "string" ? value.executor : typeof value.mode === "string" ? value.mode : void 0;
3652
+ }
3653
+ function evidenceHandle(bundleId) {
3654
+ return {
3655
+ bundle_id: bundleId,
3656
+ note: "audit/replay handle; you do not need to act on it during this turn"
3657
+ };
3658
+ }
3659
+ function errorEnvelopeFromError(error, capability, canonicalName) {
3660
+ const safe = safeToolError(error);
3661
+ const action = capability?.name ?? canonicalName;
3662
+ return {
3663
+ ok: false,
3664
+ summary: safe.message,
3665
+ action,
3666
+ kind: capability?.kind ?? "read",
3667
+ data: null,
3668
+ proposal: null,
3669
+ error: safe,
3670
+ evidence: null,
3671
+ source_database_changed: false,
3672
+ _meta: {
3673
+ canonical_capability: action
3674
+ }
3675
+ };
3676
+ }
3677
+ function safeToolError(error) {
3678
+ const runtimeCode = error instanceof McpRuntimeError ? error.code : void 0;
3679
+ if (runtimeCode === "ROW_NOT_FOUND") {
3680
+ return { code: "NOT_FOUND_IN_TENANT", message: "No authorized row was found in the trusted tenant scope.", retryable: false };
3681
+ }
3682
+ if (runtimeCode === "MCP_TOOL_NOT_FOUND") {
3683
+ return { code: "CAPABILITY_NOT_FOUND", message: "The requested Synapsor capability is not available.", retryable: false };
3684
+ }
3685
+ if (runtimeCode === "PROPOSALS_DISABLED") {
3686
+ return { code: "APPROVAL_REQUIRED", message: "Proposal tools are disabled for this runner mode.", retryable: false };
3687
+ }
3688
+ if (runtimeCode && (runtimeCode.startsWith("ARGUMENT_") || runtimeCode === "LOOKUP_ARG_MISSING" || runtimeCode === "MODEL_CANNOT_OVERRIDE_BINDING" || runtimeCode === "TRUSTED_BINDING_MISSING" || runtimeCode === "TRUSTED_CONTEXT_MISSING")) {
3689
+ return { code: "INVALID_ARGUMENT", message: "The tool input or trusted context binding is invalid.", retryable: false };
3690
+ }
3691
+ if (runtimeCode && (runtimeCode.startsWith("PATCH_") || runtimeCode === "CONFLICT_GUARD_MISSING")) {
3692
+ return { code: "POLICY_VIOLATION", message: "The requested change is outside the reviewed capability policy.", retryable: false };
3693
+ }
3694
+ if (runtimeCode === "LOCAL_STORE_UNAVAILABLE") {
3695
+ return { code: "TEMPORARILY_UNAVAILABLE", message: "The local runner store is temporarily unavailable. Restart the runner or recreate the store before retrying.", retryable: true };
3696
+ }
3697
+ if (runtimeCode === "SOURCE_CREDENTIAL_MISSING" || looksLikeInfraError(error)) {
3698
+ return { code: "TEMPORARILY_UNAVAILABLE", message: "The database is temporarily unavailable. Retry later.", retryable: true };
3699
+ }
3700
+ return { code: "INTERNAL", message: "The capability failed safely. Check the local runner logs for details.", retryable: false };
3701
+ }
3702
+ function looksLikeInfraError(error) {
3703
+ const message = error instanceof Error ? error.message : String(error ?? "");
3704
+ return /\b(ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|timeout|connect|connection|database|authentication|certificate)\b/i.test(message);
3705
+ }
3450
3706
  function buildChangeSet(input) {
3451
3707
  const patch = buildPatch(input.capability, input.args);
3452
3708
  const before = scalarRecord(input.currentRow);
@@ -3648,7 +3904,7 @@ function zodInputShape(capability) {
3648
3904
  if (spec.type === "number" && spec.maximum !== void 0) schema = schema.max(spec.maximum);
3649
3905
  if (spec.enum && spec.enum.length > 0) schema = schema.refine((value) => spec.enum?.includes(value), "value is not allowlisted");
3650
3906
  if (spec.required === false) schema = schema.optional();
3651
- shape[name] = schema.describe(`${name} business argument`);
3907
+ shape[name] = schema.describe(spec.description ?? `${name} business argument`);
3652
3908
  }
3653
3909
  return shape;
3654
3910
  }
@@ -3661,6 +3917,7 @@ function toolMetadata(capability) {
3661
3917
  input_schema: Object.fromEntries(Object.entries(capability.args).map(([name, spec]) => [name, {
3662
3918
  type: spec.type,
3663
3919
  required: spec.required !== false,
3920
+ ...spec.description !== void 0 ? { description: spec.description } : {},
3664
3921
  ...spec.max_length !== void 0 ? { max_length: spec.max_length } : {},
3665
3922
  ...spec.minimum !== void 0 ? { minimum: spec.minimum } : {},
3666
3923
  ...spec.maximum !== void 0 ? { maximum: spec.maximum } : {},
@@ -3676,11 +3933,23 @@ function toolMetadata(capability) {
3676
3933
  }
3677
3934
  };
3678
3935
  }
3679
- function capabilityDescription(capability) {
3680
- if (capability.kind === "read") {
3681
- return `Read ${capability.target.schema}.${capability.target.table} through a reviewed Synapsor capability with trusted tenant context and evidence.`;
3936
+ function capabilityDescription(capability, exposedName) {
3937
+ const lines = [];
3938
+ if (exposedName && exposedName !== capability.name) {
3939
+ lines.push(`Canonical Synapsor capability: ${capability.name}.`);
3940
+ }
3941
+ if (capability.description) {
3942
+ lines.push(capability.description);
3943
+ } else if (capability.kind === "read") {
3944
+ lines.push(`Read ${capability.target.schema}.${capability.target.table} through a reviewed Synapsor capability with trusted tenant context and evidence.`);
3945
+ } else {
3946
+ lines.push(`Create an evidence-backed Synapsor proposal for ${capability.target.schema}.${capability.target.table}; the source database is not mutated by this tool.`);
3947
+ }
3948
+ if (capability.returns_hint) {
3949
+ lines.push(capability.returns_hint);
3682
3950
  }
3683
- return `Create an evidence-backed Synapsor proposal for ${capability.target.schema}.${capability.target.table}; the source database is not mutated by this tool.`;
3951
+ lines.push("Evidence handles are audit/replay handles; the model does not need to call them during this turn.");
3952
+ return lines.join("\n");
3684
3953
  }
3685
3954
  function buildPatch(capability, args) {
3686
3955
  if (!capability.patch) throw new McpRuntimeError("PATCH_REQUIRED", "Proposal capability has no patch mapping.");
@@ -3797,6 +4066,9 @@ function isRecord3(value) {
3797
4066
  }
3798
4067
  function toolErrorPayload(error) {
3799
4068
  if (error instanceof McpRuntimeError) {
4069
+ if (error.code === "LOCAL_STORE_UNAVAILABLE") {
4070
+ return { ok: false, code: "TEMPORARILY_UNAVAILABLE", error: "The local runner store is temporarily unavailable. Restart the runner or recreate the store before retrying." };
4071
+ }
3800
4072
  return { ok: false, code: error.code, error: error.message };
3801
4073
  }
3802
4074
  return { ok: false, code: "MCP_TOOL_FAILED", error: error instanceof Error ? error.message : String(error) };
@@ -4659,6 +4931,7 @@ function normalizedWriteback(spec) {
4659
4931
  if (executor === "http_handler") {
4660
4932
  const urlEnv = spec.writeback?.handler_url_env ?? "SYNAPSOR_APP_WRITEBACK_URL";
4661
4933
  const tokenEnv = spec.writeback?.handler_token_env;
4934
+ const signingSecretEnv = spec.writeback?.handler_signing_secret_env;
4662
4935
  return {
4663
4936
  executor,
4664
4937
  executorName,
@@ -4668,12 +4941,14 @@ function normalizedWriteback(spec) {
4668
4941
  url_env: urlEnv,
4669
4942
  method: "POST",
4670
4943
  ...tokenEnv ? { auth: { type: "bearer_env", token_env: tokenEnv } } : {},
4944
+ ...signingSecretEnv ? { signing_secret_env: signingSecretEnv } : {},
4671
4945
  timeout_ms: spec.writeback?.timeout_ms ?? 5e3
4672
4946
  }
4673
4947
  },
4674
4948
  extraEnv: [
4675
4949
  { name: urlEnv, value: "http://127.0.0.1:8787/synapsor/writeback", comment: "App-owned writeback handler endpoint." },
4676
- ...tokenEnv ? [{ name: tokenEnv, value: "<handler-bearer-token>", comment: "Optional handler bearer token." }] : []
4950
+ ...tokenEnv ? [{ name: tokenEnv, value: "<handler-bearer-token>", comment: "Optional handler bearer token." }] : [],
4951
+ ...signingSecretEnv ? [{ name: signingSecretEnv, value: "<handler-hmac-signing-secret>", comment: "Optional HMAC signing secret for Runner-to-handler requests." }] : []
4677
4952
  ]
4678
4953
  };
4679
4954
  }
@@ -4811,6 +5086,7 @@ function validateSelectionSpec(spec) {
4811
5086
  spec.write_url_env,
4812
5087
  spec.writeback?.handler_url_env,
4813
5088
  spec.writeback?.handler_token_env,
5089
+ spec.writeback?.handler_signing_secret_env,
4814
5090
  spec.writeback?.handler_command_env,
4815
5091
  spec.trusted_context?.tenant_id_env,
4816
5092
  spec.trusted_context?.principal_env
@@ -6525,6 +6801,7 @@ ${cliCommandName()} --help
6525
6801
  if (command === "query-audit") return queryAudit(rest);
6526
6802
  if (command === "receipts") return receipts(rest);
6527
6803
  if (command === "activity") return activity(rest);
6804
+ if (command === "events") return events(rest);
6528
6805
  if (command === "store") return storeCommand(rest);
6529
6806
  if (command === "shadow") return shadow(rest);
6530
6807
  if (command === "ui") return ui(rest);
@@ -6725,11 +7002,13 @@ async function runInitWizard(args, options = {}) {
6725
7002
  } else if (writebackPath === "http_handler") {
6726
7003
  const urlEnv = await askEnvName(ask, "App-owned HTTP handler URL env var", optionalArg(args, "--handler-url-env") ?? "SYNAPSOR_APP_WRITEBACK_URL");
6727
7004
  const tokenEnv = await askOptionalEnvName(ask, "Optional HTTP handler bearer-token env var", optionalArg(args, "--handler-token-env") ?? "");
7005
+ const signingSecretEnv = await askOptionalEnvName(ask, "Optional HTTP handler HMAC signing-secret env var", optionalArg(args, "--handler-signing-secret-env") ?? "");
6728
7006
  writeback2 = {
6729
7007
  executor: "http_handler",
6730
7008
  executor_name: optionalArg(args, "--executor-name"),
6731
7009
  handler_url_env: urlEnv,
6732
7010
  ...tokenEnv ? { handler_token_env: tokenEnv } : {},
7011
+ ...signingSecretEnv ? { handler_signing_secret_env: signingSecretEnv } : {},
6733
7012
  timeout_ms: positiveIntegerOption(args, "--handler-timeout-ms")
6734
7013
  };
6735
7014
  } else {
@@ -7032,6 +7311,7 @@ function writebackSpecFromArgs(args) {
7032
7311
  executor_name: optionalArg(args, "--executor-name"),
7033
7312
  handler_url_env: optionalArg(args, "--handler-url-env") ?? "SYNAPSOR_APP_WRITEBACK_URL",
7034
7313
  ...optionalArg(args, "--handler-token-env") ? { handler_token_env: optionalArg(args, "--handler-token-env") } : {},
7314
+ ...optionalArg(args, "--handler-signing-secret-env") ? { handler_signing_secret_env: optionalArg(args, "--handler-signing-secret-env") } : {},
7035
7315
  timeout_ms: positiveIntegerOption(args, "--handler-timeout-ms")
7036
7316
  };
7037
7317
  }
@@ -7470,6 +7750,54 @@ function envPresenceCheck(envName, message) {
7470
7750
  message: process2.env[envName] ? `${envName} is set.` : message
7471
7751
  };
7472
7752
  }
7753
+ async function httpHandlerReachabilityCheck(executorName, rawUrl, timeoutMs) {
7754
+ try {
7755
+ const url = new URL(rawUrl);
7756
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
7757
+ return {
7758
+ name: `executor:${executorName}:handler-reachability`,
7759
+ ok: false,
7760
+ level: "fail",
7761
+ message: "HTTP handler URL must use http or https."
7762
+ };
7763
+ }
7764
+ } catch {
7765
+ return {
7766
+ name: `executor:${executorName}:handler-reachability`,
7767
+ ok: false,
7768
+ level: "fail",
7769
+ message: "HTTP handler URL env value is not a valid URL."
7770
+ };
7771
+ }
7772
+ const controller = new AbortController();
7773
+ const timeout = setTimeout(() => controller.abort(), Math.max(1, Math.min(timeoutMs || 3e3, 1e4)));
7774
+ try {
7775
+ const response = await fetch(rawUrl, {
7776
+ method: "OPTIONS",
7777
+ headers: { accept: "application/json" },
7778
+ signal: controller.signal
7779
+ });
7780
+ return {
7781
+ name: `executor:${executorName}:handler-reachability`,
7782
+ ok: true,
7783
+ level: "pass",
7784
+ message: `HTTP handler endpoint responded with HTTP ${response.status}; network path is reachable. This is not an apply/writeback probe.`
7785
+ };
7786
+ } catch (error) {
7787
+ return {
7788
+ name: `executor:${executorName}:handler-reachability`,
7789
+ ok: false,
7790
+ level: "fail",
7791
+ message: `HTTP handler endpoint did not respond to the reachability probe (${safeReachabilityError(error)}).`
7792
+ };
7793
+ } finally {
7794
+ clearTimeout(timeout);
7795
+ }
7796
+ }
7797
+ function safeReachabilityError(error) {
7798
+ if (error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError")) return "timeout";
7799
+ return "connection failed";
7800
+ }
7473
7801
  async function inspectConfiguredSource(input) {
7474
7802
  if (!process2.env[input.source.read_url_env]) return;
7475
7803
  const capabilities = (input.config.capabilities ?? []).filter((capability) => capability.source === input.sourceName);
@@ -7899,6 +8227,8 @@ function formatFirstRunDoctor(report) {
7899
8227
  async function localDoctor(args) {
7900
8228
  const configPath = optionalArg(args, "--config") ?? "synapsor.runner.json";
7901
8229
  const allowSharedCredential = args.includes("--allow-shared-credential");
8230
+ const checkHandlers = args.includes("--check-handlers");
8231
+ const checkWriteback = args.includes("--check-writeback") || args.includes("--check-db");
7902
8232
  const parsed = JSON.parse(await fs3.readFile(configPath, "utf8"));
7903
8233
  const checks = [];
7904
8234
  const validation = validateRunnerCapabilityConfig(parsed);
@@ -7941,6 +8271,24 @@ async function localDoctor(args) {
7941
8271
  } else {
7942
8272
  checks.push({ name: `source:${sourceName}:write-url-env`, ok: false, level: "fail", message: "SQL writeback proposal capabilities require write_url_env for trusted writeback." });
7943
8273
  }
8274
+ const writeUrl = source.write_url_env ? process2.env[source.write_url_env] : void 0;
8275
+ if (checkWriteback && writeUrl) {
8276
+ checks.push(...await directSqlWritebackDoctorChecks(parsed, sourceName, source, writeUrl));
8277
+ } else if (checkWriteback) {
8278
+ checks.push({
8279
+ name: `source:${sourceName}:writeback-probe`,
8280
+ ok: false,
8281
+ level: "fail",
8282
+ message: "Direct SQL writeback probe skipped because the writer env var is missing."
8283
+ });
8284
+ } else {
8285
+ checks.push({
8286
+ name: `source:${sourceName}:writeback-probe`,
8287
+ ok: true,
8288
+ level: "warn",
8289
+ message: `Direct SQL writeback was not probed. Rerun doctor with --check-writeback to verify writer connectivity, receipt-table permissions, and rollback-only target-table access.`
8290
+ });
8291
+ }
7944
8292
  }
7945
8293
  }
7946
8294
  await inspectConfiguredSource({ config: parsed, sourceName, source, checks });
@@ -7949,10 +8297,34 @@ async function localDoctor(args) {
7949
8297
  if (!isRecord6(executor)) continue;
7950
8298
  if (executor.type === "http_handler") {
7951
8299
  const urlEnv = String(executor.url_env ?? "");
7952
- if (urlEnv) checks.push(envPresenceCheck(urlEnv, `${urlEnv} is required for http_handler executor ${executorName}.`));
8300
+ if (urlEnv) {
8301
+ checks.push(envPresenceCheck(urlEnv, `${urlEnv} is required for http_handler executor ${executorName}.`));
8302
+ const handlerUrl = process2.env[urlEnv];
8303
+ if (checkHandlers && handlerUrl) {
8304
+ checks.push(await httpHandlerReachabilityCheck(executorName, handlerUrl, Number(executor.timeout_ms ?? 3e3)));
8305
+ } else if (!checkHandlers) {
8306
+ checks.push({
8307
+ name: `executor:${executorName}:handler-reachability`,
8308
+ ok: true,
8309
+ level: "warn",
8310
+ message: `Handler reachability was not probed for ${executorName}. Rerun doctor with --check-handlers to verify the network path without applying a proposal.`
8311
+ });
8312
+ }
8313
+ }
7953
8314
  const auth = isRecord6(executor.auth) ? executor.auth : void 0;
7954
8315
  const tokenEnv = typeof auth?.token_env === "string" ? auth.token_env : void 0;
7955
8316
  if (tokenEnv) checks.push(envPresenceCheck(tokenEnv, `${tokenEnv} is required for http_handler executor ${executorName} bearer auth.`));
8317
+ const signingSecretEnv = typeof executor.signing_secret_env === "string" ? executor.signing_secret_env : void 0;
8318
+ if (signingSecretEnv) {
8319
+ checks.push(envPresenceCheck(signingSecretEnv, `${signingSecretEnv} is required to sign http_handler requests for executor ${executorName}.`));
8320
+ } else {
8321
+ checks.push({
8322
+ name: `executor:${executorName}:handler-signing`,
8323
+ ok: true,
8324
+ level: "warn",
8325
+ message: `No signing_secret_env is configured for http_handler executor ${executorName}. HMAC signing is recommended unless the handler is loopback-only and protected by another trusted boundary.`
8326
+ });
8327
+ }
7956
8328
  }
7957
8329
  if (executor.type === "command_handler") {
7958
8330
  const commandEnv = String(executor.command_env ?? "");
@@ -7989,6 +8361,155 @@ async function localDoctor(args) {
7989
8361
  }
7990
8362
  return report.ok ? 0 : 1;
7991
8363
  }
8364
+ async function directSqlWritebackDoctorChecks(config, sourceName, source, writeUrl) {
8365
+ const checks = [];
8366
+ try {
8367
+ const result = await adapters[source.engine].doctor({
8368
+ controlPlaneUrl: "local",
8369
+ runnerToken: "local",
8370
+ runnerId: "doctor",
8371
+ sourceId: sourceName,
8372
+ databaseUrl: writeUrl,
8373
+ engine: source.engine,
8374
+ pollIntervalMs: 0,
8375
+ logLevel: "error",
8376
+ dryRun: true,
8377
+ stateDir: "./state"
8378
+ });
8379
+ checks.push({
8380
+ name: `source:${sourceName}:receipt-table-probe`,
8381
+ ok: result.ok,
8382
+ level: result.ok ? "pass" : "fail",
8383
+ message: result.ok ? "Writer credential can reach the database and the receipt-table rollback probe succeeded." : `Writer receipt-table probe failed (${safeDatabaseProbeError(result.details)}). ${receiptTableGuidance(source.engine)}`
8384
+ });
8385
+ } catch (error) {
8386
+ checks.push({
8387
+ name: `source:${sourceName}:receipt-table-probe`,
8388
+ ok: false,
8389
+ level: "fail",
8390
+ message: `Writer receipt-table probe failed (${safeDatabaseProbeError(error)}). ${receiptTableGuidance(source.engine)}`
8391
+ });
8392
+ }
8393
+ for (const capability of directSqlProposalCapabilities(config, sourceName)) {
8394
+ try {
8395
+ await rollbackOnlyTargetProbe(source.engine, writeUrl, capability);
8396
+ checks.push({
8397
+ name: `capability:${capability.name}:writeback-target-probe`,
8398
+ ok: true,
8399
+ level: "pass",
8400
+ message: `Rollback-only writer probe reached ${capability.target.schema}.${capability.target.table} and verified configured write columns without mutating business rows.`
8401
+ });
8402
+ } catch (error) {
8403
+ checks.push({
8404
+ name: `capability:${capability.name}:writeback-target-probe`,
8405
+ ok: false,
8406
+ level: "fail",
8407
+ message: `Rollback-only writer probe failed for configured target ${capability.target.schema}.${capability.target.table} (${safeDatabaseProbeError(error)}). Verify writer SELECT/UPDATE on the target table and configured columns.`
8408
+ });
8409
+ }
8410
+ }
8411
+ return checks;
8412
+ }
8413
+ function directSqlProposalCapabilities(config, sourceName) {
8414
+ return (config.capabilities ?? []).filter((capability) => {
8415
+ if (capability.kind !== "proposal" || capability.source !== sourceName) return false;
8416
+ return (capability.executor ?? "sql_update") === "sql_update";
8417
+ });
8418
+ }
8419
+ async function rollbackOnlyTargetProbe(engine, databaseUrl, capability) {
8420
+ if (engine === "postgres") {
8421
+ await rollbackOnlyPostgresTargetProbe(databaseUrl, capability);
8422
+ return;
8423
+ }
8424
+ await rollbackOnlyMysqlTargetProbe(databaseUrl, capability);
8425
+ }
8426
+ async function rollbackOnlyPostgresTargetProbe(databaseUrl, capability) {
8427
+ const pg = await dynamicImportModule("pg");
8428
+ const pool = new pg.Pool({ connectionString: databaseUrl });
8429
+ const client = await pool.connect();
8430
+ try {
8431
+ await client.query("BEGIN");
8432
+ try {
8433
+ const table = `${quotePostgresIdentifier2(capability.target.schema)}.${quotePostgresIdentifier2(capability.target.table)}`;
8434
+ const columns = proposalProbeColumns(capability).map(quotePostgresIdentifier2).join(", ");
8435
+ await client.query(`SELECT ${columns} FROM ${table} WHERE false FOR UPDATE`);
8436
+ for (const column of proposalUpdateProbeColumns(capability)) {
8437
+ const quoted = quotePostgresIdentifier2(column);
8438
+ await client.query(`UPDATE ${table} SET ${quoted} = ${quoted} WHERE false`);
8439
+ }
8440
+ await client.query("ROLLBACK");
8441
+ } catch (error) {
8442
+ await client.query("ROLLBACK").catch(() => void 0);
8443
+ throw error;
8444
+ }
8445
+ } finally {
8446
+ client.release();
8447
+ await pool.end();
8448
+ }
8449
+ }
8450
+ async function rollbackOnlyMysqlTargetProbe(databaseUrl, capability) {
8451
+ const mysql4 = await dynamicImportModule("mysql2/promise");
8452
+ const connection = await mysql4.createConnection({ uri: databaseUrl, dateStrings: true });
8453
+ try {
8454
+ await connection.beginTransaction();
8455
+ try {
8456
+ const table = `${quoteMysqlIdentifier2(capability.target.schema)}.${quoteMysqlIdentifier2(capability.target.table)}`;
8457
+ const columns = proposalProbeColumns(capability).map(quoteMysqlIdentifier2).join(", ");
8458
+ await connection.query(`SELECT ${columns} FROM ${table} WHERE 1 = 0 FOR UPDATE`);
8459
+ for (const column of proposalUpdateProbeColumns(capability)) {
8460
+ const quoted = quoteMysqlIdentifier2(column);
8461
+ await connection.query(`UPDATE ${table} SET ${quoted} = ${quoted} WHERE 1 = 0`);
8462
+ }
8463
+ await connection.rollback();
8464
+ } catch (error) {
8465
+ await connection.rollback().catch(() => void 0);
8466
+ throw error;
8467
+ }
8468
+ } finally {
8469
+ await connection.end();
8470
+ }
8471
+ }
8472
+ async function dynamicImportModule(specifier) {
8473
+ const importer = new Function("specifier", "return import(specifier)");
8474
+ return importer(specifier);
8475
+ }
8476
+ function proposalProbeColumns(capability) {
8477
+ const columns = /* @__PURE__ */ new Set();
8478
+ columns.add(capability.target.primary_key);
8479
+ if (capability.target.tenant_key) columns.add(capability.target.tenant_key);
8480
+ if (capability.conflict_guard?.column) columns.add(capability.conflict_guard.column);
8481
+ for (const column of capability.visible_columns ?? []) columns.add(column);
8482
+ for (const column of proposalUpdateProbeColumns(capability)) columns.add(column);
8483
+ return [...columns];
8484
+ }
8485
+ function proposalUpdateProbeColumns(capability) {
8486
+ const columns = /* @__PURE__ */ new Set();
8487
+ for (const column of capability.allowed_columns ?? []) columns.add(column);
8488
+ for (const column of Object.keys(capability.patch ?? {})) columns.add(column);
8489
+ return [...columns];
8490
+ }
8491
+ function quotePostgresIdentifier2(value) {
8492
+ return `"${value.replace(/"/g, '""')}"`;
8493
+ }
8494
+ function quoteMysqlIdentifier2(value) {
8495
+ return `\`${value.replace(/`/g, "``")}\``;
8496
+ }
8497
+ function safeDatabaseProbeError(error) {
8498
+ const raw = typeof error === "string" ? error : error instanceof Error ? error.message : JSON.stringify(error ?? {});
8499
+ const message = raw.toLowerCase();
8500
+ if (/permission|denied|not authorized|insufficient|42501|er_tableaccess_denied|er_dbaccess_denied/.test(message)) return "permission denied";
8501
+ if (/authentication|password|28p01|access denied for user|invalid authorization/.test(message)) return "authentication failed";
8502
+ if (/timeout|timed out|etimedout/.test(message)) return "timeout";
8503
+ if (/econnrefused|enotfound|eai_again|network|connection terminated|connection failed/.test(message)) return "connection failed";
8504
+ if (/does not exist|unknown database|no such table|undefined_table|er_no_such_table|42p01/.test(message)) return "configured object not found";
8505
+ return "database probe failed";
8506
+ }
8507
+ function receiptTableGuidance(engine) {
8508
+ if (engine === "postgres") {
8509
+ return `Pre-create the receipt table with "${cliCommandName()} writeback migration --engine postgres --schema synapsor" and grant it with "${cliCommandName()} writeback grants --engine postgres --schema synapsor --writer-role <writer_role>", or use an app-owned handler executor.`;
8510
+ }
8511
+ return `Pre-create the receipt table with "${cliCommandName()} writeback migration --engine mysql --schema <database_name>" and grant it with "${cliCommandName()} writeback grants --engine mysql --schema <database_name> --writer-role \\"'<writer>'@'%'\\"", or use an app-owned handler executor.`;
8512
+ }
7992
8513
  async function localDoctorStoreStats(storePath) {
7993
8514
  if (!storePath || storePath === ":memory:") return { path: storePath ?? "not configured", exists: storePath === ":memory:" };
7994
8515
  if (!await fileExists(storePath)) return { path: storePath, exists: false };
@@ -8174,6 +8695,9 @@ function executorConfig(config, executorName) {
8174
8695
  if (raw.type === "sql_update") return { type: "sql_update" };
8175
8696
  throw new Error(`executor ${executorName} has unsupported type`);
8176
8697
  }
8698
+ function signHandlerRequestBody(body, secret) {
8699
+ return `sha256=${crypto5.createHmac("sha256", secret).update(body).digest("hex")}`;
8700
+ }
8177
8701
  async function applyHttpHandlerProposal(input) {
8178
8702
  const duplicate = duplicateHandlerReceipt(input.store, input.proposalId);
8179
8703
  if (duplicate) return alreadyAppliedReceipt(duplicate.receipt, input.runnerId);
@@ -8198,6 +8722,21 @@ async function applyHttpHandlerProposal(input) {
8198
8722
  if (!token) throw new Error(`${input.executor.auth.token_env} is not set`);
8199
8723
  headers.authorization = `Bearer ${token}`;
8200
8724
  }
8725
+ const issuedAt = (/* @__PURE__ */ new Date()).toISOString();
8726
+ const requestBody = JSON.stringify({
8727
+ protocol_version: "1.0",
8728
+ ...prepared.request,
8729
+ issued_at: issuedAt,
8730
+ executor: input.executorName,
8731
+ dry_run: input.dryRun
8732
+ });
8733
+ headers["x-synapsor-issued-at"] = issuedAt;
8734
+ headers["x-synapsor-proposal-id"] = prepared.proposal.proposal_id;
8735
+ if (input.executor.signing_secret_env) {
8736
+ const signingSecret = input.env[input.executor.signing_secret_env];
8737
+ if (!signingSecret) throw new Error(`${input.executor.signing_secret_env} is not set`);
8738
+ headers["x-synapsor-signature"] = signHandlerRequestBody(requestBody, signingSecret);
8739
+ }
8201
8740
  const controller = new AbortController();
8202
8741
  const timeout = setTimeout(() => controller.abort(), Math.max(1, input.executor.timeout_ms ?? 5e3));
8203
8742
  let receipt;
@@ -8205,7 +8744,7 @@ async function applyHttpHandlerProposal(input) {
8205
8744
  const response = await fetch(url, {
8206
8745
  method: input.executor.method ?? "POST",
8207
8746
  headers,
8208
- body: JSON.stringify({ ...prepared.request, executor: input.executorName, dry_run: input.dryRun }),
8747
+ body: requestBody,
8209
8748
  signal: controller.signal
8210
8749
  });
8211
8750
  const text = await response.text();
@@ -8633,7 +9172,7 @@ async function cloudConnect(args) {
8633
9172
  return 1;
8634
9173
  }
8635
9174
  const runnerId = String(parsed.cloud.runner_id || process2.env.SYNAPSOR_RUNNER_ID || "synapsor_runner_local").trim();
8636
- const runnerVersion = String(parsed.cloud.runner_version || process2.env.npm_package_version || "0.1.0-alpha.10").trim();
9175
+ const runnerVersion = String(parsed.cloud.runner_version || process2.env.npm_package_version || "0.1.0-alpha.13").trim();
8637
9176
  const engines = normalizeEngines(parsed.cloud.engines);
8638
9177
  const capabilities = normalizeCapabilities(parsed.cloud.capabilities);
8639
9178
  const client = new ControlPlaneClient({
@@ -8687,6 +9226,7 @@ async function mcp(args) {
8687
9226
  if (subcommand === "serve-streamable-http") return mcpServeStreamableHttp(rest);
8688
9227
  if (subcommand === "audit") return mcpAudit(rest);
8689
9228
  if (subcommand === "config") return mcpConfig(rest);
9229
+ if (subcommand === "client-config") return mcpConfigure(rest);
8690
9230
  if (subcommand === "configure") return mcpConfigure(rest);
8691
9231
  if (subcommand === "smoke") return mcpSmoke(rest);
8692
9232
  usage(["mcp"]);
@@ -8695,6 +9235,7 @@ async function mcp(args) {
8695
9235
  async function tools(args) {
8696
9236
  const [subcommand, ...rest] = args;
8697
9237
  if (subcommand === "preview") return toolsPreview(rest);
9238
+ if (subcommand === "list") return toolsPreview(rest);
8698
9239
  usage(["tools"]);
8699
9240
  return 2;
8700
9241
  }
@@ -9345,44 +9886,74 @@ function quickDemoChangeSet() {
9345
9886
  };
9346
9887
  }
9347
9888
  async function mcpServe(args) {
9889
+ const transport = optionalArg(args, "--transport") ?? "stdio";
9890
+ if (transport === "streamable-http") return mcpServeStreamableHttp(args);
9891
+ if (transport === "http" || transport === "json-rpc-http" || transport === "jsonrpc-http") return mcpServeHttp(args);
9892
+ if (transport !== "stdio") throw new Error("--transport must be stdio, streamable-http, or http");
9348
9893
  const configPath = optionalArg(args, "--config") ?? process2.env.SYNAPSOR_MCP_CONFIG;
9349
9894
  const readOnly = args.includes("--read-only");
9350
9895
  const config = readOnly ? { ...await readRuntimeConfig(configPath ?? defaultConfigPath), mode: "read_only" } : void 0;
9351
- await serveStdio({
9352
- configPath,
9353
- storePath: optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE,
9354
- config
9355
- });
9356
- return 0;
9896
+ const toolNameStyle = toolNameStyleOption(args);
9897
+ const resultFormat = resultFormatOption(args);
9898
+ const storePath = optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE;
9899
+ const releaseLease = await writeStoreLease(storePath, "mcp", "stdio", args.includes("--allow-concurrent-store"));
9900
+ try {
9901
+ await serveStdio({
9902
+ configPath,
9903
+ storePath,
9904
+ config,
9905
+ toolNameStyle,
9906
+ resultFormat
9907
+ });
9908
+ return 0;
9909
+ } finally {
9910
+ await releaseLease();
9911
+ }
9357
9912
  }
9358
9913
  async function mcpServeHttp(args) {
9914
+ process2.stderr.write([
9915
+ "Warning: mcp serve-http is a legacy JSON-RPC bridge, not spec MCP Streamable HTTP.",
9916
+ `For OpenAI Agents SDK or standard HTTP MCP clients, use: ${cliCommandName()} mcp serve --transport streamable-http`,
9917
+ ""
9918
+ ].join("\n"));
9359
9919
  const configPath = optionalArg(args, "--config") ?? process2.env.SYNAPSOR_MCP_CONFIG;
9360
9920
  const readOnly = args.includes("--read-only");
9361
9921
  const config = readOnly ? { ...await readRuntimeConfig(configPath ?? defaultConfigPath), mode: "read_only" } : void 0;
9362
9922
  const host = optionalArg(args, "--host") ?? "127.0.0.1";
9363
9923
  const port = Number(optionalArg(args, "--port") ?? "8765");
9924
+ const resultFormat = resultFormatOption(args);
9364
9925
  if (!Number.isInteger(port) || port <= 0 || port > 65535) {
9365
9926
  throw new Error("--port must be an integer from 1 to 65535");
9366
9927
  }
9367
9928
  if (host === "0.0.0.0") {
9368
9929
  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");
9369
9930
  }
9370
- const server = await startHttpMcpServer({
9371
- configPath,
9372
- config,
9373
- storePath: optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE,
9374
- host,
9375
- port,
9376
- authTokenEnv: optionalArg(args, "--auth-token-env") ?? "SYNAPSOR_RUNNER_HTTP_TOKEN",
9377
- devNoAuth: args.includes("--dev-no-auth"),
9378
- corsOrigin: optionalArg(args, "--cors-origin")
9379
- });
9931
+ const storePath = optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE;
9932
+ const releaseLease = await writeStoreLease(storePath, "mcp", "legacy-jsonrpc", args.includes("--allow-concurrent-store"));
9933
+ let server;
9934
+ try {
9935
+ server = await startHttpMcpServer({
9936
+ configPath,
9937
+ config,
9938
+ storePath,
9939
+ host,
9940
+ port,
9941
+ authTokenEnv: optionalArg(args, "--auth-token-env") ?? "SYNAPSOR_RUNNER_HTTP_TOKEN",
9942
+ devNoAuth: args.includes("--dev-no-auth"),
9943
+ corsOrigin: optionalArg(args, "--cors-origin"),
9944
+ resultFormat
9945
+ });
9946
+ } catch (error) {
9947
+ await releaseLease();
9948
+ throw error;
9949
+ }
9380
9950
  process2.stderr.write("Press Ctrl+C to stop.\n");
9381
9951
  await new Promise((resolve) => {
9382
9952
  const stop = async () => {
9383
9953
  process2.off("SIGINT", stop);
9384
9954
  process2.off("SIGTERM", stop);
9385
9955
  await server.close();
9956
+ await releaseLease();
9386
9957
  resolve();
9387
9958
  };
9388
9959
  process2.once("SIGINT", stop);
@@ -9394,6 +9965,8 @@ async function mcpServeStreamableHttp(args) {
9394
9965
  const configPath = optionalArg(args, "--config") ?? process2.env.SYNAPSOR_MCP_CONFIG;
9395
9966
  const readOnly = args.includes("--read-only");
9396
9967
  const config = readOnly ? { ...await readRuntimeConfig(configPath ?? defaultConfigPath), mode: "read_only" } : void 0;
9968
+ const toolNameStyle = toolNameStyleOption(args);
9969
+ const resultFormat = resultFormatOption(args);
9397
9970
  const host = optionalArg(args, "--host") ?? "127.0.0.1";
9398
9971
  const port = Number(optionalArg(args, "--port") ?? "8766");
9399
9972
  if (!Number.isInteger(port) || port <= 0 || port > 65535) {
@@ -9402,22 +9975,33 @@ async function mcpServeStreamableHttp(args) {
9402
9975
  if (host === "0.0.0.0") {
9403
9976
  process2.stderr.write("Warning: binding Synapsor Runner Streamable HTTP MCP to 0.0.0.0 exposes model-facing tools on the network. Use TLS, private networking, authentication, and rate limits.\n");
9404
9977
  }
9405
- const server = await startStreamableHttpMcpServer({
9406
- configPath,
9407
- config,
9408
- storePath: optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE,
9409
- host,
9410
- port,
9411
- authTokenEnv: optionalArg(args, "--auth-token-env") ?? "SYNAPSOR_RUNNER_HTTP_TOKEN",
9412
- devNoAuth: args.includes("--dev-no-auth"),
9413
- corsOrigin: optionalArg(args, "--cors-origin")
9414
- });
9978
+ const storePath = optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE;
9979
+ const releaseLease = await writeStoreLease(storePath, "mcp", "streamable-http", args.includes("--allow-concurrent-store"));
9980
+ let server;
9981
+ try {
9982
+ server = await startStreamableHttpMcpServer({
9983
+ configPath,
9984
+ config,
9985
+ storePath,
9986
+ host,
9987
+ port,
9988
+ toolNameStyle,
9989
+ authTokenEnv: optionalArg(args, "--auth-token-env") ?? "SYNAPSOR_RUNNER_HTTP_TOKEN",
9990
+ devNoAuth: args.includes("--dev-no-auth"),
9991
+ corsOrigin: optionalArg(args, "--cors-origin"),
9992
+ resultFormat
9993
+ });
9994
+ } catch (error) {
9995
+ await releaseLease();
9996
+ throw error;
9997
+ }
9415
9998
  process2.stderr.write("Press Ctrl+C to stop.\n");
9416
9999
  await new Promise((resolve) => {
9417
10000
  const stop = async () => {
9418
10001
  process2.off("SIGINT", stop);
9419
10002
  process2.off("SIGTERM", stop);
9420
10003
  await server.close();
10004
+ await releaseLease();
9421
10005
  resolve();
9422
10006
  };
9423
10007
  process2.once("SIGINT", stop);
@@ -9425,6 +10009,101 @@ async function mcpServeStreamableHttp(args) {
9425
10009
  });
9426
10010
  return 0;
9427
10011
  }
10012
+ async function writeStoreLease(storePath, mode, transport, allowConcurrent) {
10013
+ const resolved = resolveStorePathForLease(storePath);
10014
+ if (!resolved) return async () => void 0;
10015
+ await assertNoActiveStoreLease(resolved, allowConcurrent, "serve");
10016
+ const leasePath = storeLeasePath(resolved);
10017
+ const lease = {
10018
+ pid: process2.pid,
10019
+ mode,
10020
+ transport,
10021
+ store_path: resolved,
10022
+ started_at: (/* @__PURE__ */ new Date()).toISOString()
10023
+ };
10024
+ await fs3.mkdir(path3.dirname(resolved), { recursive: true });
10025
+ await fs3.writeFile(leasePath, `${JSON.stringify(lease, null, 2)}
10026
+ `, "utf8");
10027
+ return async () => {
10028
+ const current = await readStoreLease(resolved);
10029
+ if (current?.pid === process2.pid && current.transport === transport) {
10030
+ await fs3.rm(leasePath, { force: true });
10031
+ }
10032
+ };
10033
+ }
10034
+ async function assertNoActiveStoreLease(storePath, force, operation) {
10035
+ const resolved = resolveStorePathForLease(storePath);
10036
+ if (!resolved) return;
10037
+ const lease = await readStoreLease(resolved);
10038
+ if (!lease) return;
10039
+ if (!pidIsActive(lease.pid)) {
10040
+ await fs3.rm(storeLeasePath(resolved), { force: true });
10041
+ return;
10042
+ }
10043
+ const message = `Local store appears active for ${lease.mode}/${lease.transport} (pid ${lease.pid}, started ${lease.started_at}). Refusing ${operation}. Stop the server or rerun with --force if you have verified it is safe.`;
10044
+ if (!force) throw new Error(message);
10045
+ process2.stderr.write(`Warning: ${message}
10046
+ `);
10047
+ }
10048
+ function resolveStorePathForLease(storePath) {
10049
+ const value = storePath ?? process2.env.SYNAPSOR_LOCAL_STORE ?? "./.synapsor/local.db";
10050
+ if (value === ":memory:") return void 0;
10051
+ return path3.resolve(value);
10052
+ }
10053
+ function storeLeasePath(resolvedStorePath) {
10054
+ return `${resolvedStorePath}.lease.json`;
10055
+ }
10056
+ async function readStoreLease(storePath) {
10057
+ const resolved = resolveStorePathForLease(storePath);
10058
+ if (!resolved) return void 0;
10059
+ try {
10060
+ const parsed = JSON.parse(await fs3.readFile(storeLeasePath(resolved), "utf8"));
10061
+ if (typeof parsed.pid !== "number" || typeof parsed.mode !== "string" || typeof parsed.transport !== "string" || typeof parsed.started_at !== "string") {
10062
+ return void 0;
10063
+ }
10064
+ return {
10065
+ pid: parsed.pid,
10066
+ mode: parsed.mode,
10067
+ transport: parsed.transport,
10068
+ store_path: typeof parsed.store_path === "string" ? parsed.store_path : resolved,
10069
+ started_at: parsed.started_at
10070
+ };
10071
+ } catch (error) {
10072
+ if (error.code === "ENOENT") return void 0;
10073
+ return void 0;
10074
+ }
10075
+ }
10076
+ function pidIsActive(pid) {
10077
+ if (!Number.isInteger(pid) || pid <= 0) return false;
10078
+ try {
10079
+ process2.kill(pid, 0);
10080
+ return true;
10081
+ } catch (error) {
10082
+ return error.code === "EPERM";
10083
+ }
10084
+ }
10085
+ function toolNameStyleOption(args) {
10086
+ const requestedStyle = optionalArg(args, "--tool-name-style");
10087
+ const requestedAliasMode = optionalArg(args, "--alias-mode");
10088
+ if (requestedStyle && requestedAliasMode && requestedStyle !== requestedAliasMode) {
10089
+ throw new Error("--tool-name-style and --alias-mode must match when both are provided");
10090
+ }
10091
+ const requested = requestedAliasMode ?? requestedStyle;
10092
+ if (args.includes("--openai-tool-aliases")) {
10093
+ if (requested && requested !== "openai") throw new Error("--openai-tool-aliases cannot be combined with a non-openai alias mode");
10094
+ return "openai";
10095
+ }
10096
+ if (!requested) return "canonical";
10097
+ if (requested === "canonical" || requested === "openai" || requested === "both") return requested;
10098
+ throw new Error("--alias-mode must be canonical, openai, or both");
10099
+ }
10100
+ function resultFormatOption(args) {
10101
+ const requested = optionalArg(args, "--result-format");
10102
+ if (!requested) return void 0;
10103
+ if (requested === "1" || requested === "v1") return 1;
10104
+ if (requested === "2" || requested === "v2") return 2;
10105
+ throw new Error("--result-format must be v1, 1, v2, or 2");
10106
+ }
9428
10107
  async function mcpAudit(args) {
9429
10108
  const format = optionalArg(args, "--format") ?? (args.includes("--json") ? "json" : "text");
9430
10109
  if (!["text", "json", "markdown"].includes(format)) {
@@ -9567,21 +10246,33 @@ function formatProposeResult(capabilityName, result, storePath) {
9567
10246
  `;
9568
10247
  }
9569
10248
  async function mcpConfigure(args) {
9570
- const client = optionalArg(args, "--client");
9571
- if (!client) throw new Error("mcp configure requires --client generic-stdio|claude-desktop|cursor|vscode");
10249
+ const client = normalizeMcpClientName(optionalArg(args, "--client"));
10250
+ if (!client) throw new Error("mcp configure requires --client generic-stdio|claude|claude-desktop|cursor|vscode|openai-agents");
9572
10251
  const useAbsolutePaths = args.includes("--absolute-paths");
9573
10252
  const rawConfigPath = optionalArg(args, "--config") ?? "./synapsor.runner.json";
9574
10253
  const rawStorePath = optionalArg(args, "--store") ?? "./.synapsor/local.db";
9575
10254
  const configPath = useAbsolutePaths ? path3.resolve(rawConfigPath) : rawConfigPath;
9576
10255
  const storePath = useAbsolutePaths ? path3.resolve(rawStorePath) : rawStorePath;
10256
+ const transport = mcpClientConfigTransport(args, client);
10257
+ const aliasMode = mcpClientConfigAliasMode(args, client);
10258
+ const includeInstructions = args.includes("--include-instructions");
10259
+ const host = optionalArg(args, "--host") ?? "127.0.0.1";
10260
+ const port = Number(optionalArg(args, "--port") ?? "8766");
10261
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
10262
+ throw new Error("--port must be an integer from 1 to 65535");
10263
+ }
10264
+ const authTokenEnv = optionalArg(args, "--auth-token-env") ?? "SYNAPSOR_RUNNER_HTTP_TOKEN";
9577
10265
  if (!await fileExists(rawConfigPath)) {
9578
10266
  process2.stderr.write(`Warning: config path does not exist yet: ${rawConfigPath}
9579
10267
  `);
9580
10268
  }
9581
- if (!path3.isAbsolute(configPath) || !path3.isAbsolute(storePath)) {
10269
+ if (transport === "stdio" && (!path3.isAbsolute(configPath) || !path3.isAbsolute(storePath))) {
9582
10270
  process2.stderr.write("Warning: relative paths are resolved by the MCP client working directory. Use --absolute-paths if the client runs from another directory.\n");
9583
10271
  }
9584
- const snippet = mcpClientSnippet(client, configPath, storePath);
10272
+ const snippet = mcpClientSnippet(client, configPath, storePath, { transport, aliasMode, host, port, authTokenEnv });
10273
+ if (includeInstructions) {
10274
+ snippet.agent_instructions = mcpAgentInstructions(client, aliasMode);
10275
+ }
9585
10276
  if (args.includes("--write")) {
9586
10277
  const destination = optionalArg(args, "--destination");
9587
10278
  if (!destination) throw new Error("mcp configure --write requires --destination <path>");
@@ -9599,20 +10290,124 @@ async function mcpConfigure(args) {
9599
10290
  async function mcpConfig(args) {
9600
10291
  const [client, ...rest] = args;
9601
10292
  if (!client || client.startsWith("--")) return mcpConfigure(["--client", "claude-desktop", ...args]);
9602
- return mcpConfigure(["--client", client, ...rest]);
10293
+ return mcpConfigure(["--client", normalizeMcpClientName(client) ?? client, ...rest]);
10294
+ }
10295
+ function normalizeMcpClientName(client) {
10296
+ if (client === "claude") return "claude-desktop";
10297
+ return client;
10298
+ }
10299
+ function mcpClientConfigTransport(args, client) {
10300
+ const requested = optionalArg(args, "--transport") ?? (client === "openai-agents" ? "streamable-http" : "stdio");
10301
+ if (requested === "stdio" || requested === "streamable-http") return requested;
10302
+ if (requested === "http" || requested === "json-rpc-http" || requested === "jsonrpc-http") {
10303
+ throw new Error("mcp config uses stdio or streamable-http. The lightweight JSON-RPC HTTP bridge is not a standard MCP client transport.");
10304
+ }
10305
+ throw new Error("--transport must be stdio or streamable-http");
10306
+ }
10307
+ function mcpClientConfigAliasMode(args, client) {
10308
+ const requested = optionalArg(args, "--alias-mode");
10309
+ const aliasMode = requested ?? (args.includes("--openai-tool-aliases") ? "openai" : client === "openai-agents" ? "openai" : "canonical");
10310
+ if (aliasMode === "canonical" || aliasMode === "openai" || aliasMode === "both") return aliasMode;
10311
+ throw new Error("--alias-mode must be canonical, openai, or both");
9603
10312
  }
9604
- function mcpClientSnippet(client, configPath, storePath) {
10313
+ function serveArgsForClient(configPath, storePath, options) {
10314
+ const args = options.transport === "streamable-http" ? [
10315
+ "mcp",
10316
+ "serve-streamable-http",
10317
+ "--config",
10318
+ configPath,
10319
+ "--store",
10320
+ storePath,
10321
+ "--host",
10322
+ options.host,
10323
+ "--port",
10324
+ String(options.port),
10325
+ "--auth-token-env",
10326
+ options.authTokenEnv
10327
+ ] : ["mcp", "serve", "--config", configPath, "--store", storePath];
10328
+ if (options.aliasMode !== "canonical") args.push("--alias-mode", options.aliasMode);
10329
+ return args;
10330
+ }
10331
+ function mcpClientSnippet(client, configPath, storePath, options) {
9605
10332
  const command = cliCommandName();
9606
- const args = ["mcp", "serve", "--config", configPath, "--store", storePath];
10333
+ const args = serveArgsForClient(configPath, storePath, options);
9607
10334
  if (client === "generic" || client === "generic-stdio") return { command, args };
9608
10335
  if (client === "claude-desktop" || client === "cursor") {
10336
+ if (options.transport !== "stdio") throw new Error(`${client} config output currently supports stdio. Use --transport stdio.`);
9609
10337
  return { mcpServers: { synapsor: { command, args } } };
9610
10338
  }
9611
10339
  if (client === "vscode") {
10340
+ if (options.transport !== "stdio") throw new Error("vscode config output currently supports stdio. Use --transport stdio.");
9612
10341
  return { servers: { synapsor: { type: "stdio", command, args } } };
9613
10342
  }
10343
+ if (client === "openai-agents") {
10344
+ if (options.transport !== "streamable-http") throw new Error("openai-agents config output uses Streamable HTTP. Use --transport streamable-http.");
10345
+ const url = `http://${options.host}:${options.port}/mcp`;
10346
+ return {
10347
+ transport: "streamable-http",
10348
+ start_server: {
10349
+ command,
10350
+ args,
10351
+ env: {
10352
+ [options.authTokenEnv]: "<set-a-random-local-token>"
10353
+ }
10354
+ },
10355
+ openai_agents_sdk: {
10356
+ package: "openai-agents",
10357
+ url,
10358
+ headers_from_env: {
10359
+ Authorization: `Bearer $${options.authTokenEnv}`
10360
+ },
10361
+ python: [
10362
+ "import os",
10363
+ "from agents.mcp import MCPServerStreamableHttp",
10364
+ "",
10365
+ "synapsor_mcp = MCPServerStreamableHttp(",
10366
+ ` params={`,
10367
+ ` "url": "${url}",`,
10368
+ ` "headers": {"Authorization": f"Bearer {os.environ['${options.authTokenEnv}']}"},`,
10369
+ " }",
10370
+ ")"
10371
+ ].join("\n")
10372
+ },
10373
+ tool_names: {
10374
+ canonical: "billing.inspect_invoice",
10375
+ model_visible_with_alias_mode_openai: "billing__inspect_invoice",
10376
+ alias_mode: options.aliasMode
10377
+ },
10378
+ notes: [
10379
+ "Start the local Streamable HTTP MCP server before creating the OpenAI Agents SDK server.",
10380
+ "OpenAI-facing configs should use --alias-mode openai because OpenAI function names cannot contain dots.",
10381
+ "Runner maps aliases back to canonical Synapsor capability names and includes the canonical name in MCP tool metadata.",
10382
+ "This config contains no database URLs, write credentials, API keys, or bearer token values."
10383
+ ]
10384
+ };
10385
+ }
9614
10386
  throw new Error(`unsupported MCP client: ${client}`);
9615
10387
  }
10388
+ function mcpAgentInstructions(client, aliasMode) {
10389
+ const toolNameNote = aliasMode === "openai" ? "OpenAI-facing tool names may use aliases such as billing__inspect_invoice. Treat the canonical Synapsor capability name in tool metadata/results as the audit name." : "Use the model-visible Synapsor tool names exactly as listed by the MCP client.";
10390
+ return {
10391
+ target_client: client,
10392
+ alias_mode: aliasMode,
10393
+ recommended_system_prompt: [
10394
+ "Use Synapsor Runner tools in a propose-first pattern.",
10395
+ "Inspect relevant records, policy rows, and other evidence before proposing a change.",
10396
+ "Do not claim a database change was committed unless a result says source_database_changed: true.",
10397
+ "Proposal tools create reviewable proposals only; they do not commit writes.",
10398
+ "You cannot approve, apply, commit, or write back through model-facing MCP tools.",
10399
+ "On VERSION_CONFLICT, re-inspect the record before proposing again.",
10400
+ "Evidence handles are audit/replay handles; you do not need to call them during the turn.",
10401
+ toolNameNote
10402
+ ].join(" "),
10403
+ checklist: [
10404
+ "Inspect evidence before proposing.",
10405
+ "Use trusted session scope; never ask the user/model for tenant or principal values.",
10406
+ "Report proposal ids and source_database_changed exactly from the tool result.",
10407
+ "If ok is false, follow error.code. On TEMPORARILY_UNAVAILABLE, retry later. On NOT_FOUND_IN_TENANT, do not infer cross-tenant existence."
10408
+ ]
10409
+ };
10410
+ }
9616
10411
  async function mcpSmoke(args) {
9617
10412
  const boundary = await inspectMcpToolBoundary(args);
9618
10413
  if (args.includes("--json")) {
@@ -9659,7 +10454,9 @@ async function toolsPreview(args) {
9659
10454
  ok: boundary.ok,
9660
10455
  config_path: boundary.configPath,
9661
10456
  store_path: boundary.storePath,
10457
+ alias_mode: boundary.aliasMode,
9662
10458
  exposed_to_mcp: boundary.names,
10459
+ alias_mappings: boundary.exposures,
9663
10460
  not_exposed_to_mcp: defaultBlockedToolSurface(),
9664
10461
  checks: boundary.checks
9665
10462
  }, null, 2)}
@@ -9672,6 +10469,7 @@ async function toolsPreview(args) {
9672
10469
  async function inspectMcpToolBoundary(args) {
9673
10470
  const configPath = optionalArg(args, "--config") ?? "./synapsor.runner.json";
9674
10471
  const storePath = optionalArg(args, "--store") ?? "./.synapsor/local.db";
10472
+ const aliasMode = args.includes("--aliases") && !optionalArg(args, "--alias-mode") && !optionalArg(args, "--tool-name-style") ? "both" : toolNameStyleOption(args);
9675
10473
  if (!await fileExists(configPath)) {
9676
10474
  throw new Error(`MCP tool preview could not find ${configPath}.
9677
10475
 
@@ -9685,7 +10483,8 @@ Run ${cliCommandName()} onboard db --from-env DATABASE_URL, or pass --config <pa
9685
10483
  const runtime = createMcpRuntime(parsed, { storePath });
9686
10484
  try {
9687
10485
  const tools2 = runtime.listTools();
9688
- const names = tools2.map((tool) => tool.name);
10486
+ const exposures = toolNameExposures(tools2.map((tool) => tool.name), aliasMode);
10487
+ const names = exposures.map((item) => item.exposedName);
9689
10488
  const serialized = JSON.stringify(tools2);
9690
10489
  const checks = [
9691
10490
  { name: "semantic tools present", ok: names.length > 0, detail: names.join(", ") || "none" },
@@ -9696,7 +10495,7 @@ Run ${cliCommandName()} onboard db --from-env DATABASE_URL, or pass --config <pa
9696
10495
  { name: "write credentials absent", ok: !/(password|secret|bearer|private[_-]?key|token)/i.test(serialized), detail: "MCP tools do not include write credentials" }
9697
10496
  ];
9698
10497
  const ok = checks.every((check) => check.ok);
9699
- return { ok, configPath, storePath, names, checks };
10498
+ return { ok, configPath, storePath, aliasMode, names, exposures, checks };
9700
10499
  } finally {
9701
10500
  runtime.close();
9702
10501
  }
@@ -9713,13 +10512,15 @@ function defaultBlockedToolSurface() {
9713
10512
  ];
9714
10513
  }
9715
10514
  function formatToolsPreview(input) {
10515
+ const exposedLines = input.exposures.length > 0 ? input.exposures.map((item) => item.isAlias ? ` - ${item.exposedName} -> ${item.canonicalName}` : ` - ${item.exposedName}`) : [" - (none)"];
9716
10516
  const lines = [
9717
10517
  `Synapsor tools preview: ${input.ok ? "ok" : "failed"}`,
9718
10518
  `Config: ${input.configPath}`,
9719
10519
  `Store: ${input.storePath}`,
10520
+ `Alias mode: ${input.aliasMode}`,
9720
10521
  "",
9721
10522
  "Exposed to MCP:",
9722
- ...input.names.length > 0 ? input.names.map((name) => ` - ${name}`) : [" - (none)"],
10523
+ ...exposedLines,
9723
10524
  "",
9724
10525
  "Not exposed to MCP:",
9725
10526
  ...defaultBlockedToolSurface().map((name) => ` - ${name}`),
@@ -10322,11 +11123,18 @@ async function activity(args) {
10322
11123
  usage(["activity"]);
10323
11124
  return 2;
10324
11125
  }
11126
+ async function events(args) {
11127
+ const [subcommand, ...rest] = args;
11128
+ if (subcommand === "tail") return eventsTail(rest);
11129
+ usage(["events"]);
11130
+ return 2;
11131
+ }
10325
11132
  async function storeCommand(args) {
10326
11133
  const [subcommand, ...rest] = args;
10327
11134
  if (subcommand === "stats") return storeStats(rest);
10328
11135
  if (subcommand === "vacuum") return storeVacuum(rest);
10329
11136
  if (subcommand === "prune") return storePrune(rest);
11137
+ if (subcommand === "reset") return storeReset(rest);
10330
11138
  usage(["store"]);
10331
11139
  return 2;
10332
11140
  }
@@ -10842,6 +11650,56 @@ async function activitySearch(args) {
10842
11650
  store.close();
10843
11651
  }
10844
11652
  }
11653
+ async function eventsTail(args) {
11654
+ assertKnownOptions(args, eventTailAllowedOptions, "events tail");
11655
+ const follow = args.includes("--follow");
11656
+ if (follow && args.includes("--json")) throw new Error("events tail --follow does not support --json yet");
11657
+ const storePath = optionalArg(args, "--store");
11658
+ const intervalMs = Number(optionalArg(args, "--interval-ms") ?? "1000");
11659
+ if (!Number.isFinite(intervalMs) || intervalMs < 250) throw new Error("--interval-ms must be at least 250");
11660
+ const filters = eventFiltersFromArgs(args);
11661
+ const printOnce = async (seen2) => {
11662
+ const store = await openLocalStore(["--store", storePath ?? "./.synapsor/local.db"]);
11663
+ try {
11664
+ const rows = store.listEvents(filters).sort((left, right) => left.event_id - right.event_id).filter((event) => !seen2?.has(event.event_id));
11665
+ if (seen2) rows.forEach((event) => seen2.add(event.event_id));
11666
+ if (args.includes("--json")) {
11667
+ process2.stdout.write(`${JSON.stringify({ events: rows }, null, 2)}
11668
+ `);
11669
+ } else if (rows.length === 0 && !follow) {
11670
+ process2.stdout.write("No local events found.\n");
11671
+ } else {
11672
+ for (const event of rows) process2.stdout.write(formatEventLine(event, showDetails(args)));
11673
+ }
11674
+ return rows.length;
11675
+ } finally {
11676
+ store.close();
11677
+ }
11678
+ };
11679
+ if (!follow) {
11680
+ await printOnce();
11681
+ return 0;
11682
+ }
11683
+ const seen = /* @__PURE__ */ new Set();
11684
+ await printOnce(seen);
11685
+ await new Promise((resolve) => {
11686
+ const timer = setInterval(() => {
11687
+ void printOnce(seen).catch((error) => {
11688
+ process2.stderr.write(`events tail error: ${safeErrorMessage(error)}
11689
+ `);
11690
+ });
11691
+ }, intervalMs);
11692
+ const stop = () => {
11693
+ clearInterval(timer);
11694
+ process2.off("SIGINT", stop);
11695
+ process2.off("SIGTERM", stop);
11696
+ resolve();
11697
+ };
11698
+ process2.once("SIGINT", stop);
11699
+ process2.once("SIGTERM", stop);
11700
+ });
11701
+ return 0;
11702
+ }
10845
11703
  async function storeStats(args) {
10846
11704
  assertKnownOptions(args, storeStatsAllowedOptions, "store stats");
10847
11705
  const store = await openLocalStore(args);
@@ -10879,6 +11737,7 @@ async function storePrune(args) {
10879
11737
  if (args.includes("--yes") && args.includes("--dry-run")) throw new Error("store prune accepts either --dry-run or --yes, not both");
10880
11738
  const cutoff = cutoffFromOlderThan(olderThan);
10881
11739
  const dryRun = !args.includes("--yes");
11740
+ if (!dryRun) await assertNoActiveStoreLease(optionalArg(args, "--store"), args.includes("--force"), "store prune");
10882
11741
  const store = await openLocalStore(args);
10883
11742
  try {
10884
11743
  const result = store.pruneBefore(cutoff, { dryRun });
@@ -10890,6 +11749,36 @@ async function storePrune(args) {
10890
11749
  store.close();
10891
11750
  }
10892
11751
  }
11752
+ async function storeReset(args) {
11753
+ assertKnownOptions(args, storeResetAllowedOptions, "store reset");
11754
+ const storePath = optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE ?? "./.synapsor/local.db";
11755
+ if (storePath === ":memory:") throw new Error("store reset does not apply to :memory: stores");
11756
+ if (!args.includes("--yes")) {
11757
+ throw new Error("store reset is destructive for the local ledger. Rerun with --yes after backing up anything you need.");
11758
+ }
11759
+ await assertNoActiveStoreLease(storePath, args.includes("--force"), "store reset");
11760
+ const resolved = path3.resolve(storePath);
11761
+ const candidates = [resolved, `${resolved}-wal`, `${resolved}-shm`, storeLeasePath(resolved)];
11762
+ const removed = [];
11763
+ for (const candidate of candidates) {
11764
+ try {
11765
+ await fs3.rm(candidate, { force: true });
11766
+ removed.push(candidate);
11767
+ } catch (error) {
11768
+ if (error.code !== "ENOENT") throw error;
11769
+ }
11770
+ }
11771
+ const result = {
11772
+ ok: true,
11773
+ store: resolved,
11774
+ removed,
11775
+ source_database_changed: false
11776
+ };
11777
+ if (args.includes("--json")) process2.stdout.write(`${JSON.stringify(result, null, 2)}
11778
+ `);
11779
+ else process2.stdout.write(formatStoreReset(result));
11780
+ return 0;
11781
+ }
10893
11782
  var commonReadOptions = /* @__PURE__ */ new Set(["--store", "--json", "--details", "--debug"]);
10894
11783
  var showAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions]);
10895
11784
  var exportAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions, "--output", "--out", "--format", "--evidence", "--audit"]);
@@ -10949,6 +11838,17 @@ var receiptListAllowedOptions = /* @__PURE__ */ new Set([
10949
11838
  "--to",
10950
11839
  "--limit"
10951
11840
  ]);
11841
+ var eventTailAllowedOptions = /* @__PURE__ */ new Set([
11842
+ ...commonReadOptions,
11843
+ "--proposal",
11844
+ "--kind",
11845
+ "--actor",
11846
+ "--from",
11847
+ "--to",
11848
+ "--limit",
11849
+ "--follow",
11850
+ "--interval-ms"
11851
+ ]);
10952
11852
  var replayShowAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions, "--proposal", "--replay", "--evidence"]);
10953
11853
  var replayExportAllowedOptions = /* @__PURE__ */ new Set([...replayShowAllowedOptions, "--output", "--out", "--format"]);
10954
11854
  var replayListAllowedOptions = /* @__PURE__ */ new Set([
@@ -10991,7 +11891,8 @@ var activitySearchAllowedOptions = /* @__PURE__ */ new Set([
10991
11891
  ]);
10992
11892
  var storeStatsAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions]);
10993
11893
  var storeVacuumAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions]);
10994
- var storePruneAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions, "--older-than", "--dry-run", "--yes"]);
11894
+ var storePruneAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions, "--older-than", "--dry-run", "--yes", "--force"]);
11895
+ var storeResetAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions, "--yes", "--force"]);
10995
11896
  function assertKnownOptions(args, allowed, commandName) {
10996
11897
  for (const arg of args) {
10997
11898
  if (!arg.startsWith("--")) continue;
@@ -11140,6 +12041,16 @@ function receiptFiltersFromActivityArgs(args, store) {
11140
12041
  limit: limitFromArgs(args)
11141
12042
  };
11142
12043
  }
12044
+ function eventFiltersFromArgs(args) {
12045
+ return {
12046
+ proposal: optionalArg(args, "--proposal"),
12047
+ kind: optionalArg(args, "--kind"),
12048
+ actor: optionalArg(args, "--actor"),
12049
+ from: optionalArg(args, "--from"),
12050
+ to: optionalArg(args, "--to"),
12051
+ limit: limitFromArgs(args)
12052
+ };
12053
+ }
11143
12054
  function linkedProposalFilter(args, store, options = {}) {
11144
12055
  const noLinkedProposal = "__synapsor_no_linked_proposal__";
11145
12056
  const replay2 = optionalArg(args, "--replay");
@@ -11397,7 +12308,7 @@ function databaseInputFromArgs(args) {
11397
12308
  if (inlineUrl && !isDatabaseUrl(inlineUrl)) {
11398
12309
  throw new Error("--from must be a postgres://, postgresql://, or mysql:// URL.");
11399
12310
  }
11400
- const fromEnv = optionalArg(args, "--from-env") ?? optionalArg(args, "--database-url-env");
12311
+ const fromEnv = optionalArg(args, "--from-env") ?? optionalArg(args, "--url-env") ?? optionalArg(args, "--database-url-env");
11401
12312
  const configDatabaseUrlEnv = fromEnv ?? "SYNAPSOR_DATABASE_READ_URL";
11402
12313
  if (inlineUrl) {
11403
12314
  return {
@@ -11443,6 +12354,7 @@ function firstPositional(args) {
11443
12354
  "--format",
11444
12355
  "--from",
11445
12356
  "--from-env",
12357
+ "--url-env",
11446
12358
  "--host",
11447
12359
  "--idempotency-key",
11448
12360
  "--input",
@@ -11633,11 +12545,11 @@ function formatProposalDetail(proposal, storedEvidenceItemCount) {
11633
12545
  ...formatChangeLines(proposal)
11634
12546
  ].join("\n") + "\n";
11635
12547
  }
11636
- function formatProposalEventDetail(events) {
11637
- if (events.length === 0) return "Events:\n none\n";
12548
+ function formatProposalEventDetail(events2) {
12549
+ if (events2.length === 0) return "Events:\n none\n";
11638
12550
  return [
11639
12551
  "Events:",
11640
- ...events.map((event) => ` event ${event.event_id}: ${event.kind} by ${event.actor} at ${event.created_at}`)
12552
+ ...events2.map((event) => ` event ${event.event_id}: ${event.kind} by ${event.actor} at ${event.created_at}`)
11641
12553
  ].join("\n") + "\n";
11642
12554
  }
11643
12555
  function formatProposalDebug(proposal, storePath) {
@@ -12099,6 +13011,22 @@ function formatActivityNext(items, storeSuffix) {
12099
13011
  return `${lines.join("\n")}
12100
13012
  `;
12101
13013
  }
13014
+ function formatEventLine(event, details = false) {
13015
+ const lines = [
13016
+ `${event.created_at} ${event.kind}`,
13017
+ ` proposal: ${event.proposal_id}`,
13018
+ ` actor: ${event.actor}`
13019
+ ];
13020
+ if (details && Object.keys(event.payload).length > 0) {
13021
+ lines.push(` payload: ${JSON.stringify(event.payload)}`);
13022
+ }
13023
+ lines.push("");
13024
+ return `${lines.join("\n")}
13025
+ `;
13026
+ }
13027
+ function safeErrorMessage(error) {
13028
+ return error instanceof Error ? error.message : String(error);
13029
+ }
12102
13030
  function formatStoreStats(stats) {
12103
13031
  return [
12104
13032
  `Local store: ${stats.path}`,
@@ -12130,6 +13058,18 @@ function formatStorePrune(result) {
12130
13058
  return `${lines.join("\n")}
12131
13059
  `;
12132
13060
  }
13061
+ function formatStoreReset(result) {
13062
+ const lines = [
13063
+ "Local store reset complete",
13064
+ `Store: ${result.store}`,
13065
+ `Source database changed: ${result.source_database_changed ? "yes" : "no"}`,
13066
+ "",
13067
+ "Removed:",
13068
+ ...result.removed.length ? result.removed.map((entry) => ` - ${entry}`) : [" - no local store files were present"]
13069
+ ];
13070
+ return `${lines.join("\n")}
13071
+ `;
13072
+ }
12133
13073
  function cutoffFromOlderThan(value) {
12134
13074
  const match = value.match(/^(\d+)([smhd])$/i);
12135
13075
  if (!match) throw new Error("--older-than must use a duration such as 30d, 12h, 90m, or 0d");
@@ -12371,7 +13311,7 @@ function starterCloudConfig() {
12371
13311
  base_url_env: "SYNAPSOR_CLOUD_BASE_URL",
12372
13312
  runner_token_env: "SYNAPSOR_RUNNER_TOKEN",
12373
13313
  runner_id: "synapsor_runner_local",
12374
- runner_version: "0.1.0-alpha.10",
13314
+ runner_version: "0.1.0-alpha.13",
12375
13315
  project_id: "token_scope",
12376
13316
  adapter_id: "mcp.your_adapter",
12377
13317
  source_id: "src_replace_me",
@@ -12423,6 +13363,7 @@ function isKnownTopLevelCommand(command) {
12423
13363
  "query-audit",
12424
13364
  "receipts",
12425
13365
  "activity",
13366
+ "events",
12426
13367
  "store",
12427
13368
  "shadow",
12428
13369
  "ui"
@@ -12451,6 +13392,7 @@ Commands:
12451
13392
  mcp Serve safe semantic tools over MCP
12452
13393
  onboard One-command own-database setup
12453
13394
  smoke Test generated tool calls before wiring an MCP client
13395
+ tools List model-facing MCP tools and aliases
12454
13396
  writeback Print direct SQL writeback receipt DDL, grants, and checks
12455
13397
  handler Create app-owned writeback handler templates
12456
13398
  propose Create a local evidence-backed proposal
@@ -12460,6 +13402,7 @@ Commands:
12460
13402
  query-audit Inspect local query audit records
12461
13403
  receipts Inspect guarded writeback receipts
12462
13404
  activity Search local evidence/replay ledger
13405
+ events Tail local proposal/writeback lifecycle events
12463
13406
  store Inspect and maintain the local SQLite ledger
12464
13407
  apply Apply an approved proposal with guarded writeback
12465
13408
  replay Show what happened
@@ -12472,6 +13415,7 @@ Examples:
12472
13415
  ${cmd} inspect --from-env DATABASE_URL
12473
13416
  ${cmd} init --wizard --from-env DATABASE_URL
12474
13417
  ${cmd} smoke call --config ./synapsor.runner.json --store ./.synapsor/local.db
13418
+ ${cmd} tools list --aliases --config ./synapsor.runner.json --store ./.synapsor/local.db
12475
13419
  ${cmd} handler template node-fastify --output ./synapsor-writeback-handler.mjs
12476
13420
  ${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db
12477
13421
  ${cmd} propose billing.propose_late_fee_waiver --sample
@@ -12479,7 +13423,7 @@ Examples:
12479
13423
  `,
12480
13424
  start: `Usage:
12481
13425
  ${cmd} start --from-env DATABASE_URL [--schema public] [--mode read_only|shadow|review]
12482
- ${cmd} start --from-env DATABASE_URL --mode review --writeback http_handler --handler-url-env APP_WRITEBACK_URL
13426
+ ${cmd} start --from-env DATABASE_URL --mode review --writeback http_handler --handler-url-env APP_WRITEBACK_URL [--handler-signing-secret-env APP_WRITEBACK_SIGNING_SECRET]
12483
13427
  ${cmd} start
12484
13428
 
12485
13429
  With --from-env, run the guided own-database setup: inspect schema, choose one
@@ -12492,43 +13436,63 @@ so it is not confused with first-run onboarding.
12492
13436
  `,
12493
13437
  inspect: `Usage:
12494
13438
  ${cmd} inspect --from-env DATABASE_URL [--engine auto|postgres|mysql] [--schema public] [--json]
13439
+ ${cmd} inspect --engine postgres --url-env DATABASE_URL
12495
13440
  ${cmd} inspect "<postgres-or-mysql-url>" [--engine auto|postgres|mysql] [--schema public] [--json]
12496
13441
 
12497
13442
  Inspect schema metadata without mutating the database or printing credentials.
12498
13443
  `,
12499
13444
  init: `Usage:
12500
13445
  ${cmd} init --wizard --from-env DATABASE_URL [--mode read_only|review|shadow] [--out synapsor.runner.json]
13446
+ ${cmd} init --engine postgres --url-env DATABASE_URL --mode review --table public.invoices
12501
13447
  ${cmd} init --inspection-json schema.json --table invoices --mode review --patch-from-arg waiver_reason=reason
12502
- ${cmd} init --inspection-json schema.json --table invoices --mode review --writeback http_handler --handler-url-env APP_WRITEBACK_URL
13448
+ ${cmd} init --inspection-json schema.json --table invoices --mode review --writeback http_handler --handler-url-env APP_WRITEBACK_URL [--handler-signing-secret-env APP_WRITEBACK_SIGNING_SECRET]
12503
13449
 
12504
13450
  Generate a reviewed Synapsor Runner contract. Defaults to read-only in the wizard.
12505
13451
  Review mode writeback choices: sql_update, http_handler, command_handler.
12506
13452
  `,
12507
13453
  mcp: `Usage:
12508
13454
  ${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db
13455
+ ${cmd} mcp serve --transport streamable-http --config ./synapsor.runner.json --store ./.synapsor/local.db --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
12509
13456
  ${cmd} mcp serve-streamable-http --config ./synapsor.runner.json --store ./.synapsor/local.db --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
12510
13457
  ${cmd} mcp serve-http --config ./synapsor.runner.json --store ./.synapsor/local.db --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
12511
13458
  ${cmd} mcp config --absolute-paths --config ./synapsor.runner.json --store ./.synapsor/local.db
13459
+ ${cmd} mcp client-config --client openai-agents --config ./synapsor.runner.json --store ./.synapsor/local.db
12512
13460
  ${cmd} mcp audit --example dangerous-db-mcp
12513
13461
  ${cmd} mcp audit ./tools-list.json
12514
13462
 
12515
13463
  Use stdio for local MCP clients that launch the runner. Use Streamable HTTP for standard HTTP MCP clients. Use serve-http only when you explicitly want the lightweight JSON-RPC bridge.
12516
13464
  MCP clients see semantic tools. They do not receive raw SQL, write credentials, approval tools, or commit tools.
13465
+ `,
13466
+ tools: `Usage:
13467
+ ${cmd} tools list --config ./synapsor.runner.json --store ./.synapsor/local.db
13468
+ ${cmd} tools list --aliases --config ./synapsor.runner.json --store ./.synapsor/local.db
13469
+ ${cmd} tools preview --config ./synapsor.runner.json --store ./.synapsor/local.db
13470
+
13471
+ List the model-facing MCP tools generated from a reviewed Runner config.
13472
+ Use --aliases to show canonical Synapsor names and OpenAI-safe aliases.
13473
+ This command never prints database URLs or write credentials.
12517
13474
  `,
12518
13475
  "mcp serve": `Usage:
12519
- ${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db [--read-only] [--local]
13476
+ ${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db [--transport stdio] [--read-only] [--local] [--alias-mode canonical|openai|both] [--result-format v1|v2]
13477
+ ${cmd} mcp serve --transport streamable-http --config ./synapsor.runner.json --store ./.synapsor/local.db --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN [--result-format v2]
12520
13478
 
12521
13479
  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.
13480
+ Use --alias-mode openai, or --openai-tool-aliases, for clients that reject dotted tool names. Use --alias-mode both to expose canonical and alias names.
13481
+ Use --result-format v2 to return one stable ok/summary/data/proposal/error envelope from every tool call.
12522
13482
  `,
12523
13483
  "mcp serve-streamable-http": `Usage:
12524
13484
  export SYNAPSOR_RUNNER_HTTP_TOKEN=...
12525
- ${cmd} mcp serve-streamable-http --config ./synapsor.runner.json --store ./.synapsor/local.db [--host 127.0.0.1] [--port 8766] [--auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN]
13485
+ ${cmd} mcp serve-streamable-http --config ./synapsor.runner.json --store ./.synapsor/local.db [--host 127.0.0.1] [--port 8766] [--auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN] [--alias-mode canonical|openai|both] [--result-format v1|v2]
12526
13486
 
12527
13487
  Start the spec-compatible MCP Streamable HTTP endpoint for clients and SDKs that support HTTP MCP.
12528
13488
  Bearer auth is required by default.
12529
13489
 
12530
13490
  Alpha scope:
12531
13491
  - Supports MCP initialize/session behavior through the official MCP Streamable HTTP transport.
13492
+ - Use --alias-mode openai, or --openai-tool-aliases, for clients that reject dotted tool names.
13493
+ - Use --alias-mode both to expose canonical names and aliases.
13494
+ - Use --result-format v2 for the stable ok/summary/data/proposal/error envelope.
13495
+ - OpenAI aliases expose names such as billing__inspect_invoice while preserving the canonical Synapsor name in _meta.
12532
13496
  - Use /mcp for the MCP endpoint and /healthz for service health.
12533
13497
  - Sessions are in-memory. Restarting the runner clears active HTTP MCP sessions.
12534
13498
 
@@ -12541,7 +13505,7 @@ Security:
12541
13505
  `,
12542
13506
  "mcp serve-http": `Usage:
12543
13507
  export SYNAPSOR_RUNNER_HTTP_TOKEN=...
12544
- ${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]
13508
+ ${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] [--result-format v1|v2]
12545
13509
 
12546
13510
  Start the lightweight HTTP JSON-RPC bridge for app/server deployments that want simple POST calls.
12547
13511
  Bearer auth is required by default.
@@ -12557,9 +13521,20 @@ Security:
12557
13521
  - Optional CORS: --cors-origin http://localhost:3000
12558
13522
  `,
12559
13523
  "mcp config": `Usage:
12560
- ${cmd} mcp config [claude-desktop|cursor|generic|vscode] [--absolute-paths] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
13524
+ ${cmd} mcp config [claude-desktop|cursor|generic|vscode|openai-agents] [--absolute-paths] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
13525
+ ${cmd} mcp client-config --client openai-agents [--transport streamable-http] [--port 8766] [--alias-mode openai] [--include-instructions] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
12561
13526
 
12562
13527
  Print MCP client configuration that references the local runner command, not database URLs. Defaults to claude-desktop.
13528
+ OpenAI Agents SDK output uses Streamable HTTP and OpenAI-safe aliases by default.
13529
+ `,
13530
+ "mcp client-config": `Usage:
13531
+ ${cmd} mcp client-config --client claude-desktop [--absolute-paths] [--include-instructions] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
13532
+ ${cmd} mcp client-config --client cursor [--absolute-paths] [--include-instructions] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
13533
+ ${cmd} mcp client-config --client openai-agents [--transport streamable-http] [--port 8766] [--alias-mode openai] [--include-instructions] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
13534
+
13535
+ Print MCP client configuration that references the local runner command, not database URLs.
13536
+ OpenAI Agents SDK output uses Streamable HTTP and OpenAI-safe aliases by default.
13537
+ Use --include-instructions to include the recommended propose-first agent prompt.
12563
13538
  `,
12564
13539
  smoke: `Usage:
12565
13540
  ${cmd} smoke call [capability-name] [--sample] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
@@ -12631,10 +13606,13 @@ Static MCP/database risk review only. This is not a security guarantee.
12631
13606
  doctor: `Usage:
12632
13607
  ${cmd} doctor --config synapsor.runner.json
12633
13608
  ${cmd} doctor --config synapsor.runner.json --json
13609
+ ${cmd} doctor --config synapsor.runner.json --check-handlers
13610
+ ${cmd} doctor --config synapsor.runner.json --check-writeback
12634
13611
  ${cmd} doctor --config synapsor.runner.json --report --redact --output synapsor-doctor.md
12635
13612
  ${cmd} doctor --first-run
12636
13613
 
12637
- 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.
13614
+ Validate local config, environment bindings, semantic tool boundary, source metadata when reachable, handler signing/reachability, direct SQL writeback readiness, and local store stats. Reports are redacted; do not paste secrets into issues.
13615
+ Use --check-writeback only after reviewing receipt-table DDL/grants; it connects with the trusted writer and can create the receipt table if the writer has permission.
12638
13616
  `,
12639
13617
  proposals: `Usage:
12640
13618
  ${cmd} proposals list [--tenant acme] [--capability billing.propose_late_fee_waiver] [--object invoice:INV-3001] [--status applied]
@@ -12701,14 +13679,24 @@ or an administrator must pre-create and grant it.
12701
13679
  ${cmd} activity search --capability billing.propose_late_fee_waiver --from 2026-06-01 --to 2026-06-23
12702
13680
 
12703
13681
  Search the local SQLite evidence/replay ledger across proposals, evidence, query audit, receipts, and replay records.
13682
+ `,
13683
+ events: `Usage:
13684
+ ${cmd} events tail --store ./.synapsor/local.db
13685
+ ${cmd} events tail --proposal wrp_...
13686
+ ${cmd} events tail --kind writeback_applied
13687
+ ${cmd} events tail --follow --interval-ms 1000
13688
+
13689
+ Show local proposal/writeback lifecycle events such as proposal_created, proposal_approved, writeback_applied, writeback_conflict, and writeback_failed.
12704
13690
  `,
12705
13691
  store: `Usage:
12706
13692
  ${cmd} store stats --store ./.synapsor/local.db
12707
13693
  ${cmd} store vacuum --store ./.synapsor/local.db
12708
13694
  ${cmd} store prune --store ./.synapsor/local.db --older-than 30d --dry-run
12709
13695
  ${cmd} store prune --store ./.synapsor/local.db --older-than 30d --yes
13696
+ ${cmd} store prune --store ./.synapsor/local.db --older-than 30d --yes --force
13697
+ ${cmd} store reset --store ./.synapsor/local.db --yes
12710
13698
 
12711
- Local store maintenance only. Prune defaults to dry-run and never touches your source Postgres/MySQL database.
13699
+ Local store maintenance only. Prune defaults to dry-run and reset requires --yes. These commands never touch your source Postgres/MySQL database. Destructive operations refuse while an active server lease exists unless --force is provided.
12712
13700
  `,
12713
13701
  demo: `Usage:
12714
13702
  ${cmd} demo [--force]