@synapsor/runner 0.1.0-alpha.15 → 0.1.0-alpha.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/runner.mjs CHANGED
@@ -451,6 +451,7 @@ var SOURCE_KEYS = /* @__PURE__ */ new Set([
451
451
  "engine",
452
452
  "read_url_env",
453
453
  "write_url_env",
454
+ "read_only",
454
455
  "statement_timeout_ms",
455
456
  "ssl"
456
457
  ]);
@@ -564,6 +565,7 @@ function validateRunnerCapabilityConfig(input) {
564
565
  validateTrustedContext(input.trusted_context, input.contexts, input.capabilities, input.mode, strict, errors, warnings);
565
566
  validateExecutors(input.executors, input.mode, strict, errors);
566
567
  validateCapabilities(input.capabilities, input.sources, input.contexts, input.executors, input.mode, strict, errors, warnings);
568
+ validateWritebackReadiness(input.sources, input.capabilities, input.mode, errors, warnings);
567
569
  scanForForbiddenFields(input, "$", errors);
568
570
  return { ok: errors.length === 0, errors, warnings };
569
571
  }
@@ -622,14 +624,37 @@ function validateSources(value, mode, strict, errors, warnings) {
622
624
  if (source.statement_timeout_ms !== void 0 && !isPositiveInteger(source.statement_timeout_ms)) {
623
625
  errors.push({ path: `${path4}.statement_timeout_ms`, code: "INVALID_TIMEOUT", message: "statement_timeout_ms must be a positive integer." });
624
626
  }
627
+ if (source.read_only !== void 0 && typeof source.read_only !== "boolean") {
628
+ errors.push({ path: `${path4}.read_only`, code: "INVALID_SOURCE_READ_ONLY", message: "read_only must be true or false when provided." });
629
+ }
630
+ }
631
+ }
632
+ function validateWritebackReadiness(sources, capabilities, mode, errors, warnings) {
633
+ if (mode !== "review" || !isRecord(sources) || !Array.isArray(capabilities)) return;
634
+ capabilities.forEach((capability, index) => {
635
+ if (!isRecord(capability) || capability.kind !== "proposal") return;
636
+ const sourceName = isNonEmptyString(capability.source) ? capability.source : void 0;
637
+ const source = sourceName ? sources[sourceName] : void 0;
638
+ if (!sourceName || !isRecord(source)) return;
639
+ const executor = isNonEmptyString(capability.executor) ? capability.executor : "sql_update";
640
+ const directSql = executor === "sql_update";
641
+ if (!directSql) return;
642
+ if (source.read_only === true) {
643
+ errors.push({
644
+ path: `$.capabilities[${index}].executor`,
645
+ code: "READ_ONLY_SOURCE_DIRECT_WRITEBACK",
646
+ message: `Proposal capability ${String(capability.name ?? index)} uses direct SQL writeback, but source ${sourceName} is marked read_only.`
647
+ });
648
+ return;
649
+ }
625
650
  if (source.write_url_env === void 0) {
626
651
  warnings.push({
627
- path: `${path4}.write_url_env`,
652
+ path: `$.sources.${sourceName}.write_url_env`,
628
653
  code: "WRITEBACK_DISABLED",
629
- message: "No write_url_env is configured; review-mode proposal execution cannot apply external DB changes."
654
+ message: "No write_url_env is configured; direct SQL review-mode proposal execution cannot apply external DB changes."
630
655
  });
631
656
  }
632
- }
657
+ });
633
658
  }
634
659
  function validateCloud(value, mode, strict, errors) {
635
660
  if (mode !== "cloud") {
@@ -4466,6 +4491,7 @@ import { Pool as Pool3 } from "pg";
4466
4491
  var TENANT_COLUMNS = /* @__PURE__ */ new Set(["tenant_id", "account_id", "organization_id", "org_id", "workspace_id", "customer_id"]);
4467
4492
  var CONFLICT_COLUMNS = /* @__PURE__ */ new Set(["updated_at", "modified_at", "row_version", "version", "lock_version", "etag"]);
4468
4493
  var IMMUTABLE_COLUMNS = /* @__PURE__ */ new Set(["id", "uuid", "created_at", "created_by"]);
4494
+ var DEFAULT_RESULT_FORMAT = 2;
4469
4495
  var SENSITIVE_PATTERNS = [
4470
4496
  /password/i,
4471
4497
  /password_hash/i,
@@ -4525,15 +4551,18 @@ function generateRunnerConfigFromSpec(spec) {
4525
4551
  const lookupArg = spec.lookup_arg ?? `${objectName}_id`;
4526
4552
  const inspectToolName = spec.inspect_tool_name ?? `${spec.namespace}.inspect_${objectName}`;
4527
4553
  const proposalToolName = spec.proposal_tool_name ?? `${spec.namespace}.propose_${objectName}_update`;
4554
+ const objectLabel = objectName.replace(/_/g, " ");
4528
4555
  const visibleColumns = unique([spec.primary_key, spec.tenant_key, spec.conflict_column, ...spec.visible_columns].filter((value) => Boolean(value)));
4529
4556
  const readCapability = {
4530
4557
  name: inspectToolName,
4531
4558
  kind: "read",
4559
+ description: spec.inspect_description ?? `Inspect one ${objectLabel} in trusted tenant scope before answering or proposing a change.`,
4560
+ returns_hint: spec.inspect_returns_hint ?? `Returns reviewed ${objectLabel} fields, evidence handle, query audit, and source_database_changed:false.`,
4532
4561
  source: sourceName,
4533
4562
  context: "local_operator",
4534
4563
  target: target(spec),
4535
4564
  args: {
4536
- [lookupArg]: { type: "string", required: true, max_length: 128 }
4565
+ [lookupArg]: { type: "string", required: true, max_length: 128, description: `${capitalize(objectLabel)} id from the user request or trusted app context.` }
4537
4566
  },
4538
4567
  lookup: { id_from_arg: lookupArg },
4539
4568
  visible_columns: visibleColumns,
@@ -4546,6 +4575,8 @@ function generateRunnerConfigFromSpec(spec) {
4546
4575
  capabilities.push({
4547
4576
  name: proposalToolName,
4548
4577
  kind: "proposal",
4578
+ description: spec.proposal_description ?? `Create a review-required proposal to update one ${objectLabel}. The source database remains unchanged until approval and writeback.`,
4579
+ returns_hint: spec.proposal_returns_hint ?? "Returns a proposal id, exact before/after diff, evidence handle, approval status, and source_database_changed:false.",
4549
4580
  source: sourceName,
4550
4581
  context: "local_operator",
4551
4582
  ...writeback2.executor !== "sql_update" ? { executor: writeback2.executorName } : {},
@@ -4569,12 +4600,14 @@ function generateRunnerConfigFromSpec(spec) {
4569
4600
  const config = {
4570
4601
  version: 1,
4571
4602
  mode,
4603
+ result_format: spec.result_format ?? DEFAULT_RESULT_FORMAT,
4572
4604
  storage: { sqlite_path: "./.synapsor/local.db" },
4573
4605
  sources: {
4574
4606
  [sourceName]: {
4575
4607
  engine: spec.engine,
4576
4608
  read_url_env: readUrlEnv,
4577
4609
  ...mode === "review" && writeUrlEnv ? { write_url_env: writeUrlEnv } : {},
4610
+ ...mode === "review" && writeback2.executor !== "sql_update" && !writeUrlEnv ? { read_only: true } : {},
4578
4611
  statement_timeout_ms: spec.statement_timeout_ms ?? 3e3
4579
4612
  }
4580
4613
  },
@@ -5017,6 +5050,7 @@ function validateSelectionSpec(spec) {
5017
5050
  if (spec.version !== void 0 && spec.version !== 1) throw new Error("onboarding selection version must be 1.");
5018
5051
  if (spec.engine !== "postgres" && spec.engine !== "mysql") throw new Error("selection engine must be postgres or mysql.");
5019
5052
  if (!["read_only", "shadow", "review", void 0].includes(spec.mode)) throw new Error("selection mode must be read_only, shadow, or review.");
5053
+ if (spec.result_format !== void 0 && spec.result_format !== 1 && spec.result_format !== 2) throw new Error("selection result_format must be 1 or 2.");
5020
5054
  if (spec.writeback?.executor && !["sql_update", "http_handler", "command_handler"].includes(spec.writeback.executor)) {
5021
5055
  throw new Error("selection writeback.executor must be sql_update, http_handler, or command_handler.");
5022
5056
  }
@@ -5121,6 +5155,10 @@ function singularize(value) {
5121
5155
  function safeName(value) {
5122
5156
  return value.toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "record";
5123
5157
  }
5158
+ function capitalize(value) {
5159
+ if (!value) return value;
5160
+ return `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}`;
5161
+ }
5124
5162
  function assertSafeIdentifier(identifier) {
5125
5163
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
5126
5164
  throw new Error(`unsafe identifier in selection: ${identifier}`);
@@ -6573,6 +6611,19 @@ var defaultConfigPath = "synapsor.runner.json";
6573
6611
  var defaultStorePath = "./.synapsor/local.db";
6574
6612
  var quickDemoStorePath = "./.synapsor/quick-demo.db";
6575
6613
  var generatedSmokeInputPath = "./.synapsor/smoke-input.json";
6614
+ var handlerSecurityWarning = [
6615
+ "IMPORTANT: your app handler owns the final business write.",
6616
+ "Runner creates the proposal and calls your handler only after approval, but your handler must still enforce:",
6617
+ "- tenant/scope check;",
6618
+ "- expected-version or conflict guard;",
6619
+ "- idempotency key;",
6620
+ "- allowed business action;",
6621
+ "- transaction/rollback;",
6622
+ "- safe error receipt.",
6623
+ "",
6624
+ "If you skip those checks, you can reintroduce cross-tenant writes, lost updates, or duplicate writes.",
6625
+ "Use the generated template/helper pattern and keep handler credentials out of MCP."
6626
+ ].join("\n");
6576
6627
  var handlerTemplateDefinitions = {
6577
6628
  "node-fastify": {
6578
6629
  aliases: ["node", "fastify"],
@@ -6608,6 +6659,15 @@ app.post("/synapsor/writeback", async (request, reply) => {
6608
6659
  }
6609
6660
 
6610
6661
  /*
6662
+ * IMPORTANT: your app handler owns the final business write.
6663
+ * Runner creates the proposal and calls your handler only after approval,
6664
+ * but your handler must still enforce tenant/scope, expected-version or
6665
+ * conflict guard, idempotency key, allowed business action,
6666
+ * transaction/rollback, and safe error receipt.
6667
+ *
6668
+ * If you skip those checks, you can reintroduce cross-tenant writes,
6669
+ * lost updates, or duplicate writes. Keep handler credentials out of MCP.
6670
+ *
6611
6671
  * Put your app-owned transaction here.
6612
6672
  *
6613
6673
  * Examples:
@@ -6665,6 +6725,15 @@ async def synapsor_writeback(body: dict, authorization: str | None = Header(defa
6665
6725
 
6666
6726
  # Put your app-owned transaction here.
6667
6727
  #
6728
+ # IMPORTANT: your app handler owns the final business write.
6729
+ # Runner creates the proposal and calls your handler only after approval,
6730
+ # but your handler must still enforce tenant/scope, expected-version or
6731
+ # conflict guard, idempotency key, allowed business action,
6732
+ # transaction/rollback, and safe error receipt.
6733
+ #
6734
+ # If you skip those checks, you can reintroduce cross-tenant writes,
6735
+ # lost updates, or duplicate writes. Keep handler credentials out of MCP.
6736
+ #
6668
6737
  # Examples:
6669
6738
  # - insert a refund_review row;
6670
6739
  # - insert an account_credit row;
@@ -6716,6 +6785,15 @@ if (request.dry_run) {
6716
6785
  }
6717
6786
 
6718
6787
  /*
6788
+ * IMPORTANT: your app handler owns the final business write.
6789
+ * Runner creates the proposal and calls your handler only after approval,
6790
+ * but your handler must still enforce tenant/scope, expected-version or
6791
+ * conflict guard, idempotency key, allowed business action,
6792
+ * transaction/rollback, and safe error receipt.
6793
+ *
6794
+ * If you skip those checks, you can reintroduce cross-tenant writes,
6795
+ * lost updates, or duplicate writes. Keep handler credentials out of MCP.
6796
+ *
6719
6797
  * Put your app-owned command transaction here.
6720
6798
  *
6721
6799
  * Examples:
@@ -6784,6 +6862,7 @@ ${cliCommandName()} --help
6784
6862
  if (command === "propose") return propose(rest);
6785
6863
  if (command === "audit") return audit(rest);
6786
6864
  if (command === "start") return start(rest);
6865
+ if (command === "up") return up(rest);
6787
6866
  if (command === "runner") return runnerCommand(rest);
6788
6867
  if (command === "cloud") return cloud(rest);
6789
6868
  if (command === "mcp") return mcp(rest);
@@ -6813,11 +6892,16 @@ ${cliCommandName()} --help
6813
6892
  return 2;
6814
6893
  }
6815
6894
  async function init(args) {
6895
+ const answersPath = optionalArg(args, "--answers");
6896
+ if (answersPath) {
6897
+ return initFromAnswers(args, answersPath);
6898
+ }
6816
6899
  const specPath = optionalArg(args, "--spec");
6817
6900
  if (specPath) {
6818
6901
  return initFromSpec(args, specPath);
6819
6902
  }
6820
- if (args.includes("--wizard") || process2.stdin.isTTY && process2.stdout.isTTY && !args.includes("--starter")) {
6903
+ const scripted = isScriptedOnboardingArgs(args);
6904
+ if (args.includes("--wizard") || process2.stdin.isTTY && process2.stdout.isTTY && !args.includes("--starter") && !scripted) {
6821
6905
  return runInitWizard(args);
6822
6906
  }
6823
6907
  const inspectionJson = optionalArg(args, "--inspection-json");
@@ -6983,12 +7067,49 @@ async function runInitWizard(args, options = {}) {
6983
7067
  if (transitionFromColumns.length > 0) ensureColumnsExist(transitionFromColumns, columns, "transition from");
6984
7068
  }
6985
7069
  }
6986
- const namespace = await askDefault(ask, "Capability namespace", optionalArg(args, "--namespace") ?? recipeSpec?.namespace ?? "source");
6987
- const objectName = await askDefault(ask, "Business object name", optionalArg(args, "--object-name") ?? recipeSpec?.object_name ?? safeObjectName(table.name));
7070
+ const inferredObjectName = recipeSpec?.object_name ?? safeObjectName(table.name);
7071
+ const namespace = await askDefault(ask, "Capability namespace", optionalArg(args, "--namespace") ?? recipeSpec?.namespace ?? inferCapabilityNamespace(table.name));
7072
+ const objectName = await askDefault(ask, "Business object name", optionalArg(args, "--object-name") ?? inferredObjectName);
6988
7073
  const lookupArg = await askDefault(ask, "Model-visible object id argument", optionalArg(args, "--lookup-arg") ?? recipeSpec?.lookup_arg ?? `${objectName}_id`);
7074
+ const defaultInspectToolName = recipeSpec?.inspect_tool_name ?? `${namespace}.inspect_${objectName}`;
7075
+ const inspectToolName = await askDefault(
7076
+ ask,
7077
+ "Read capability name",
7078
+ optionalArg(args, "--read-tool") ?? optionalArg(args, "--inspect-tool-name") ?? defaultInspectToolName
7079
+ );
7080
+ const defaultProposalToolName = recipeSpec?.proposal_tool_name ?? `${namespace}.propose_${objectName}_update`;
7081
+ const proposalToolName = mode === "read_only" ? void 0 : await askDefault(
7082
+ ask,
7083
+ "Proposal capability name",
7084
+ optionalArg(args, "--proposal-tool") ?? optionalArg(args, "--proposal-tool-name") ?? defaultProposalToolName
7085
+ );
6989
7086
  const smokeObjectId = await askDefault(ask, "Optional real object id for a first smoke call", optionalArg(args, "--smoke-id") ?? "");
7087
+ const objectLabel = objectName.replace(/_/g, " ");
7088
+ const inspectDescription = await askDefault(
7089
+ ask,
7090
+ "Read capability description",
7091
+ optionalArg(args, "--inspect-description") ?? `Inspect one ${objectLabel} in trusted tenant scope before answering or proposing a change.`
7092
+ );
7093
+ const inspectReturnsHint = await askDefault(
7094
+ ask,
7095
+ "Read capability returns hint",
7096
+ optionalArg(args, "--inspect-returns-hint") ?? `Returns reviewed ${objectLabel} fields, evidence handle, query audit, and source_database_changed:false.`
7097
+ );
7098
+ const proposalDescription = mode === "read_only" ? void 0 : await askDefault(
7099
+ ask,
7100
+ "Proposal capability description",
7101
+ optionalArg(args, "--proposal-description") ?? `Create a review-required proposal to update one ${objectLabel}. The source database remains unchanged until approval and writeback.`
7102
+ );
7103
+ const proposalReturnsHint = mode === "read_only" ? void 0 : await askDefault(
7104
+ ask,
7105
+ "Proposal capability returns hint",
7106
+ optionalArg(args, "--proposal-returns-hint") ?? "Returns a proposal id, exact before/after diff, evidence handle, approval status, and source_database_changed:false."
7107
+ );
7108
+ const resultFormatAnswer = await askChoice(ask, "MCP result envelope", optionalArg(args, "--result-format") ? normalizeResultFormatAnswer(optionalArg(args, "--result-format")) : "v2", ["v2", "v1", "default"]);
7109
+ const resultFormat = resultFormatAnswer === "v1" ? 1 : resultFormatAnswer === "v2" ? 2 : void 0;
6990
7110
  let writeUrlEnv = optionalArg(args, "--write-url-env");
6991
7111
  let writeback2;
7112
+ let generatedHandlerTemplate;
6992
7113
  if (mode === "review") {
6993
7114
  const writebackPath = await askChoice(
6994
7115
  ask,
@@ -7011,6 +7132,12 @@ async function runInitWizard(args, options = {}) {
7011
7132
  ...signingSecretEnv ? { handler_signing_secret_env: signingSecretEnv } : {},
7012
7133
  timeout_ms: positiveIntegerOption(args, "--handler-timeout-ms")
7013
7134
  };
7135
+ const writeTemplate = await askChoice(ask, "Write starter app-owned handler template", args.includes("--skip-handler-template") ? "no" : "yes", ["yes", "no"]);
7136
+ if (writeTemplate === "yes") {
7137
+ const template = await askChoice(ask, "Handler template", optionalArg(args, "--handler-template") ?? "node-fastify", ["node-fastify", "python-fastapi"]);
7138
+ const output = await askDefault(ask, "Handler template output", optionalArg(args, "--handler-output") ?? optionalArg(args, "--handler-template-output") ?? handlerTemplateDefinitions[template].fileName);
7139
+ generatedHandlerTemplate = { name: template, output };
7140
+ }
7014
7141
  } else {
7015
7142
  const commandEnv = await askEnvName(ask, "App-owned command handler env var", optionalArg(args, "--handler-command-env") ?? "SYNAPSOR_APP_WRITEBACK_COMMAND");
7016
7143
  writeback2 = {
@@ -7019,10 +7146,15 @@ async function runInitWizard(args, options = {}) {
7019
7146
  handler_command_env: commandEnv,
7020
7147
  timeout_ms: positiveIntegerOption(args, "--handler-timeout-ms")
7021
7148
  };
7149
+ const writeTemplate = await askChoice(ask, "Write starter app-owned handler template", args.includes("--skip-handler-template") ? "no" : "yes", ["yes", "no"]);
7150
+ if (writeTemplate === "yes") {
7151
+ const output = await askDefault(ask, "Handler template output", optionalArg(args, "--handler-output") ?? optionalArg(args, "--handler-template-output") ?? handlerTemplateDefinitions.command.fileName);
7152
+ generatedHandlerTemplate = { name: "command", output };
7153
+ }
7022
7154
  }
7023
7155
  }
7024
7156
  const approvalRole = mode === "read_only" ? "local_reviewer" : await askDefault(ask, "Required approval role", optionalArg(args, "--approval-role") ?? recipeSpec?.approval?.required_role ?? "local_reviewer");
7025
- const spec = {
7157
+ let spec = {
7026
7158
  version: 1,
7027
7159
  engine: inspection.engine,
7028
7160
  mode,
@@ -7037,9 +7169,14 @@ async function runInitWizard(args, options = {}) {
7037
7169
  conflict_column: conflictAnswer || void 0,
7038
7170
  namespace,
7039
7171
  object_name: objectName,
7040
- inspect_tool_name: recipeSpec?.inspect_tool_name,
7041
- proposal_tool_name: recipeSpec?.proposal_tool_name,
7172
+ inspect_tool_name: inspectToolName,
7173
+ proposal_tool_name: proposalToolName,
7174
+ inspect_description: inspectDescription,
7175
+ inspect_returns_hint: inspectReturnsHint,
7176
+ proposal_description: proposalDescription,
7177
+ proposal_returns_hint: proposalReturnsHint,
7042
7178
  lookup_arg: lookupArg,
7179
+ result_format: resultFormat,
7043
7180
  visible_columns: visibleColumns,
7044
7181
  allowed_columns: allowedColumns,
7045
7182
  patch,
@@ -7055,26 +7192,48 @@ async function runInitWizard(args, options = {}) {
7055
7192
  },
7056
7193
  writeback: writeback2
7057
7194
  };
7058
- const generated = generateRunnerConfigFromSpec(spec);
7059
- const generatedCapabilities = generated.config.capabilities;
7060
- const tools2 = generatedCapabilities.map((capability) => `${capability.name} (${capability.kind})`);
7061
- const smokeToolName = generatedCapabilities[0]?.name ?? "<inspect_tool>";
7195
+ let generated = generateRunnerConfigFromSpec(spec);
7062
7196
  stdout.write("\nPreview:\n");
7063
- stdout.write(` trusted context: tenant from ${tenantEnv}${singleTenantDev ? " (single-tenant dev source)" : tenantAnswer ? ` via ${tenantAnswer}` : ""}; principal from ${principalEnv}
7197
+ printWizardContractPreview(stdout, { spec, generated, engine: inspection.engine, table });
7198
+ if (generatedHandlerTemplate) {
7199
+ stdout.write(` handler template: ${generatedHandlerTemplate.output}
7064
7200
  `);
7065
- stdout.write(` source: ${inspection.engine} ${table.schema}.${table.name}
7066
- `);
7067
- stdout.write(` mode: ${mode}
7068
- `);
7069
- stdout.write(` writeback path: ${writeback2?.executor ?? (mode === "review" ? "sql_update" : "none")}
7070
- `);
7071
- stdout.write(` exposed tools: ${tools2.join(", ")}
7201
+ stdout.write(`${handlerSecurityWarning}
7072
7202
  `);
7073
- stdout.write(" not exposed: execute_sql, approval tools, commit tools, database URLs, write credentials, model-controlled tenant authority\n");
7203
+ }
7204
+ const editPreview = await askDefault(ask, "Edit visible fields or capability names before writing? Type yes to edit", "no");
7205
+ if (editPreview.toLowerCase() === "yes") {
7206
+ const updatedVisible = parseColumnList(await askDefault(
7207
+ ask,
7208
+ "Final visible columns",
7209
+ spec.visible_columns?.join(",") ?? visibleColumns.join(",")
7210
+ ));
7211
+ ensureColumnsExist(updatedVisible, columns, "visible");
7212
+ const currentReadTool = spec.inspect_tool_name ?? generated.config.capabilities.find((capability) => capability.kind === "read")?.name ?? inspectToolName;
7213
+ const updatedReadTool = await askDefault(ask, "Final read capability name", currentReadTool);
7214
+ const currentProposalTool = spec.proposal_tool_name ?? generated.config.capabilities.find((capability) => capability.kind === "proposal")?.name ?? proposalToolName ?? "";
7215
+ const updatedProposalTool = spec.mode === "read_only" ? void 0 : await askDefault(ask, "Final proposal capability name", currentProposalTool);
7216
+ spec = {
7217
+ ...spec,
7218
+ visible_columns: updatedVisible,
7219
+ inspect_tool_name: updatedReadTool,
7220
+ proposal_tool_name: updatedProposalTool
7221
+ };
7222
+ generated = generateRunnerConfigFromSpec(spec);
7223
+ stdout.write("\nUpdated preview:\n");
7224
+ printWizardContractPreview(stdout, { spec, generated, engine: inspection.engine, table });
7225
+ }
7226
+ const generatedCapabilities = generated.config.capabilities;
7227
+ const smokeToolName = generatedCapabilities[0]?.name ?? "<inspect_tool>";
7074
7228
  const confirmed = await askDefault(ask, "Write generated config and MCP snippets? Type yes to continue", "no");
7075
7229
  if (confirmed.toLowerCase() !== "yes") throw new Error("guided init canceled before writing files");
7076
7230
  const outputPath = outputArg(args) ?? "synapsor.runner.json";
7077
7231
  await writeGeneratedOnboardingFiles(outputPath, generated, args.includes("--force"), { printNext: false });
7232
+ if (generatedHandlerTemplate) {
7233
+ await writeHandlerTemplateFile(generatedHandlerTemplate.name, generatedHandlerTemplate.output, args.includes("--force"));
7234
+ stdout.write(`created ${generatedHandlerTemplate.output}
7235
+ `);
7236
+ }
7078
7237
  if (smokeObjectId) {
7079
7238
  await writeGeneratedSmokeInputFile(lookupArg, smokeObjectId, args.includes("--force"));
7080
7239
  stdout.write(`created ${generatedSmokeInputPath}
@@ -7103,9 +7262,42 @@ async function runInitWizard(args, options = {}) {
7103
7262
  `);
7104
7263
  }
7105
7264
  stdout.write(` 3. Serve MCP tools: ${cliCommandName()} mcp serve --config ${outputPath} --store ${defaultStorePath}
7265
+ `);
7266
+ stdout.write(` OpenAI Agents SDK: use ${cliCommandName()} mcp serve-streamable-http --config ${outputPath} --store ${defaultStorePath} --alias-mode openai
7106
7267
  `);
7107
7268
  return 0;
7108
7269
  }
7270
+ function printWizardContractPreview(stdout, input) {
7271
+ const capabilities = input.generated.config.capabilities;
7272
+ const tools2 = capabilities.map((capability) => `${capability.name} (${capability.kind})`);
7273
+ const readCapability = capabilities.find((capability) => capability.kind === "read")?.name ?? input.spec.inspect_tool_name ?? "<read_tool>";
7274
+ const proposalCapability = capabilities.find((capability) => capability.kind === "proposal")?.name ?? input.spec.proposal_tool_name;
7275
+ const visibleColumns = input.spec.visible_columns ?? [];
7276
+ const tenantEnv = input.spec.trusted_context?.tenant_id_env ?? "SYNAPSOR_TENANT_ID";
7277
+ const principalEnv = input.spec.trusted_context?.principal_env ?? "SYNAPSOR_PRINCIPAL";
7278
+ const visiblePreview = visibleColumns.length <= 12 ? visibleColumns.join(", ") : `${visibleColumns.slice(0, 12).join(", ")} (+${visibleColumns.length - 12} more)`;
7279
+ stdout.write(` trusted context: tenant from ${tenantEnv}${input.spec.single_tenant_dev ? " (single-tenant dev source)" : input.spec.tenant_key ? ` via ${input.spec.tenant_key}` : ""}; principal from ${principalEnv}
7280
+ `);
7281
+ stdout.write(` source: ${input.engine} ${input.table.schema}.${input.table.name}
7282
+ `);
7283
+ stdout.write(` primary key: ${input.spec.primary_key}${input.spec.conflict_column ? `; conflict guard: ${input.spec.conflict_column}` : ""}
7284
+ `);
7285
+ stdout.write(` visible fields: ${visiblePreview || "none"}
7286
+ `);
7287
+ stdout.write(` mode: ${input.spec.mode}
7288
+ `);
7289
+ stdout.write(` result envelope: ${input.spec.result_format ? `v${input.spec.result_format}` : "default"}
7290
+ `);
7291
+ stdout.write(` writeback path: ${input.spec.writeback?.executor ?? (input.spec.mode === "review" ? "sql_update" : "none")}
7292
+ `);
7293
+ stdout.write(` read capability: ${readCapability}
7294
+ `);
7295
+ if (proposalCapability) stdout.write(` proposal capability: ${proposalCapability}
7296
+ `);
7297
+ stdout.write(` exposed tools: ${tools2.join(", ")}
7298
+ `);
7299
+ stdout.write(" not exposed: execute_sql, approval tools, commit tools, database URLs, write credentials, model-controlled tenant authority\n");
7300
+ }
7109
7301
  async function initFromSpec(args, specPath) {
7110
7302
  if (!args.includes("--non-interactive")) {
7111
7303
  throw new Error("init --spec requires --non-interactive so reviewed selections are explicit.");
@@ -7114,9 +7306,123 @@ async function initFromSpec(args, specPath) {
7114
7306
  const force = args.includes("--force");
7115
7307
  const spec = JSON.parse(await fs3.readFile(specPath, "utf8"));
7116
7308
  const generated = generateRunnerConfigFromSpec(spec);
7309
+ if (args.includes("--dry-run")) {
7310
+ process2.stdout.write(`${JSON.stringify(generated.config, null, 2)}
7311
+ `);
7312
+ return 0;
7313
+ }
7314
+ await writeGeneratedOnboardingFiles(output, generated, force);
7315
+ return 0;
7316
+ }
7317
+ async function initFromAnswers(args, answersPath) {
7318
+ const output = outputArg(args) ?? "synapsor.runner.json";
7319
+ const force = args.includes("--force");
7320
+ const raw = JSON.parse(await fs3.readFile(answersPath, "utf8"));
7321
+ const spec = answersToSelectionSpec(raw);
7322
+ const generated = generateRunnerConfigFromSpec(spec);
7323
+ if (args.includes("--dry-run")) {
7324
+ process2.stdout.write(`${JSON.stringify(generated.config, null, 2)}
7325
+ `);
7326
+ return 0;
7327
+ }
7117
7328
  await writeGeneratedOnboardingFiles(output, generated, force);
7329
+ await maybeWriteHandlerTemplateForArgs(args, spec.writeback);
7118
7330
  return 0;
7119
7331
  }
7332
+ function isScriptedOnboardingArgs(args) {
7333
+ return args.includes("--yes") || args.includes("--non-interactive") || args.includes("--dry-run") || Boolean(optionalArg(args, "--answers")) || Boolean(optionalArg(args, "--inspection-json")) || Boolean(optionalArg(args, "--table"));
7334
+ }
7335
+ function answersToSelectionSpec(raw) {
7336
+ if (!isRecord6(raw)) throw new Error("--answers file must contain a JSON object");
7337
+ const mode = stringValue(raw.mode) ?? "review";
7338
+ if (!["read_only", "shadow", "review"].includes(mode)) throw new Error("answers.mode must be read_only, shadow, or review");
7339
+ const engine = stringValue(raw.engine) ?? "postgres";
7340
+ if (engine !== "postgres" && engine !== "mysql") throw new Error("answers.engine must be postgres or mysql");
7341
+ const table = requiredAnswerString(raw.table, "table");
7342
+ const objectName = stringValue(raw.object_name) ?? safeObjectName(table);
7343
+ const namespace = stringValue(raw.namespace) ?? inferCapabilityNamespace(table);
7344
+ const writebackRaw = stringValue(raw.writeback) ?? "sql_update";
7345
+ if (!["sql_update", "http_handler", "command_handler"].includes(writebackRaw)) throw new Error("answers.writeback must be sql_update, http_handler, or command_handler");
7346
+ const writeback2 = writebackRaw === "sql_update" ? { executor: "sql_update" } : writebackRaw === "http_handler" ? {
7347
+ executor: "http_handler",
7348
+ handler_url_env: stringValue(raw.handler_url_env) ?? "SYNAPSOR_APP_WRITEBACK_URL",
7349
+ ...stringValue(raw.handler_token_env) ? { handler_token_env: stringValue(raw.handler_token_env) } : {},
7350
+ ...stringValue(raw.handler_signing_secret_env) ? { handler_signing_secret_env: stringValue(raw.handler_signing_secret_env) } : {}
7351
+ } : {
7352
+ executor: "command_handler",
7353
+ handler_command_env: stringValue(raw.handler_command_env) ?? "SYNAPSOR_APP_WRITEBACK_COMMAND"
7354
+ };
7355
+ const patch = parsePatchBindings(arrayOrStringList(raw.patch), "--answers.patch");
7356
+ const allowedColumns = arrayOrStringList(raw.allowed_columns);
7357
+ return {
7358
+ version: 1,
7359
+ engine,
7360
+ mode,
7361
+ source_name: stringValue(raw.source_name),
7362
+ read_url_env: stringValue(raw.read_url_env) ?? stringValue(raw.database_url_env) ?? "DATABASE_URL",
7363
+ write_url_env: writeback2.executor === "sql_update" ? stringValue(raw.write_url_env) ?? "SYNAPSOR_DATABASE_WRITE_URL" : stringValue(raw.write_url_env),
7364
+ schema: requiredAnswerString(raw.schema, "schema"),
7365
+ table,
7366
+ primary_key: requiredAnswerString(raw.primary_key, "primary_key"),
7367
+ tenant_key: stringValue(raw.tenant_column) ?? stringValue(raw.tenant_key),
7368
+ single_tenant_dev: raw.single_tenant_dev === true,
7369
+ conflict_column: stringValue(raw.conflict_column),
7370
+ namespace,
7371
+ object_name: objectName,
7372
+ inspect_tool_name: stringValue(raw.read_tool) ?? stringValue(raw.inspect_tool_name),
7373
+ proposal_tool_name: stringValue(raw.proposal_tool) ?? stringValue(raw.proposal_tool_name),
7374
+ lookup_arg: stringValue(raw.id_arg) ?? stringValue(raw.lookup_arg),
7375
+ inspect_description: stringValue(raw.read_description) ?? stringValue(raw.inspect_description),
7376
+ inspect_returns_hint: stringValue(raw.read_returns_hint) ?? stringValue(raw.inspect_returns_hint),
7377
+ proposal_description: stringValue(raw.proposal_description),
7378
+ proposal_returns_hint: stringValue(raw.proposal_returns_hint),
7379
+ result_format: resultFormatFromAnswerValue(raw.result_format),
7380
+ visible_columns: arrayOrStringList(raw.visible_columns),
7381
+ allowed_columns: allowedColumns.length > 0 ? allowedColumns : void 0,
7382
+ patch,
7383
+ numeric_bounds: parseNumericBoundsInput(arrayOrStringList(raw.patch_bounds ?? raw.numeric_bounds).join(",")),
7384
+ transition_guards: parseTransitionGuardsInput(arrayOrStringList(raw.status_guards ?? raw.transition_guards).join(",")),
7385
+ trusted_context: {
7386
+ tenant_id_env: stringValue(raw.tenant_env) ?? "SYNAPSOR_TENANT_ID",
7387
+ principal_env: stringValue(raw.principal_env) ?? "SYNAPSOR_PRINCIPAL"
7388
+ },
7389
+ approval: {
7390
+ required_role: stringValue(raw.approval_role) ?? "local_reviewer"
7391
+ },
7392
+ writeback: writeback2
7393
+ };
7394
+ }
7395
+ async function maybeWriteHandlerTemplateForArgs(args, writeback2) {
7396
+ if (!writeback2 || writeback2.executor === "sql_update" || args.includes("--no-emit-handler") || args.includes("--skip-handler-template")) return;
7397
+ if (!args.includes("--emit-handler") && !optionalArg(args, "--handler-template") && !optionalArg(args, "--handler-output") && !optionalArg(args, "--handler-template-output")) return;
7398
+ const defaultTemplate = writeback2.executor === "command_handler" ? "command" : "node-fastify";
7399
+ const template = resolveHandlerTemplateName(optionalArg(args, "--handler-template") ?? defaultTemplate);
7400
+ const output = optionalArg(args, "--handler-output") ?? optionalArg(args, "--handler-template-output") ?? handlerTemplateDefinitions[template].fileName;
7401
+ await writeHandlerTemplateFile(template, output, args.includes("--force"));
7402
+ process2.stdout.write(`created ${output}
7403
+ `);
7404
+ process2.stdout.write(`${handlerSecurityWarning}
7405
+ `);
7406
+ }
7407
+ function stringValue(value) {
7408
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
7409
+ }
7410
+ function requiredAnswerString(value, field) {
7411
+ const result = stringValue(value);
7412
+ if (!result) throw new Error(`--answers missing ${field}`);
7413
+ return result;
7414
+ }
7415
+ function arrayOrStringList(value) {
7416
+ if (Array.isArray(value)) return value.map((item) => String(item).trim()).filter(Boolean);
7417
+ if (typeof value === "string") return value.split(",").map((item) => item.trim()).filter(Boolean);
7418
+ return [];
7419
+ }
7420
+ function resultFormatFromAnswerValue(value) {
7421
+ if (value === void 0 || value === null || value === "" || value === "default") return void 0;
7422
+ if (value === 1 || value === "1" || value === "v1") return 1;
7423
+ if (value === 2 || value === "2" || value === "v2") return 2;
7424
+ throw new Error("result_format must be default, v1, v2, 1, or 2");
7425
+ }
7120
7426
  async function initFromInspection(args, inspection, databaseUrlEnv) {
7121
7427
  const tableName = optionalArg(args, "--table");
7122
7428
  if (!tableName) {
@@ -7140,7 +7446,7 @@ async function initFromInspection(args, inspection, databaseUrlEnv) {
7140
7446
  process2.stderr.write(`warning: no database primary-key constraint detected for ${table.schema}.${table.name}; using candidate column ${primaryKey}. Verify uniqueness before enabling writeback.
7141
7447
  `);
7142
7448
  }
7143
- const tenantKey = optionalArg(args, "--tenant-key") ?? table.suggestions.tenant_columns[0];
7449
+ const tenantKey = optionalArg(args, "--tenant-column") ?? optionalArg(args, "--tenant-key") ?? table.suggestions.tenant_columns[0];
7144
7450
  const singleTenantDev = args.includes("--single-tenant-dev");
7145
7451
  if (!tenantKey && !singleTenantDev) {
7146
7452
  throw new Error(`--tenant-key is required for ${table.schema}.${table.name}, or pass --single-tenant-dev for a reviewed single-tenant dev source.`);
@@ -7163,22 +7469,31 @@ async function initFromInspection(args, inspection, databaseUrlEnv) {
7163
7469
  const allowedColumns = listArg(args, "--allowed-columns") ?? Object.keys(patch);
7164
7470
  const writeback2 = writebackSpecFromArgs(args);
7165
7471
  const sqlWriteback = (writeback2?.executor ?? "sql_update") === "sql_update";
7472
+ const objectName = optionalArg(args, "--object-name") ?? safeObjectName(table.name);
7473
+ const namespace = optionalArg(args, "--namespace") ?? inferCapabilityNamespace(table.name);
7166
7474
  const spec = {
7167
7475
  version: 1,
7168
7476
  engine: inspection.engine,
7169
7477
  mode,
7170
7478
  source_name: optionalArg(args, "--source-name"),
7171
7479
  read_url_env: databaseUrlEnv,
7172
- write_url_env: sqlWriteback ? optionalArg(args, "--write-url-env") ?? "SYNAPSOR_DATABASE_WRITE_URL" : void 0,
7480
+ write_url_env: sqlWriteback ? optionalArg(args, "--write-url-env") ?? "SYNAPSOR_DATABASE_WRITE_URL" : optionalArg(args, "--write-url-env"),
7173
7481
  schema: table.schema,
7174
7482
  table: table.name,
7175
7483
  primary_key: primaryKey,
7176
7484
  tenant_key: tenantKey,
7177
7485
  single_tenant_dev: singleTenantDev,
7178
7486
  conflict_column: conflictColumn,
7179
- namespace: optionalArg(args, "--namespace") ?? "source",
7180
- object_name: optionalArg(args, "--object-name"),
7181
- lookup_arg: optionalArg(args, "--lookup-arg"),
7487
+ namespace,
7488
+ object_name: objectName,
7489
+ inspect_tool_name: optionalArg(args, "--read-tool") ?? optionalArg(args, "--inspect-tool-name"),
7490
+ proposal_tool_name: optionalArg(args, "--proposal-tool") ?? optionalArg(args, "--proposal-tool-name"),
7491
+ lookup_arg: optionalArg(args, "--id-arg") ?? optionalArg(args, "--lookup-arg"),
7492
+ inspect_description: optionalArg(args, "--read-description") ?? optionalArg(args, "--inspect-description"),
7493
+ inspect_returns_hint: optionalArg(args, "--read-returns-hint") ?? optionalArg(args, "--inspect-returns-hint"),
7494
+ proposal_description: optionalArg(args, "--proposal-description"),
7495
+ proposal_returns_hint: optionalArg(args, "--proposal-returns-hint"),
7496
+ result_format: resultFormatOption(args),
7182
7497
  visible_columns: visibleColumns,
7183
7498
  allowed_columns: allowedColumns,
7184
7499
  patch,
@@ -7194,7 +7509,13 @@ async function initFromInspection(args, inspection, databaseUrlEnv) {
7194
7509
  writeback: writeback2
7195
7510
  };
7196
7511
  const generated = generateRunnerConfigFromSpec(spec);
7512
+ if (args.includes("--dry-run")) {
7513
+ process2.stdout.write(`${JSON.stringify(generated.config, null, 2)}
7514
+ `);
7515
+ return 0;
7516
+ }
7197
7517
  await writeGeneratedOnboardingFiles(outputArg(args) ?? "synapsor.runner.json", generated, args.includes("--force"));
7518
+ await maybeWriteHandlerTemplateForArgs(args, writeback2);
7198
7519
  process2.stdout.write(`selected ${table.schema}.${table.name} from ${inspection.engine} inspection
7199
7520
  `);
7200
7521
  process2.stdout.write(`exposed tools: ${generated.config.capabilities.map((capability) => capability.name).join(", ")}
@@ -7410,6 +7731,11 @@ function safeObjectName(tableName) {
7410
7731
  const base = tableName.replace(/[^A-Za-z0-9_]/g, "_").replace(/s$/, "");
7411
7732
  return /^[A-Za-z_]/.test(base) ? base : `record_${base}`;
7412
7733
  }
7734
+ function inferCapabilityNamespace(tableName) {
7735
+ const objectName = safeObjectName(tableName);
7736
+ const [firstPart] = objectName.split("_").filter(Boolean);
7737
+ return firstPart ?? objectName;
7738
+ }
7413
7739
  function requiredWritebackEngine(args) {
7414
7740
  const value = optionalArg(args, "--engine") ?? firstPositional(args);
7415
7741
  if (value === "postgres" || value === "mysql") return value;
@@ -7514,6 +7840,7 @@ function repeatedArgs(args, flag) {
7514
7840
  }
7515
7841
  function parsePatchFlags(args) {
7516
7842
  const patch = {};
7843
+ Object.assign(patch, parsePatchBindings(repeatedArgs(args, "--patch"), "--patch"));
7517
7844
  for (const binding of repeatedArgs(args, "--patch-fixed")) {
7518
7845
  const [column, ...rest] = binding.split("=");
7519
7846
  const value = rest.join("=");
@@ -7528,8 +7855,27 @@ function parsePatchFlags(args) {
7528
7855
  }
7529
7856
  return patch;
7530
7857
  }
7858
+ function parsePatchBindings(bindings, label) {
7859
+ const patch = {};
7860
+ for (const rawBinding of bindings.flatMap((binding) => binding.split(",")).map((item) => item.trim()).filter(Boolean)) {
7861
+ const [column, ...rest] = rawBinding.split("=");
7862
+ const expression = rest.join("=");
7863
+ if (!column || !expression) throw new Error(`${label} must use column=fixed:value or column=arg:name`);
7864
+ const [kind, ...valueParts] = expression.split(":");
7865
+ const value = valueParts.join(":");
7866
+ if (!valueParts.length || !value) throw new Error(`${label} must use column=fixed:value or column=arg:name`);
7867
+ if (kind === "fixed") {
7868
+ patch[column] = { fixed: parseFixedPatchValue(value) };
7869
+ } else if (kind === "arg") {
7870
+ patch[column] = { from_arg: value };
7871
+ } else {
7872
+ throw new Error(`${label} patch kind for ${column} must be fixed or arg`);
7873
+ }
7874
+ }
7875
+ return patch;
7876
+ }
7531
7877
  function parseNumericBoundsFlags(args) {
7532
- return parseNumericBoundsInput(repeatedArgs(args, "--numeric-bound").join(","));
7878
+ return parseNumericBoundsInput([...repeatedArgs(args, "--numeric-bound"), ...repeatedArgs(args, "--patch-bounds")].join(","));
7533
7879
  }
7534
7880
  function parseNumericBoundsInput(input) {
7535
7881
  const bounds = {};
@@ -7561,7 +7907,7 @@ function formatNumericBounds(bounds) {
7561
7907
  return Object.entries(bounds).map(([column, bound]) => `${column}=${bound.minimum ?? ""}:${bound.maximum ?? ""}`).join(",");
7562
7908
  }
7563
7909
  function parseTransitionGuardFlags(args) {
7564
- return parseTransitionGuardsInput(repeatedArgs(args, "--transition-guard").join(","));
7910
+ return parseTransitionGuardsInput([...repeatedArgs(args, "--transition-guard"), ...repeatedArgs(args, "--status-guards")].join(","));
7565
7911
  }
7566
7912
  function parseTransitionGuardsInput(input) {
7567
7913
  const guards = {};
@@ -9131,9 +9477,211 @@ async function start(args = []) {
9131
9477
  await startPolling(config, adapters, controller.signal);
9132
9478
  return 0;
9133
9479
  }
9480
+ async function up(args = []) {
9481
+ const allowed = /* @__PURE__ */ new Set([
9482
+ "--config",
9483
+ "--store",
9484
+ "--transport",
9485
+ "--serve",
9486
+ "--with-handler",
9487
+ "--host",
9488
+ "--port",
9489
+ "--auth-token-env",
9490
+ "--alias-mode",
9491
+ "--tool-name-style",
9492
+ "--openai-tool-aliases",
9493
+ "--result-format",
9494
+ "--handler-check",
9495
+ "--open-ui",
9496
+ "--print-next",
9497
+ "--dry-run",
9498
+ "--dev-no-auth",
9499
+ "--cors-origin",
9500
+ "--allow-concurrent-store"
9501
+ ]);
9502
+ assertKnownOptions(args, allowed, "up");
9503
+ const configPath = optionalArg(args, "--config") ?? defaultConfigPath;
9504
+ const config = await readRuntimeConfig(configPath);
9505
+ const storePath = optionalArg(args, "--store") ?? config.storage?.sqlite_path ?? defaultStorePath;
9506
+ const serveRequested = args.includes("--serve");
9507
+ const transport = optionalArg(args, "--transport") ?? (serveRequested ? "streamable-http" : "stdio");
9508
+ if (transport !== "stdio" && transport !== "streamable-http") {
9509
+ throw new Error("--transport must be stdio or streamable-http");
9510
+ }
9511
+ if (serveRequested && transport === "stdio") {
9512
+ throw new Error("up --serve starts the Streamable HTTP MCP server. Omit --transport or use --transport streamable-http; for stdio, use mcp client-config so the client launches Runner.");
9513
+ }
9514
+ const port = Number(optionalArg(args, "--port") ?? "8766");
9515
+ if (transport === "streamable-http" && (!Number.isInteger(port) || port <= 0 || port > 65535)) {
9516
+ throw new Error("--port must be an integer from 1 to 65535");
9517
+ }
9518
+ const aliasMode = toolNameStyleOption(args);
9519
+ const resultFormat = resultFormatOption(args);
9520
+ const validation = validateRunnerCapabilityConfig(config);
9521
+ if (!validation.ok) {
9522
+ throw new Error(`cannot bring Runner up with invalid config: ${validation.errors.map((error) => `${error.path} ${error.code}`).join("; ")}`);
9523
+ }
9524
+ if (storePath !== ":memory:") {
9525
+ await fs3.mkdir(path3.dirname(path3.resolve(storePath)), { recursive: true });
9526
+ }
9527
+ await assertNoActiveStoreLease(storePath, args.includes("--allow-concurrent-store"), "review-mode up");
9528
+ const boundary = await inspectMcpToolBoundary([
9529
+ "--config",
9530
+ configPath,
9531
+ "--store",
9532
+ storePath,
9533
+ "--alias-mode",
9534
+ aliasMode
9535
+ ]);
9536
+ process2.stdout.write(formatReviewModeUp({
9537
+ aliasMode,
9538
+ authTokenEnv: optionalArg(args, "--auth-token-env") ?? "SYNAPSOR_RUNNER_HTTP_TOKEN",
9539
+ boundary,
9540
+ config,
9541
+ configPath,
9542
+ dryRun: args.includes("--dry-run"),
9543
+ host: optionalArg(args, "--host") ?? "127.0.0.1",
9544
+ openUi: args.includes("--open-ui"),
9545
+ port,
9546
+ resultFormat,
9547
+ serveRequested,
9548
+ storePath,
9549
+ transport
9550
+ }));
9551
+ if (args.includes("--with-handler") || args.includes("--handler-check")) {
9552
+ process2.stdout.write("\nHandler check:\n");
9553
+ const doctorCode = await doctor(["--config", configPath, "--store", storePath, "--check-handlers"]);
9554
+ if (doctorCode !== 0) return doctorCode;
9555
+ }
9556
+ if (args.includes("--dry-run") || !serveRequested) return boundary.ok ? 0 : 1;
9557
+ if (!boundary.ok) return 1;
9558
+ const serveArgs = [
9559
+ "--config",
9560
+ configPath,
9561
+ "--store",
9562
+ storePath,
9563
+ "--host",
9564
+ optionalArg(args, "--host") ?? "127.0.0.1",
9565
+ "--port",
9566
+ String(port),
9567
+ "--auth-token-env",
9568
+ optionalArg(args, "--auth-token-env") ?? "SYNAPSOR_RUNNER_HTTP_TOKEN",
9569
+ "--alias-mode",
9570
+ aliasMode,
9571
+ ...resultFormat ? ["--result-format", String(resultFormat)] : [],
9572
+ ...args.includes("--dev-no-auth") ? ["--dev-no-auth"] : [],
9573
+ ...optionalArg(args, "--cors-origin") ? ["--cors-origin", optionalArg(args, "--cors-origin")] : [],
9574
+ ...args.includes("--allow-concurrent-store") ? ["--allow-concurrent-store"] : []
9575
+ ];
9576
+ return mcpServeStreamableHttp(serveArgs);
9577
+ }
9578
+ function formatReviewModeUp(input) {
9579
+ const lines = [
9580
+ "Synapsor Runner review-mode up",
9581
+ "",
9582
+ `Config: ${input.configPath}`,
9583
+ `Store: ${input.storePath}`,
9584
+ `Mode: ${input.config.mode}`,
9585
+ `Transport: ${input.transport}`,
9586
+ `Serve now: ${input.serveRequested ? "yes" : "no"}`,
9587
+ `Alias mode: ${input.aliasMode}`,
9588
+ `Result format: ${input.resultFormat ? `v${input.resultFormat}` : configResultFormat(input.config)}`,
9589
+ `Dry run: ${input.dryRun ? "yes" : "no"}`,
9590
+ "",
9591
+ "Model-facing tools:",
9592
+ ...formatUpToolLines(input.boundary),
9593
+ "",
9594
+ "Writeback paths:",
9595
+ ...formatUpWritebackLines(input.config)
9596
+ ];
9597
+ const handlerLines = formatUpHandlerLines(input.config);
9598
+ if (handlerLines.length > 0) {
9599
+ lines.push("", "App-owned handler requirements:", ...handlerLines, "", handlerSecurityWarning);
9600
+ }
9601
+ lines.push("", "Server guidance:");
9602
+ if (input.transport === "stdio") {
9603
+ lines.push(
9604
+ " stdio mode is launched by an MCP client. This command does not hold a protocol session open.",
9605
+ ` Print client config: ${cliCommandName()} mcp client-config --client claude-desktop --config ${input.configPath} --store ${input.storePath}`,
9606
+ ` Serve command used by clients: ${cliCommandName()} mcp serve --config ${input.configPath} --store ${input.storePath} --alias-mode ${input.aliasMode}`
9607
+ );
9608
+ } else {
9609
+ lines.push(
9610
+ ` Streamable HTTP endpoint: http://${input.host}:${input.port}/mcp`,
9611
+ ` Auth token env: ${input.authTokenEnv} (${process2.env[input.authTokenEnv] ? "set" : "missing"})`,
9612
+ input.serveRequested ? input.dryRun ? " Status: dry run only; server not started." : " Status: starting after this checklist." : ` Start command: ${cliCommandName()} up --serve --config ${input.configPath} --store ${input.storePath} --port ${input.port} --auth-token-env ${input.authTokenEnv} --alias-mode ${input.aliasMode}`
9613
+ );
9614
+ }
9615
+ if (input.openUi) {
9616
+ lines.push("", "Local review UI:", ` ${cliCommandName()} ui --open --tour --config ${input.configPath} --store ${input.storePath}`);
9617
+ }
9618
+ lines.push("", "Next commands:", ...formatUpNextCommands(input.config, input.configPath, input.storePath), "");
9619
+ return `${lines.join("\n")}
9620
+ `;
9621
+ }
9622
+ function formatUpToolLines(boundary) {
9623
+ if (boundary.exposures.length === 0) return [" - (none)"];
9624
+ return boundary.exposures.map((item) => item.isAlias ? ` - ${item.exposedName} -> ${item.canonicalName}` : ` - ${item.exposedName}`);
9625
+ }
9626
+ function formatUpWritebackLines(config) {
9627
+ const proposals2 = (config.capabilities ?? []).filter((capability) => capability.kind === "proposal");
9628
+ if (proposals2.length === 0) return [" - no proposal capabilities; this config is read-only from Runner's perspective"];
9629
+ return proposals2.map((capability) => {
9630
+ const executorName = capability.executor ?? "sql_update";
9631
+ if (executorName === "sql_update") {
9632
+ const source = config.sources?.[capability.source];
9633
+ const envName = source?.write_url_env ?? "SYNAPSOR_DATABASE_URL";
9634
+ return ` - ${capability.name}: direct guarded one-row UPDATE via ${envName} (${process2.env[envName] ? "set" : "missing"})`;
9635
+ }
9636
+ const executor = config.executors?.[executorName];
9637
+ return ` - ${capability.name}: app-owned ${String(executor?.type ?? "executor")} ${executorName}`;
9638
+ });
9639
+ }
9640
+ function formatUpHandlerLines(config) {
9641
+ const lines = [];
9642
+ for (const [name, executor] of Object.entries(config.executors ?? {})) {
9643
+ if (!isRecord6(executor)) continue;
9644
+ if (executor.type === "http_handler") {
9645
+ const urlEnv = typeof executor.url_env === "string" ? executor.url_env : "";
9646
+ const auth = isRecord6(executor.auth) ? executor.auth : void 0;
9647
+ const tokenEnv = typeof auth?.token_env === "string" ? auth.token_env : void 0;
9648
+ const signingSecretEnv = typeof executor.signing_secret_env === "string" ? executor.signing_secret_env : void 0;
9649
+ lines.push(` - ${name}: http_handler`);
9650
+ if (urlEnv) lines.push(` url env: ${urlEnv} (${process2.env[urlEnv] ? "set" : "missing"})`);
9651
+ if (tokenEnv) lines.push(` bearer token env: ${tokenEnv} (${process2.env[tokenEnv] ? "set" : "missing"})`);
9652
+ if (signingSecretEnv) lines.push(` signing secret env: ${signingSecretEnv} (${process2.env[signingSecretEnv] ? "set" : "missing"})`);
9653
+ if (!signingSecretEnv) lines.push(" signing secret env: not configured (recommended unless loopback-only)");
9654
+ } else if (executor.type === "command_handler") {
9655
+ const commandEnv = typeof executor.command_env === "string" ? executor.command_env : "";
9656
+ lines.push(` - ${name}: command_handler`);
9657
+ if (commandEnv) lines.push(` command env: ${commandEnv} (${process2.env[commandEnv] ? "set" : "missing"})`);
9658
+ }
9659
+ }
9660
+ return lines;
9661
+ }
9662
+ function configResultFormat(config) {
9663
+ return config.result_format === 2 ? "v2" : config.result_format === 1 ? "v1" : "default";
9664
+ }
9665
+ function formatUpNextCommands(config, configPath, storePath) {
9666
+ const firstTool = (config.capabilities ?? [])[0]?.name ?? "<capability>";
9667
+ const hasHandlers = Object.keys(config.executors ?? {}).length > 0;
9668
+ return [
9669
+ ` - Preview tools: ${cliCommandName()} tools preview --config ${configPath} --store ${storePath}`,
9670
+ ` - Smoke call: ${cliCommandName()} smoke call ${firstTool} --sample --config ${configPath} --store ${storePath}`,
9671
+ ` - List proposals: ${cliCommandName()} proposals list --store ${storePath}`,
9672
+ ` - Show proposal: ${cliCommandName()} proposals show latest --store ${storePath}`,
9673
+ ` - Approve proposal: ${cliCommandName()} proposals approve latest --yes --store ${storePath}`,
9674
+ ` - Apply approved proposal: ${cliCommandName()} apply latest --config ${configPath} --store ${storePath}`,
9675
+ ` - Replay: ${cliCommandName()} replay show latest --store ${storePath}`,
9676
+ ` - Tail events: ${cliCommandName()} events tail --store ${storePath}`,
9677
+ ` - Direct writeback doctor: ${cliCommandName()} doctor --config ${configPath} --check-writeback`,
9678
+ ...hasHandlers ? [` - Handler doctor: ${cliCommandName()} doctor --config ${configPath} --check-handlers`] : []
9679
+ ];
9680
+ }
9134
9681
  async function runnerCommand(args) {
9135
9682
  const [subcommand, ...rest] = args;
9136
9683
  if (subcommand === "start") return start(rest);
9684
+ if (subcommand === "up") return up(rest);
9137
9685
  if (subcommand === "doctor") return doctor(rest);
9138
9686
  usage();
9139
9687
  return 2;
@@ -9172,7 +9720,7 @@ async function cloudConnect(args) {
9172
9720
  return 1;
9173
9721
  }
9174
9722
  const runnerId = String(parsed.cloud.runner_id || process2.env.SYNAPSOR_RUNNER_ID || "synapsor_runner_local").trim();
9175
- const runnerVersion = String(parsed.cloud.runner_version || process2.env.npm_package_version || "0.1.0-alpha.15").trim();
9723
+ const runnerVersion = String(parsed.cloud.runner_version || process2.env.npm_package_version || "0.1.0-alpha.17").trim();
9176
9724
  const engines = normalizeEngines(parsed.cloud.engines);
9177
9725
  const capabilities = normalizeCapabilities(parsed.cloud.capabilities);
9178
9726
  const client = new ControlPlaneClient({
@@ -9277,21 +9825,28 @@ async function handlerTemplate(args) {
9277
9825
  return 0;
9278
9826
  }
9279
9827
  const output = outputArg(args) ?? definition.fileName;
9280
- await writeFileGuarded(output, content, args.includes("--force"));
9281
- if (name === "command" || output.endsWith(".mjs") || output.endsWith(".js")) {
9282
- await fs3.chmod(path3.resolve(output), 493).catch(() => void 0);
9283
- }
9828
+ await writeHandlerTemplateFile(name, output, args.includes("--force"));
9284
9829
  process2.stdout.write(`created ${output}
9285
9830
  `);
9286
- process2.stdout.write("Review the template and add your app-owned transaction, authorization, idempotency, and conflict checks before using it with approved proposals.\n");
9831
+ process2.stdout.write(`${handlerSecurityWarning}
9832
+ `);
9287
9833
  return 0;
9288
9834
  }
9835
+ async function writeHandlerTemplateFile(name, output, force) {
9836
+ const definition = handlerTemplateDefinitions[name];
9837
+ await writeFileGuarded(output, definition.content, force);
9838
+ if (name === "command" || output.endsWith(".mjs") || output.endsWith(".js")) {
9839
+ await fs3.chmod(path3.resolve(output), 493).catch(() => void 0);
9840
+ }
9841
+ }
9289
9842
  function formatHandlerTemplateList() {
9290
9843
  return [
9291
9844
  "Synapsor app-owned writeback handler templates",
9292
9845
  "",
9293
9846
  ...Object.entries(handlerTemplateDefinitions).map(([name, definition]) => `- ${name}: ${definition.description}`),
9294
9847
  "",
9848
+ handlerSecurityWarning,
9849
+ "",
9295
9850
  "Examples:",
9296
9851
  ` ${cliCommandName()} handler template node-fastify --output ./synapsor-writeback-handler.mjs`,
9297
9852
  ` ${cliCommandName()} handler template python-fastapi --output ./synapsor_writeback_handler.py`,
@@ -9390,8 +9945,10 @@ async function onboard(args) {
9390
9945
  process2.stdout.write("You will inspect metadata, choose one table/view, confirm safety rules, and generate semantic MCP tools without writing JSON by hand.\n\n");
9391
9946
  const outputPath = outputArg(rest) ?? "synapsor.runner.json";
9392
9947
  const storePath = optionalArg(rest, "--store") ?? "./.synapsor/local.db";
9393
- const result = await runInitWizard(["--wizard", ...rest]);
9948
+ const scripted = isScriptedOnboardingArgs(rest);
9949
+ const result = scripted ? await init(["--non-interactive", ...rest]) : await runInitWizard(["--wizard", ...rest]);
9394
9950
  if (result !== 0) return result;
9951
+ if (rest.includes("--dry-run")) return 0;
9395
9952
  process2.stdout.write("\nValidation:\n");
9396
9953
  const configCode = await configValidate(["--config", outputPath]);
9397
9954
  const smokeCode = await mcpSmoke(["--config", outputPath, "--store", storePath]);
@@ -10040,7 +10597,7 @@ async function assertNoActiveStoreLease(storePath, force, operation) {
10040
10597
  await fs3.rm(storeLeasePath(resolved), { force: true });
10041
10598
  return;
10042
10599
  }
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.`;
10600
+ 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 --allow-concurrent-store/--force if you have verified it is safe.`;
10044
10601
  if (!force) throw new Error(message);
10045
10602
  process2.stderr.write(`Warning: ${message}
10046
10603
  `);
@@ -10104,6 +10661,12 @@ function resultFormatOption(args) {
10104
10661
  if (requested === "2" || requested === "v2") return 2;
10105
10662
  throw new Error("--result-format must be v1, 1, v2, or 2");
10106
10663
  }
10664
+ function normalizeResultFormatAnswer(value) {
10665
+ if (value === "1" || value === "v1") return "v1";
10666
+ if (value === "2" || value === "v2") return "v2";
10667
+ if (value === "default") return "default";
10668
+ throw new Error("--result-format must be default, v1, 1, v2, or 2");
10669
+ }
10107
10670
  async function mcpAudit(args) {
10108
10671
  const format = optionalArg(args, "--format") ?? (args.includes("--json") ? "json" : "text");
10109
10672
  if (!["text", "json", "markdown"].includes(format)) {
@@ -11126,6 +11689,7 @@ async function activity(args) {
11126
11689
  async function events(args) {
11127
11690
  const [subcommand, ...rest] = args;
11128
11691
  if (subcommand === "tail") return eventsTail(rest);
11692
+ if (subcommand === "webhook" || subcommand === "push") return eventsWebhook(rest);
11129
11693
  usage(["events"]);
11130
11694
  return 2;
11131
11695
  }
@@ -11700,6 +12264,75 @@ async function eventsTail(args) {
11700
12264
  });
11701
12265
  return 0;
11702
12266
  }
12267
+ async function eventsWebhook(args) {
12268
+ assertKnownOptions(args, eventWebhookAllowedOptions, "events webhook");
12269
+ const url = optionalArg(args, "--url") ?? envValue(optionalArg(args, "--url-env"));
12270
+ if (!url) throw new Error("events webhook requires --url <https://...> or --url-env <ENV>");
12271
+ const endpoint = new URL(url);
12272
+ if (!["http:", "https:"].includes(endpoint.protocol)) throw new Error("events webhook URL must use http or https");
12273
+ const follow = args.includes("--follow");
12274
+ const dryRun = args.includes("--dry-run");
12275
+ const jsonOutput = args.includes("--json");
12276
+ if (follow && jsonOutput) throw new Error("events webhook --follow does not support --json");
12277
+ const storePath = optionalArg(args, "--store");
12278
+ const intervalMs = Number(optionalArg(args, "--interval-ms") ?? "1000");
12279
+ const timeoutMs = Number(optionalArg(args, "--timeout-ms") ?? "5000");
12280
+ const sinceEventId = optionalPositiveIntegerArg(args, "--since-event-id");
12281
+ if (!Number.isFinite(intervalMs) || intervalMs < 250) throw new Error("--interval-ms must be at least 250");
12282
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 250) throw new Error("--timeout-ms must be at least 250");
12283
+ const token = envValue(optionalArg(args, "--auth-token-env"));
12284
+ const filters = eventFiltersFromArgs(args);
12285
+ const seen = /* @__PURE__ */ new Set();
12286
+ const pushOnce = async () => {
12287
+ const store = await openLocalStore(["--store", storePath ?? "./.synapsor/local.db"]);
12288
+ try {
12289
+ const rows = store.listEvents(filters).filter((event) => sinceEventId === void 0 || event.event_id > sinceEventId).sort((left, right) => left.event_id - right.event_id).filter((event) => !seen.has(event.event_id));
12290
+ rows.forEach((event) => seen.add(event.event_id));
12291
+ let delivered = 0;
12292
+ const payloads = [];
12293
+ for (const event of rows) {
12294
+ const payload = localEventWebhookPayload(event, store.path);
12295
+ payloads.push(payload);
12296
+ if (dryRun) {
12297
+ if (!jsonOutput) process2.stdout.write(`${JSON.stringify(payload, null, 2)}
12298
+ `);
12299
+ } else {
12300
+ await postLocalEventWebhook(endpoint, payload, { token, timeoutMs });
12301
+ if (!jsonOutput) process2.stdout.write(`pushed event ${event.event_id} ${event.kind} for ${event.proposal_id} to ${redactWebhookUrl(endpoint)}
12302
+ `);
12303
+ }
12304
+ delivered += 1;
12305
+ }
12306
+ if (rows.length === 0 && !follow && !jsonOutput) process2.stdout.write(dryRun ? "No local events matched for dry-run.\n" : "No local events matched.\n");
12307
+ return { delivered, payloads };
12308
+ } finally {
12309
+ store.close();
12310
+ }
12311
+ };
12312
+ const first = await pushOnce();
12313
+ if (jsonOutput && !follow) {
12314
+ process2.stdout.write(`${JSON.stringify({ ok: true, dry_run: dryRun, delivered: first.delivered, webhook: redactWebhookUrl(endpoint), events: first.payloads }, null, 2)}
12315
+ `);
12316
+ }
12317
+ if (!follow) return 0;
12318
+ await new Promise((resolve) => {
12319
+ const timer = setInterval(() => {
12320
+ void pushOnce().catch((error) => {
12321
+ process2.stderr.write(`events webhook error: ${safeErrorMessage(error)}
12322
+ `);
12323
+ });
12324
+ }, intervalMs);
12325
+ const stop = () => {
12326
+ clearInterval(timer);
12327
+ process2.off("SIGINT", stop);
12328
+ process2.off("SIGTERM", stop);
12329
+ resolve();
12330
+ };
12331
+ process2.once("SIGINT", stop);
12332
+ process2.once("SIGTERM", stop);
12333
+ });
12334
+ return 0;
12335
+ }
11703
12336
  async function storeStats(args) {
11704
12337
  assertKnownOptions(args, storeStatsAllowedOptions, "store stats");
11705
12338
  const store = await openLocalStore(args);
@@ -11849,6 +12482,15 @@ var eventTailAllowedOptions = /* @__PURE__ */ new Set([
11849
12482
  "--follow",
11850
12483
  "--interval-ms"
11851
12484
  ]);
12485
+ var eventWebhookAllowedOptions = /* @__PURE__ */ new Set([
12486
+ ...eventTailAllowedOptions,
12487
+ "--url",
12488
+ "--url-env",
12489
+ "--auth-token-env",
12490
+ "--timeout-ms",
12491
+ "--since-event-id",
12492
+ "--dry-run"
12493
+ ]);
11852
12494
  var replayShowAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions, "--proposal", "--replay", "--evidence"]);
11853
12495
  var replayExportAllowedOptions = /* @__PURE__ */ new Set([...replayShowAllowedOptions, "--output", "--out", "--format"]);
11854
12496
  var replayListAllowedOptions = /* @__PURE__ */ new Set([
@@ -12051,6 +12693,17 @@ function eventFiltersFromArgs(args) {
12051
12693
  limit: limitFromArgs(args)
12052
12694
  };
12053
12695
  }
12696
+ function optionalPositiveIntegerArg(args, flag) {
12697
+ const value = optionalArg(args, flag);
12698
+ if (!value) return void 0;
12699
+ const parsed = Number(value);
12700
+ if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`${flag} must be a non-negative integer`);
12701
+ return parsed;
12702
+ }
12703
+ function envValue(name) {
12704
+ if (!name) return void 0;
12705
+ return process2.env[name];
12706
+ }
12054
12707
  function linkedProposalFilter(args, store, options = {}) {
12055
12708
  const noLinkedProposal = "__synapsor_no_linked_proposal__";
12056
12709
  const replay2 = optionalArg(args, "--replay");
@@ -12231,8 +12884,8 @@ async function prepareReferenceDemo(args) {
12231
12884
  ].join("\n"));
12232
12885
  const down = spawnSync("docker", ["compose", "-f", composePath, "down", "-v", "--remove-orphans"], { stdio: "inherit", env: process2.env });
12233
12886
  if (down.status !== 0) return down.status ?? 1;
12234
- const up = spawnSync("docker", ["compose", "-f", composePath, "up", "-d"], { stdio: "inherit", env: process2.env });
12235
- if (up.status !== 0) return up.status ?? 1;
12887
+ const up2 = spawnSync("docker", ["compose", "-f", composePath, "up", "-d"], { stdio: "inherit", env: process2.env });
12888
+ if (up2.status !== 0) return up2.status ?? 1;
12236
12889
  await waitForReferenceDemoDatabase();
12237
12890
  await fs3.copyFile(path3.join(demoDir, "synapsor.runner.json"), configPath);
12238
12891
  process2.stdout.write([
@@ -12384,6 +13037,10 @@ function firstPositional(args) {
12384
13037
  "--reason",
12385
13038
  "--recipe",
12386
13039
  "--receipt",
13040
+ "--read-tool",
13041
+ "--inspect-tool-name",
13042
+ "--proposal-tool",
13043
+ "--proposal-tool-name",
12387
13044
  "--replay",
12388
13045
  "--runner",
12389
13046
  "--schema",
@@ -13024,6 +13681,48 @@ function formatEventLine(event, details = false) {
13024
13681
  return `${lines.join("\n")}
13025
13682
  `;
13026
13683
  }
13684
+ function localEventWebhookPayload(event, storePath) {
13685
+ return {
13686
+ schema_version: "synapsor.local-event-webhook.v1",
13687
+ delivered_at: (/* @__PURE__ */ new Date()).toISOString(),
13688
+ source: {
13689
+ kind: "local_store",
13690
+ store_path: storePath
13691
+ },
13692
+ event
13693
+ };
13694
+ }
13695
+ async function postLocalEventWebhook(endpoint, payload, options) {
13696
+ const controller = new AbortController();
13697
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
13698
+ try {
13699
+ const headers = {
13700
+ "content-type": "application/json",
13701
+ accept: "application/json",
13702
+ "user-agent": "synapsor-runner-events-webhook"
13703
+ };
13704
+ if (options.token) headers.authorization = `Bearer ${options.token}`;
13705
+ const response = await fetch(endpoint, {
13706
+ method: "POST",
13707
+ headers,
13708
+ body: JSON.stringify(payload),
13709
+ signal: controller.signal
13710
+ });
13711
+ if (!response.ok) {
13712
+ const text = await response.text().catch(() => "");
13713
+ throw new Error(`webhook returned HTTP ${response.status}${text ? `: ${text.slice(0, 200)}` : ""}`);
13714
+ }
13715
+ } finally {
13716
+ clearTimeout(timeout);
13717
+ }
13718
+ }
13719
+ function redactWebhookUrl(endpoint) {
13720
+ const copy = new URL(endpoint.toString());
13721
+ copy.username = "";
13722
+ copy.password = "";
13723
+ copy.search = "";
13724
+ return copy.toString();
13725
+ }
13027
13726
  function safeErrorMessage(error) {
13028
13727
  return error instanceof Error ? error.message : String(error);
13029
13728
  }
@@ -13311,7 +14010,7 @@ function starterCloudConfig() {
13311
14010
  base_url_env: "SYNAPSOR_CLOUD_BASE_URL",
13312
14011
  runner_token_env: "SYNAPSOR_RUNNER_TOKEN",
13313
14012
  runner_id: "synapsor_runner_local",
13314
- runner_version: "0.1.0-alpha.15",
14013
+ runner_version: "0.1.0-alpha.17",
13315
14014
  project_id: "token_scope",
13316
14015
  adapter_id: "mcp.your_adapter",
13317
14016
  source_id: "src_replace_me",
@@ -13346,6 +14045,7 @@ function isKnownTopLevelCommand(command) {
13346
14045
  "propose",
13347
14046
  "audit",
13348
14047
  "start",
14048
+ "up",
13349
14049
  "runner",
13350
14050
  "cloud",
13351
14051
  "mcp",
@@ -13388,6 +14088,7 @@ Usage:
13388
14088
  Commands:
13389
14089
  inspect Inspect a Postgres/MySQL schema
13390
14090
  start Start guided own-database setup, or no-arg legacy worker polling
14091
+ up Bring up local review mode guidance/server
13391
14092
  init Generate a Synapsor capability contract
13392
14093
  mcp Serve safe semantic tools over MCP
13393
14094
  onboard One-command own-database setup
@@ -13402,7 +14103,7 @@ Commands:
13402
14103
  query-audit Inspect local query audit records
13403
14104
  receipts Inspect guarded writeback receipts
13404
14105
  activity Search local evidence/replay ledger
13405
- events Tail local proposal/writeback lifecycle events
14106
+ events Tail or push local proposal/writeback lifecycle events
13406
14107
  store Inspect and maintain the local SQLite ledger
13407
14108
  apply Apply an approved proposal with guarded writeback
13408
14109
  replay Show what happened
@@ -13411,6 +14112,7 @@ Commands:
13411
14112
 
13412
14113
  Examples:
13413
14114
  ${cmd} start --from-env DATABASE_URL
14115
+ ${cmd} up --config ./synapsor.runner.json --store ./.synapsor/local.db --dry-run
13414
14116
  ${cmd} onboard db --from-env DATABASE_URL
13415
14117
  ${cmd} inspect --from-env DATABASE_URL
13416
14118
  ${cmd} init --wizard --from-env DATABASE_URL
@@ -13420,6 +14122,29 @@ Examples:
13420
14122
  ${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db
13421
14123
  ${cmd} propose billing.propose_late_fee_waiver --sample
13422
14124
  ${cmd} audit ./synapsor.runner.json
14125
+ `,
14126
+ up: `Usage:
14127
+ ${cmd} up --config ./synapsor.runner.json --store ./.synapsor/local.db [--transport stdio|streamable-http]
14128
+ ${cmd} up --serve --config ./synapsor.runner.json --store ./.synapsor/local.db --port 8766 --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
14129
+ ${cmd} up --config ./synapsor.runner.json --store ./.synapsor/local.db --handler-check --dry-run
14130
+
14131
+ Validate the local Runner config and store, summarize model-facing tools,
14132
+ explain direct SQL versus app-owned executor writeback, and print the next
14133
+ smoke/approve/apply/replay commands.
14134
+
14135
+ With --transport stdio, \`${cmd} up\` prints MCP client wiring because stdio is
14136
+ launched by the client. \`${cmd} up --serve\` starts the standard Streamable HTTP
14137
+ MCP server after the checklist. Use --with-handler to run the handler doctor
14138
+ before serving app-owned writeback configs.
14139
+
14140
+ Options:
14141
+ --serve
14142
+ --alias-mode canonical|openai|both
14143
+ --result-format v1|v2
14144
+ --handler-check
14145
+ --with-handler
14146
+ --open-ui
14147
+ --dry-run
13423
14148
  `,
13424
14149
  start: `Usage:
13425
14150
  ${cmd} start --from-env DATABASE_URL [--schema public] [--mode read_only|shadow|review]
@@ -13444,12 +14169,17 @@ Inspect schema metadata without mutating the database or printing credentials.
13444
14169
  init: `Usage:
13445
14170
  ${cmd} init --wizard --from-env DATABASE_URL [--mode read_only|review|shadow] [--out synapsor.runner.json]
13446
14171
  ${cmd} init --engine postgres --url-env DATABASE_URL --mode review --table public.invoices
13447
- ${cmd} init --inspection-json schema.json --table invoices --mode review --patch-from-arg waiver_reason=reason
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]
14172
+ ${cmd} init --inspection-json schema.json --table invoices --mode review --patch late_fee_cents=fixed:0,waiver_reason=arg:reason
14173
+ ${cmd} init --answers answers.json --yes
14174
+ ${cmd} init --inspection-json schema.json --table invoices --mode review --writeback http_handler --handler-url-env APP_WRITEBACK_URL --emit-handler [--handler-signing-secret-env APP_WRITEBACK_SIGNING_SECRET]
13449
14175
 
13450
14176
  Generate a reviewed Synapsor Runner contract. Defaults to read-only in the wizard.
13451
14177
  Review mode writeback choices: sql_update, http_handler, command_handler.
13452
- `,
14178
+ If --namespace is omitted, init derives one from the table name instead of using source.*.
14179
+ Use --read-tool and --proposal-tool to override the exact model-facing capability names.
14180
+ The guided wizard shows a final preview and lets you revise visible fields or capability names before writing files.
14181
+ Use --yes/--non-interactive plus explicit flags, or --answers, for script/agent-friendly setup without prompts.
14182
+ `,
13453
14183
  mcp: `Usage:
13454
14184
  ${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db
13455
14185
  ${cmd} mcp serve --transport streamable-http --config ./synapsor.runner.json --store ./.synapsor/local.db --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
@@ -13576,9 +14306,12 @@ The template receives an approved proposal writeback request and must return an
13576
14306
  `,
13577
14307
  onboard: `Usage:
13578
14308
  ${cmd} onboard db --from-env DATABASE_URL [--schema public] [--mode read_only|shadow|review]
13579
- ${cmd} onboard db --from-env DATABASE_URL --mode review --writeback http_handler --handler-url-env APP_WRITEBACK_URL
14309
+ ${cmd} onboard db --from-env DATABASE_URL --table invoices --mode review --patch late_fee_cents=fixed:0 --write-url-env SYNAPSOR_DATABASE_WRITE_URL --yes
14310
+ ${cmd} onboard db --from-env DATABASE_URL --table invoices --mode review --writeback http_handler --handler-url-env APP_WRITEBACK_URL --emit-handler --yes
14311
+ ${cmd} onboard db --answers answers.json --yes
13580
14312
 
13581
14313
  Guided own-database setup: inspect schema, choose one object, create trusted context, choose read-only/shadow/review writeback mode, generate semantic tools, validate config, run a tool-boundary smoke check, and print the MCP/UI next steps.
14314
+ Use --yes/--non-interactive with explicit flags, or --answers, when CI or an LLM agent must run without prompts.
13582
14315
  `,
13583
14316
  propose: `Usage:
13584
14317
  ${cmd} propose <capability-name> --sample [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
@@ -13682,12 +14415,15 @@ Search the local SQLite evidence/replay ledger across proposals, evidence, query
13682
14415
  `,
13683
14416
  events: `Usage:
13684
14417
  ${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
14418
+ ${cmd} events tail --proposal wrp_...
14419
+ ${cmd} events tail --kind writeback_applied
14420
+ ${cmd} events tail --follow --interval-ms 1000
14421
+ ${cmd} events webhook --url http://127.0.0.1:8788/synapsor/events --kind proposal_created
14422
+ ${cmd} events webhook --url-env SYNAPSOR_EVENT_WEBHOOK_URL --auth-token-env SYNAPSOR_EVENT_WEBHOOK_TOKEN --follow
14423
+ ${cmd} events webhook --url http://127.0.0.1:8788/synapsor/events --dry-run
13688
14424
 
13689
- Show local proposal/writeback lifecycle events such as proposal_created, proposal_approved, writeback_applied, writeback_conflict, and writeback_failed.
13690
- `,
14425
+ Show or push local proposal/writeback lifecycle events such as proposal_created, proposal_approved, writeback_applied, writeback_conflict, and writeback_failed. Webhook delivery POSTs one local event envelope per event and never exposes database credentials.
14426
+ `,
13691
14427
  store: `Usage:
13692
14428
  ${cmd} store stats --store ./.synapsor/local.db
13693
14429
  ${cmd} store vacuum --store ./.synapsor/local.db