@synapsor/runner 0.1.0-alpha.15 → 0.1.0-alpha.16
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/CHANGELOG.md +24 -6
- package/README.md +23 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/runner.mjs +352 -11
- package/docs/app-owned-executors.md +7 -0
- package/docs/handler-helper.md +7 -0
- package/docs/release-notes.md +35 -1
- package/docs/release-policy.md +15 -1
- package/docs/writeback-executors.md +13 -0
- package/examples/app-owned-writeback/README.md +7 -0
- package/examples/app-owned-writeback/command-handler.mjs +9 -0
- package/examples/app-owned-writeback/node-fastify-handler.mjs +9 -0
- package/examples/app-owned-writeback/python-fastapi-handler.py +9 -0
- package/examples/mcp-postgres-billing-app-handler/README.md +7 -0
- package/examples/mcp-postgres-billing-app-handler/app-handler.mjs +6 -0
- package/package.json +1 -1
- package/schemas/onboarding-selection.v1.schema.json +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,13 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
No unreleased changes yet.
|
|
6
|
+
|
|
7
|
+
## 0.1.0-alpha.16
|
|
8
|
+
|
|
5
9
|
### Added
|
|
6
10
|
|
|
7
|
-
- `
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
- `synapsor-runner up` for first-session review-mode bring-up. It validates
|
|
12
|
+
the local config/store, checks active store leases, summarizes model-facing
|
|
13
|
+
tools, explains direct SQL versus app-owned executor writeback, and prints
|
|
14
|
+
the next smoke, approval, apply, replay, UI, and doctor commands.
|
|
15
|
+
- Guided app-owned executor setup can now write a starter handler template
|
|
16
|
+
during `init --wizard` / `start --from-env ... --mode review`.
|
|
17
|
+
- `result_format: 2` for a stable MCP result envelope with `ok`, `summary`,
|
|
18
|
+
`data`, `proposal`, `error`, `evidence`, `source_database_changed`, and
|
|
19
|
+
`_meta.canonical_capability`.
|
|
10
20
|
- `--result-format v1|v2` for `mcp serve`, `mcp serve --transport
|
|
11
|
-
streamable-http`, `mcp serve-streamable-http`, and the legacy JSON-RPC
|
|
21
|
+
streamable-http`, `mcp serve-streamable-http`, and the legacy JSON-RPC
|
|
22
|
+
bridge.
|
|
12
23
|
- Capability config fields `description`, per-argument `description`, and
|
|
13
24
|
`returns_hint`; these are surfaced in MCP tool metadata.
|
|
14
25
|
- `tools list` as a first-class alias for `tools preview`, including
|
|
@@ -16,16 +27,23 @@
|
|
|
16
27
|
- `mcp client-config --include-instructions` for Claude/Cursor/OpenAI-style
|
|
17
28
|
client snippets with propose-first agent guidance.
|
|
18
29
|
- `schemas/synapsor.runner.schema.json` for editor validation.
|
|
19
|
-
- `docs/capability-authoring.md
|
|
20
|
-
|
|
30
|
+
- `docs/capability-authoring.md`, `docs/result-envelope-v2.md`, and RFC source
|
|
31
|
+
docs under `docs/rfcs/`.
|
|
21
32
|
|
|
22
33
|
### Changed
|
|
23
34
|
|
|
35
|
+
- Handler templates, template CLI output, app-owned writeback docs, and
|
|
36
|
+
examples now carry the explicit handler security warning: app handlers own the
|
|
37
|
+
final business write and must re-check tenant/scope, conflict guards,
|
|
38
|
+
idempotency, business action, transactions, and safe receipts.
|
|
24
39
|
- OpenAI-safe aliases include the canonical Synapsor capability name in
|
|
25
40
|
descriptions/metadata so model-visible aliases can still be audited against
|
|
26
41
|
dotted capability names.
|
|
27
42
|
- v2 MCP errors redact raw driver/infra strings and map failures to a small
|
|
28
43
|
safe error-code enum.
|
|
44
|
+
- Release policy now keeps the stable channel gated on `up`, review-mode wizard
|
|
45
|
+
verification, handler warning coverage, clean npm install checks, and at
|
|
46
|
+
least one external developer following the README without source reading.
|
|
29
47
|
|
|
30
48
|
### Compatibility
|
|
31
49
|
|
package/README.md
CHANGED
|
@@ -223,6 +223,21 @@ The end-to-end shape is:
|
|
|
223
223
|
The generated config is just the safety contract. A small reviewed version
|
|
224
224
|
looks like this:
|
|
225
225
|
|
|
226
|
+
Bring the generated review-mode workspace up with one command:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
npx -y -p @synapsor/runner@alpha synapsor-runner up \
|
|
230
|
+
--config ./synapsor.runner.json \
|
|
231
|
+
--store ./.synapsor/local.db \
|
|
232
|
+
--dry-run
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`up` validates the config/store, summarizes model-facing tools, shows whether
|
|
236
|
+
proposal tools use direct SQL writeback or app-owned executors, checks active
|
|
237
|
+
store leases, and prints the next smoke, approve, apply, replay, UI, and doctor
|
|
238
|
+
commands. Use `--transport streamable-http` when you want `up` to start the
|
|
239
|
+
standard HTTP MCP server.
|
|
240
|
+
|
|
226
241
|
```json
|
|
227
242
|
{
|
|
228
243
|
"version": 1,
|
|
@@ -588,6 +603,14 @@ Use direct guarded DB writeback for simple local/staging single-row updates. If
|
|
|
588
603
|
your application service already owns business writes, configure an
|
|
589
604
|
`http_handler` or `command_handler` executor. Approval still happens outside
|
|
590
605
|
MCP, and the handler returns an applied/conflict/failed receipt for replay.
|
|
606
|
+
|
|
607
|
+
> **Important:** your app handler owns the final business write. Runner creates
|
|
608
|
+
> the proposal and calls your handler only after approval, but your handler must
|
|
609
|
+
> still enforce tenant/scope checks, expected-version or conflict guards,
|
|
610
|
+
> idempotency keys, allowed business actions, transaction/rollback, and safe
|
|
611
|
+
> error receipts. If you skip those checks, you can reintroduce cross-tenant
|
|
612
|
+
> writes, lost updates, or duplicate writes. Keep handler credentials out of MCP.
|
|
613
|
+
|
|
591
614
|
Starter handlers are included under `examples/app-owned-writeback`.
|
|
592
615
|
The packaged app-owned billing example also includes a bundled
|
|
593
616
|
`synapsor-handler.mjs` helper shim; `@synapsor/handler` is not published as a
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAYA,OAAO,EAAqG,KAAK,WAAW,EAA2F,MAAM,6BAA6B,CAAC;AAmB3P,OAAO,EAA6G,KAAK,YAAY,EAAwB,MAAM,2BAA2B,CAAC;AAC/L,OAAO,EAOL,KAAK,gBAAgB,EAEtB,MAAM,mCAAmC,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAYA,OAAO,EAAqG,KAAK,WAAW,EAA2F,MAAM,6BAA6B,CAAC;AAmB3P,OAAO,EAA6G,KAAK,YAAY,EAAwB,MAAM,2BAA2B,CAAC;AAC/L,OAAO,EAOL,KAAK,gBAAgB,EAEtB,MAAM,mCAAmC,CAAC;AA4U3C,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAmD1D;AA0DD,KAAK,SAAS,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAE9E,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,GAAE;IACP,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;CACvC,GACL,OAAO,CAAC,MAAM,CAAC,CA+RjB;AAivDD,wBAAsB,0BAA0B,CAAC,GAAG,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAM/H"}
|
package/dist/runner.mjs
CHANGED
|
@@ -4525,15 +4525,18 @@ function generateRunnerConfigFromSpec(spec) {
|
|
|
4525
4525
|
const lookupArg = spec.lookup_arg ?? `${objectName}_id`;
|
|
4526
4526
|
const inspectToolName = spec.inspect_tool_name ?? `${spec.namespace}.inspect_${objectName}`;
|
|
4527
4527
|
const proposalToolName = spec.proposal_tool_name ?? `${spec.namespace}.propose_${objectName}_update`;
|
|
4528
|
+
const objectLabel = objectName.replace(/_/g, " ");
|
|
4528
4529
|
const visibleColumns = unique([spec.primary_key, spec.tenant_key, spec.conflict_column, ...spec.visible_columns].filter((value) => Boolean(value)));
|
|
4529
4530
|
const readCapability = {
|
|
4530
4531
|
name: inspectToolName,
|
|
4531
4532
|
kind: "read",
|
|
4533
|
+
description: spec.inspect_description ?? `Inspect one ${objectLabel} in trusted tenant scope before answering or proposing a change.`,
|
|
4534
|
+
returns_hint: spec.inspect_returns_hint ?? `Returns reviewed ${objectLabel} fields, evidence handle, query audit, and source_database_changed:false.`,
|
|
4532
4535
|
source: sourceName,
|
|
4533
4536
|
context: "local_operator",
|
|
4534
4537
|
target: target(spec),
|
|
4535
4538
|
args: {
|
|
4536
|
-
[lookupArg]: { type: "string", required: true, max_length: 128 }
|
|
4539
|
+
[lookupArg]: { type: "string", required: true, max_length: 128, description: `${capitalize(objectLabel)} id from the user request or trusted app context.` }
|
|
4537
4540
|
},
|
|
4538
4541
|
lookup: { id_from_arg: lookupArg },
|
|
4539
4542
|
visible_columns: visibleColumns,
|
|
@@ -4546,6 +4549,8 @@ function generateRunnerConfigFromSpec(spec) {
|
|
|
4546
4549
|
capabilities.push({
|
|
4547
4550
|
name: proposalToolName,
|
|
4548
4551
|
kind: "proposal",
|
|
4552
|
+
description: spec.proposal_description ?? `Create a review-required proposal to update one ${objectLabel}. The source database remains unchanged until approval and writeback.`,
|
|
4553
|
+
returns_hint: spec.proposal_returns_hint ?? "Returns a proposal id, exact before/after diff, evidence handle, approval status, and source_database_changed:false.",
|
|
4549
4554
|
source: sourceName,
|
|
4550
4555
|
context: "local_operator",
|
|
4551
4556
|
...writeback2.executor !== "sql_update" ? { executor: writeback2.executorName } : {},
|
|
@@ -4569,6 +4574,7 @@ function generateRunnerConfigFromSpec(spec) {
|
|
|
4569
4574
|
const config = {
|
|
4570
4575
|
version: 1,
|
|
4571
4576
|
mode,
|
|
4577
|
+
...spec.result_format ? { result_format: spec.result_format } : {},
|
|
4572
4578
|
storage: { sqlite_path: "./.synapsor/local.db" },
|
|
4573
4579
|
sources: {
|
|
4574
4580
|
[sourceName]: {
|
|
@@ -5017,6 +5023,7 @@ function validateSelectionSpec(spec) {
|
|
|
5017
5023
|
if (spec.version !== void 0 && spec.version !== 1) throw new Error("onboarding selection version must be 1.");
|
|
5018
5024
|
if (spec.engine !== "postgres" && spec.engine !== "mysql") throw new Error("selection engine must be postgres or mysql.");
|
|
5019
5025
|
if (!["read_only", "shadow", "review", void 0].includes(spec.mode)) throw new Error("selection mode must be read_only, shadow, or review.");
|
|
5026
|
+
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
5027
|
if (spec.writeback?.executor && !["sql_update", "http_handler", "command_handler"].includes(spec.writeback.executor)) {
|
|
5021
5028
|
throw new Error("selection writeback.executor must be sql_update, http_handler, or command_handler.");
|
|
5022
5029
|
}
|
|
@@ -5121,6 +5128,10 @@ function singularize(value) {
|
|
|
5121
5128
|
function safeName(value) {
|
|
5122
5129
|
return value.toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "record";
|
|
5123
5130
|
}
|
|
5131
|
+
function capitalize(value) {
|
|
5132
|
+
if (!value) return value;
|
|
5133
|
+
return `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}`;
|
|
5134
|
+
}
|
|
5124
5135
|
function assertSafeIdentifier(identifier) {
|
|
5125
5136
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
5126
5137
|
throw new Error(`unsafe identifier in selection: ${identifier}`);
|
|
@@ -6573,6 +6584,19 @@ var defaultConfigPath = "synapsor.runner.json";
|
|
|
6573
6584
|
var defaultStorePath = "./.synapsor/local.db";
|
|
6574
6585
|
var quickDemoStorePath = "./.synapsor/quick-demo.db";
|
|
6575
6586
|
var generatedSmokeInputPath = "./.synapsor/smoke-input.json";
|
|
6587
|
+
var handlerSecurityWarning = [
|
|
6588
|
+
"IMPORTANT: your app handler owns the final business write.",
|
|
6589
|
+
"Runner creates the proposal and calls your handler only after approval, but your handler must still enforce:",
|
|
6590
|
+
"- tenant/scope check;",
|
|
6591
|
+
"- expected-version or conflict guard;",
|
|
6592
|
+
"- idempotency key;",
|
|
6593
|
+
"- allowed business action;",
|
|
6594
|
+
"- transaction/rollback;",
|
|
6595
|
+
"- safe error receipt.",
|
|
6596
|
+
"",
|
|
6597
|
+
"If you skip those checks, you can reintroduce cross-tenant writes, lost updates, or duplicate writes.",
|
|
6598
|
+
"Use the generated template/helper pattern and keep handler credentials out of MCP."
|
|
6599
|
+
].join("\n");
|
|
6576
6600
|
var handlerTemplateDefinitions = {
|
|
6577
6601
|
"node-fastify": {
|
|
6578
6602
|
aliases: ["node", "fastify"],
|
|
@@ -6608,6 +6632,15 @@ app.post("/synapsor/writeback", async (request, reply) => {
|
|
|
6608
6632
|
}
|
|
6609
6633
|
|
|
6610
6634
|
/*
|
|
6635
|
+
* IMPORTANT: your app handler owns the final business write.
|
|
6636
|
+
* Runner creates the proposal and calls your handler only after approval,
|
|
6637
|
+
* but your handler must still enforce tenant/scope, expected-version or
|
|
6638
|
+
* conflict guard, idempotency key, allowed business action,
|
|
6639
|
+
* transaction/rollback, and safe error receipt.
|
|
6640
|
+
*
|
|
6641
|
+
* If you skip those checks, you can reintroduce cross-tenant writes,
|
|
6642
|
+
* lost updates, or duplicate writes. Keep handler credentials out of MCP.
|
|
6643
|
+
*
|
|
6611
6644
|
* Put your app-owned transaction here.
|
|
6612
6645
|
*
|
|
6613
6646
|
* Examples:
|
|
@@ -6665,6 +6698,15 @@ async def synapsor_writeback(body: dict, authorization: str | None = Header(defa
|
|
|
6665
6698
|
|
|
6666
6699
|
# Put your app-owned transaction here.
|
|
6667
6700
|
#
|
|
6701
|
+
# IMPORTANT: your app handler owns the final business write.
|
|
6702
|
+
# Runner creates the proposal and calls your handler only after approval,
|
|
6703
|
+
# but your handler must still enforce tenant/scope, expected-version or
|
|
6704
|
+
# conflict guard, idempotency key, allowed business action,
|
|
6705
|
+
# transaction/rollback, and safe error receipt.
|
|
6706
|
+
#
|
|
6707
|
+
# If you skip those checks, you can reintroduce cross-tenant writes,
|
|
6708
|
+
# lost updates, or duplicate writes. Keep handler credentials out of MCP.
|
|
6709
|
+
#
|
|
6668
6710
|
# Examples:
|
|
6669
6711
|
# - insert a refund_review row;
|
|
6670
6712
|
# - insert an account_credit row;
|
|
@@ -6716,6 +6758,15 @@ if (request.dry_run) {
|
|
|
6716
6758
|
}
|
|
6717
6759
|
|
|
6718
6760
|
/*
|
|
6761
|
+
* IMPORTANT: your app handler owns the final business write.
|
|
6762
|
+
* Runner creates the proposal and calls your handler only after approval,
|
|
6763
|
+
* but your handler must still enforce tenant/scope, expected-version or
|
|
6764
|
+
* conflict guard, idempotency key, allowed business action,
|
|
6765
|
+
* transaction/rollback, and safe error receipt.
|
|
6766
|
+
*
|
|
6767
|
+
* If you skip those checks, you can reintroduce cross-tenant writes,
|
|
6768
|
+
* lost updates, or duplicate writes. Keep handler credentials out of MCP.
|
|
6769
|
+
*
|
|
6719
6770
|
* Put your app-owned command transaction here.
|
|
6720
6771
|
*
|
|
6721
6772
|
* Examples:
|
|
@@ -6784,6 +6835,7 @@ ${cliCommandName()} --help
|
|
|
6784
6835
|
if (command === "propose") return propose(rest);
|
|
6785
6836
|
if (command === "audit") return audit(rest);
|
|
6786
6837
|
if (command === "start") return start(rest);
|
|
6838
|
+
if (command === "up") return up(rest);
|
|
6787
6839
|
if (command === "runner") return runnerCommand(rest);
|
|
6788
6840
|
if (command === "cloud") return cloud(rest);
|
|
6789
6841
|
if (command === "mcp") return mcp(rest);
|
|
@@ -6987,8 +7039,32 @@ async function runInitWizard(args, options = {}) {
|
|
|
6987
7039
|
const objectName = await askDefault(ask, "Business object name", optionalArg(args, "--object-name") ?? recipeSpec?.object_name ?? safeObjectName(table.name));
|
|
6988
7040
|
const lookupArg = await askDefault(ask, "Model-visible object id argument", optionalArg(args, "--lookup-arg") ?? recipeSpec?.lookup_arg ?? `${objectName}_id`);
|
|
6989
7041
|
const smokeObjectId = await askDefault(ask, "Optional real object id for a first smoke call", optionalArg(args, "--smoke-id") ?? "");
|
|
7042
|
+
const objectLabel = objectName.replace(/_/g, " ");
|
|
7043
|
+
const inspectDescription = await askDefault(
|
|
7044
|
+
ask,
|
|
7045
|
+
"Read capability description",
|
|
7046
|
+
optionalArg(args, "--inspect-description") ?? `Inspect one ${objectLabel} in trusted tenant scope before answering or proposing a change.`
|
|
7047
|
+
);
|
|
7048
|
+
const inspectReturnsHint = await askDefault(
|
|
7049
|
+
ask,
|
|
7050
|
+
"Read capability returns hint",
|
|
7051
|
+
optionalArg(args, "--inspect-returns-hint") ?? `Returns reviewed ${objectLabel} fields, evidence handle, query audit, and source_database_changed:false.`
|
|
7052
|
+
);
|
|
7053
|
+
const proposalDescription = mode === "read_only" ? void 0 : await askDefault(
|
|
7054
|
+
ask,
|
|
7055
|
+
"Proposal capability description",
|
|
7056
|
+
optionalArg(args, "--proposal-description") ?? `Create a review-required proposal to update one ${objectLabel}. The source database remains unchanged until approval and writeback.`
|
|
7057
|
+
);
|
|
7058
|
+
const proposalReturnsHint = mode === "read_only" ? void 0 : await askDefault(
|
|
7059
|
+
ask,
|
|
7060
|
+
"Proposal capability returns hint",
|
|
7061
|
+
optionalArg(args, "--proposal-returns-hint") ?? "Returns a proposal id, exact before/after diff, evidence handle, approval status, and source_database_changed:false."
|
|
7062
|
+
);
|
|
7063
|
+
const resultFormatAnswer = await askChoice(ask, "MCP result envelope", optionalArg(args, "--result-format") ? normalizeResultFormatAnswer(optionalArg(args, "--result-format")) : "default", ["default", "v1", "v2"]);
|
|
7064
|
+
const resultFormat = resultFormatAnswer === "v1" ? 1 : resultFormatAnswer === "v2" ? 2 : void 0;
|
|
6990
7065
|
let writeUrlEnv = optionalArg(args, "--write-url-env");
|
|
6991
7066
|
let writeback2;
|
|
7067
|
+
let generatedHandlerTemplate;
|
|
6992
7068
|
if (mode === "review") {
|
|
6993
7069
|
const writebackPath = await askChoice(
|
|
6994
7070
|
ask,
|
|
@@ -7011,6 +7087,12 @@ async function runInitWizard(args, options = {}) {
|
|
|
7011
7087
|
...signingSecretEnv ? { handler_signing_secret_env: signingSecretEnv } : {},
|
|
7012
7088
|
timeout_ms: positiveIntegerOption(args, "--handler-timeout-ms")
|
|
7013
7089
|
};
|
|
7090
|
+
const writeTemplate = await askChoice(ask, "Write starter app-owned handler template", args.includes("--skip-handler-template") ? "no" : "yes", ["yes", "no"]);
|
|
7091
|
+
if (writeTemplate === "yes") {
|
|
7092
|
+
const template = await askChoice(ask, "Handler template", optionalArg(args, "--handler-template") ?? "node-fastify", ["node-fastify", "python-fastapi"]);
|
|
7093
|
+
const output = await askDefault(ask, "Handler template output", optionalArg(args, "--handler-template-output") ?? handlerTemplateDefinitions[template].fileName);
|
|
7094
|
+
generatedHandlerTemplate = { name: template, output };
|
|
7095
|
+
}
|
|
7014
7096
|
} else {
|
|
7015
7097
|
const commandEnv = await askEnvName(ask, "App-owned command handler env var", optionalArg(args, "--handler-command-env") ?? "SYNAPSOR_APP_WRITEBACK_COMMAND");
|
|
7016
7098
|
writeback2 = {
|
|
@@ -7019,6 +7101,11 @@ async function runInitWizard(args, options = {}) {
|
|
|
7019
7101
|
handler_command_env: commandEnv,
|
|
7020
7102
|
timeout_ms: positiveIntegerOption(args, "--handler-timeout-ms")
|
|
7021
7103
|
};
|
|
7104
|
+
const writeTemplate = await askChoice(ask, "Write starter app-owned handler template", args.includes("--skip-handler-template") ? "no" : "yes", ["yes", "no"]);
|
|
7105
|
+
if (writeTemplate === "yes") {
|
|
7106
|
+
const output = await askDefault(ask, "Handler template output", optionalArg(args, "--handler-template-output") ?? handlerTemplateDefinitions.command.fileName);
|
|
7107
|
+
generatedHandlerTemplate = { name: "command", output };
|
|
7108
|
+
}
|
|
7022
7109
|
}
|
|
7023
7110
|
}
|
|
7024
7111
|
const approvalRole = mode === "read_only" ? "local_reviewer" : await askDefault(ask, "Required approval role", optionalArg(args, "--approval-role") ?? recipeSpec?.approval?.required_role ?? "local_reviewer");
|
|
@@ -7039,7 +7126,12 @@ async function runInitWizard(args, options = {}) {
|
|
|
7039
7126
|
object_name: objectName,
|
|
7040
7127
|
inspect_tool_name: recipeSpec?.inspect_tool_name,
|
|
7041
7128
|
proposal_tool_name: recipeSpec?.proposal_tool_name,
|
|
7129
|
+
inspect_description: inspectDescription,
|
|
7130
|
+
inspect_returns_hint: inspectReturnsHint,
|
|
7131
|
+
proposal_description: proposalDescription,
|
|
7132
|
+
proposal_returns_hint: proposalReturnsHint,
|
|
7042
7133
|
lookup_arg: lookupArg,
|
|
7134
|
+
result_format: resultFormat,
|
|
7043
7135
|
visible_columns: visibleColumns,
|
|
7044
7136
|
allowed_columns: allowedColumns,
|
|
7045
7137
|
patch,
|
|
@@ -7065,16 +7157,29 @@ async function runInitWizard(args, options = {}) {
|
|
|
7065
7157
|
stdout.write(` source: ${inspection.engine} ${table.schema}.${table.name}
|
|
7066
7158
|
`);
|
|
7067
7159
|
stdout.write(` mode: ${mode}
|
|
7160
|
+
`);
|
|
7161
|
+
stdout.write(` result envelope: ${resultFormat ? `v${resultFormat}` : "default"}
|
|
7068
7162
|
`);
|
|
7069
7163
|
stdout.write(` writeback path: ${writeback2?.executor ?? (mode === "review" ? "sql_update" : "none")}
|
|
7070
7164
|
`);
|
|
7071
7165
|
stdout.write(` exposed tools: ${tools2.join(", ")}
|
|
7072
7166
|
`);
|
|
7073
7167
|
stdout.write(" not exposed: execute_sql, approval tools, commit tools, database URLs, write credentials, model-controlled tenant authority\n");
|
|
7168
|
+
if (generatedHandlerTemplate) {
|
|
7169
|
+
stdout.write(` handler template: ${generatedHandlerTemplate.output}
|
|
7170
|
+
`);
|
|
7171
|
+
stdout.write(`${handlerSecurityWarning}
|
|
7172
|
+
`);
|
|
7173
|
+
}
|
|
7074
7174
|
const confirmed = await askDefault(ask, "Write generated config and MCP snippets? Type yes to continue", "no");
|
|
7075
7175
|
if (confirmed.toLowerCase() !== "yes") throw new Error("guided init canceled before writing files");
|
|
7076
7176
|
const outputPath = outputArg(args) ?? "synapsor.runner.json";
|
|
7077
7177
|
await writeGeneratedOnboardingFiles(outputPath, generated, args.includes("--force"), { printNext: false });
|
|
7178
|
+
if (generatedHandlerTemplate) {
|
|
7179
|
+
await writeHandlerTemplateFile(generatedHandlerTemplate.name, generatedHandlerTemplate.output, args.includes("--force"));
|
|
7180
|
+
stdout.write(`created ${generatedHandlerTemplate.output}
|
|
7181
|
+
`);
|
|
7182
|
+
}
|
|
7078
7183
|
if (smokeObjectId) {
|
|
7079
7184
|
await writeGeneratedSmokeInputFile(lookupArg, smokeObjectId, args.includes("--force"));
|
|
7080
7185
|
stdout.write(`created ${generatedSmokeInputPath}
|
|
@@ -7103,6 +7208,8 @@ async function runInitWizard(args, options = {}) {
|
|
|
7103
7208
|
`);
|
|
7104
7209
|
}
|
|
7105
7210
|
stdout.write(` 3. Serve MCP tools: ${cliCommandName()} mcp serve --config ${outputPath} --store ${defaultStorePath}
|
|
7211
|
+
`);
|
|
7212
|
+
stdout.write(` OpenAI Agents SDK: use ${cliCommandName()} mcp serve-streamable-http --config ${outputPath} --store ${defaultStorePath} --alias-mode openai
|
|
7106
7213
|
`);
|
|
7107
7214
|
return 0;
|
|
7108
7215
|
}
|
|
@@ -7179,6 +7286,11 @@ async function initFromInspection(args, inspection, databaseUrlEnv) {
|
|
|
7179
7286
|
namespace: optionalArg(args, "--namespace") ?? "source",
|
|
7180
7287
|
object_name: optionalArg(args, "--object-name"),
|
|
7181
7288
|
lookup_arg: optionalArg(args, "--lookup-arg"),
|
|
7289
|
+
inspect_description: optionalArg(args, "--inspect-description"),
|
|
7290
|
+
inspect_returns_hint: optionalArg(args, "--inspect-returns-hint"),
|
|
7291
|
+
proposal_description: optionalArg(args, "--proposal-description"),
|
|
7292
|
+
proposal_returns_hint: optionalArg(args, "--proposal-returns-hint"),
|
|
7293
|
+
result_format: resultFormatOption(args),
|
|
7182
7294
|
visible_columns: visibleColumns,
|
|
7183
7295
|
allowed_columns: allowedColumns,
|
|
7184
7296
|
patch,
|
|
@@ -9131,9 +9243,202 @@ async function start(args = []) {
|
|
|
9131
9243
|
await startPolling(config, adapters, controller.signal);
|
|
9132
9244
|
return 0;
|
|
9133
9245
|
}
|
|
9246
|
+
async function up(args = []) {
|
|
9247
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
9248
|
+
"--config",
|
|
9249
|
+
"--store",
|
|
9250
|
+
"--transport",
|
|
9251
|
+
"--host",
|
|
9252
|
+
"--port",
|
|
9253
|
+
"--auth-token-env",
|
|
9254
|
+
"--alias-mode",
|
|
9255
|
+
"--tool-name-style",
|
|
9256
|
+
"--openai-tool-aliases",
|
|
9257
|
+
"--result-format",
|
|
9258
|
+
"--handler-check",
|
|
9259
|
+
"--open-ui",
|
|
9260
|
+
"--print-next",
|
|
9261
|
+
"--dry-run",
|
|
9262
|
+
"--dev-no-auth",
|
|
9263
|
+
"--cors-origin",
|
|
9264
|
+
"--allow-concurrent-store"
|
|
9265
|
+
]);
|
|
9266
|
+
assertKnownOptions(args, allowed, "up");
|
|
9267
|
+
const configPath = optionalArg(args, "--config") ?? defaultConfigPath;
|
|
9268
|
+
const config = await readRuntimeConfig(configPath);
|
|
9269
|
+
const storePath = optionalArg(args, "--store") ?? config.storage?.sqlite_path ?? defaultStorePath;
|
|
9270
|
+
const transport = optionalArg(args, "--transport") ?? "stdio";
|
|
9271
|
+
if (transport !== "stdio" && transport !== "streamable-http") {
|
|
9272
|
+
throw new Error("--transport must be stdio or streamable-http");
|
|
9273
|
+
}
|
|
9274
|
+
const port = Number(optionalArg(args, "--port") ?? "8766");
|
|
9275
|
+
if (transport === "streamable-http" && (!Number.isInteger(port) || port <= 0 || port > 65535)) {
|
|
9276
|
+
throw new Error("--port must be an integer from 1 to 65535");
|
|
9277
|
+
}
|
|
9278
|
+
const aliasMode = toolNameStyleOption(args);
|
|
9279
|
+
const resultFormat = resultFormatOption(args);
|
|
9280
|
+
const validation = validateRunnerCapabilityConfig(config);
|
|
9281
|
+
if (!validation.ok) {
|
|
9282
|
+
throw new Error(`cannot bring Runner up with invalid config: ${validation.errors.map((error) => `${error.path} ${error.code}`).join("; ")}`);
|
|
9283
|
+
}
|
|
9284
|
+
if (storePath !== ":memory:") {
|
|
9285
|
+
await fs3.mkdir(path3.dirname(path3.resolve(storePath)), { recursive: true });
|
|
9286
|
+
}
|
|
9287
|
+
await assertNoActiveStoreLease(storePath, args.includes("--allow-concurrent-store"), "review-mode up");
|
|
9288
|
+
const boundary = await inspectMcpToolBoundary([
|
|
9289
|
+
"--config",
|
|
9290
|
+
configPath,
|
|
9291
|
+
"--store",
|
|
9292
|
+
storePath,
|
|
9293
|
+
"--alias-mode",
|
|
9294
|
+
aliasMode
|
|
9295
|
+
]);
|
|
9296
|
+
process2.stdout.write(formatReviewModeUp({
|
|
9297
|
+
aliasMode,
|
|
9298
|
+
authTokenEnv: optionalArg(args, "--auth-token-env") ?? "SYNAPSOR_RUNNER_HTTP_TOKEN",
|
|
9299
|
+
boundary,
|
|
9300
|
+
config,
|
|
9301
|
+
configPath,
|
|
9302
|
+
dryRun: args.includes("--dry-run"),
|
|
9303
|
+
host: optionalArg(args, "--host") ?? "127.0.0.1",
|
|
9304
|
+
openUi: args.includes("--open-ui"),
|
|
9305
|
+
port,
|
|
9306
|
+
resultFormat,
|
|
9307
|
+
storePath,
|
|
9308
|
+
transport
|
|
9309
|
+
}));
|
|
9310
|
+
if (args.includes("--handler-check")) {
|
|
9311
|
+
process2.stdout.write("\nHandler check:\n");
|
|
9312
|
+
const doctorCode = await doctor(["--config", configPath, "--store", storePath, "--check-handlers"]);
|
|
9313
|
+
if (doctorCode !== 0) return doctorCode;
|
|
9314
|
+
}
|
|
9315
|
+
if (args.includes("--dry-run") || transport === "stdio") return boundary.ok ? 0 : 1;
|
|
9316
|
+
const serveArgs = [
|
|
9317
|
+
"--config",
|
|
9318
|
+
configPath,
|
|
9319
|
+
"--store",
|
|
9320
|
+
storePath,
|
|
9321
|
+
"--host",
|
|
9322
|
+
optionalArg(args, "--host") ?? "127.0.0.1",
|
|
9323
|
+
"--port",
|
|
9324
|
+
String(port),
|
|
9325
|
+
"--auth-token-env",
|
|
9326
|
+
optionalArg(args, "--auth-token-env") ?? "SYNAPSOR_RUNNER_HTTP_TOKEN",
|
|
9327
|
+
"--alias-mode",
|
|
9328
|
+
aliasMode,
|
|
9329
|
+
...resultFormat ? ["--result-format", String(resultFormat)] : [],
|
|
9330
|
+
...args.includes("--dev-no-auth") ? ["--dev-no-auth"] : [],
|
|
9331
|
+
...optionalArg(args, "--cors-origin") ? ["--cors-origin", optionalArg(args, "--cors-origin")] : [],
|
|
9332
|
+
...args.includes("--allow-concurrent-store") ? ["--allow-concurrent-store"] : []
|
|
9333
|
+
];
|
|
9334
|
+
return mcpServeStreamableHttp(serveArgs);
|
|
9335
|
+
}
|
|
9336
|
+
function formatReviewModeUp(input) {
|
|
9337
|
+
const lines = [
|
|
9338
|
+
"Synapsor Runner review-mode up",
|
|
9339
|
+
"",
|
|
9340
|
+
`Config: ${input.configPath}`,
|
|
9341
|
+
`Store: ${input.storePath}`,
|
|
9342
|
+
`Mode: ${input.config.mode}`,
|
|
9343
|
+
`Transport: ${input.transport}`,
|
|
9344
|
+
`Alias mode: ${input.aliasMode}`,
|
|
9345
|
+
`Result format: ${input.resultFormat ? `v${input.resultFormat}` : configResultFormat(input.config)}`,
|
|
9346
|
+
`Dry run: ${input.dryRun ? "yes" : "no"}`,
|
|
9347
|
+
"",
|
|
9348
|
+
"Model-facing tools:",
|
|
9349
|
+
...formatUpToolLines(input.boundary),
|
|
9350
|
+
"",
|
|
9351
|
+
"Writeback paths:",
|
|
9352
|
+
...formatUpWritebackLines(input.config)
|
|
9353
|
+
];
|
|
9354
|
+
const handlerLines = formatUpHandlerLines(input.config);
|
|
9355
|
+
if (handlerLines.length > 0) {
|
|
9356
|
+
lines.push("", "App-owned handler requirements:", ...handlerLines, "", handlerSecurityWarning);
|
|
9357
|
+
}
|
|
9358
|
+
lines.push("", "Server guidance:");
|
|
9359
|
+
if (input.transport === "stdio") {
|
|
9360
|
+
lines.push(
|
|
9361
|
+
" stdio mode is launched by an MCP client. This command does not hold a protocol session open.",
|
|
9362
|
+
` Print client config: ${cliCommandName()} mcp client-config --client claude-desktop --config ${input.configPath} --store ${input.storePath}`,
|
|
9363
|
+
` Serve command used by clients: ${cliCommandName()} mcp serve --config ${input.configPath} --store ${input.storePath} --alias-mode ${input.aliasMode}`
|
|
9364
|
+
);
|
|
9365
|
+
} else {
|
|
9366
|
+
lines.push(
|
|
9367
|
+
` Streamable HTTP endpoint: http://${input.host}:${input.port}/mcp`,
|
|
9368
|
+
` Auth token env: ${input.authTokenEnv} (${process2.env[input.authTokenEnv] ? "set" : "missing"})`,
|
|
9369
|
+
` Start command: ${cliCommandName()} mcp serve-streamable-http --config ${input.configPath} --store ${input.storePath} --port ${input.port} --auth-token-env ${input.authTokenEnv} --alias-mode ${input.aliasMode}`
|
|
9370
|
+
);
|
|
9371
|
+
}
|
|
9372
|
+
if (input.openUi) {
|
|
9373
|
+
lines.push("", "Local review UI:", ` ${cliCommandName()} ui --open --tour --config ${input.configPath} --store ${input.storePath}`);
|
|
9374
|
+
}
|
|
9375
|
+
lines.push("", "Next commands:", ...formatUpNextCommands(input.config, input.configPath, input.storePath), "");
|
|
9376
|
+
return `${lines.join("\n")}
|
|
9377
|
+
`;
|
|
9378
|
+
}
|
|
9379
|
+
function formatUpToolLines(boundary) {
|
|
9380
|
+
if (boundary.exposures.length === 0) return [" - (none)"];
|
|
9381
|
+
return boundary.exposures.map((item) => item.isAlias ? ` - ${item.exposedName} -> ${item.canonicalName}` : ` - ${item.exposedName}`);
|
|
9382
|
+
}
|
|
9383
|
+
function formatUpWritebackLines(config) {
|
|
9384
|
+
const proposals2 = (config.capabilities ?? []).filter((capability) => capability.kind === "proposal");
|
|
9385
|
+
if (proposals2.length === 0) return [" - no proposal capabilities; this config is read-only from Runner's perspective"];
|
|
9386
|
+
return proposals2.map((capability) => {
|
|
9387
|
+
const executorName = capability.executor ?? "sql_update";
|
|
9388
|
+
if (executorName === "sql_update") {
|
|
9389
|
+
const source = config.sources?.[capability.source];
|
|
9390
|
+
const envName = source?.write_url_env ?? "SYNAPSOR_DATABASE_URL";
|
|
9391
|
+
return ` - ${capability.name}: direct guarded one-row UPDATE via ${envName} (${process2.env[envName] ? "set" : "missing"})`;
|
|
9392
|
+
}
|
|
9393
|
+
const executor = config.executors?.[executorName];
|
|
9394
|
+
return ` - ${capability.name}: app-owned ${String(executor?.type ?? "executor")} ${executorName}`;
|
|
9395
|
+
});
|
|
9396
|
+
}
|
|
9397
|
+
function formatUpHandlerLines(config) {
|
|
9398
|
+
const lines = [];
|
|
9399
|
+
for (const [name, executor] of Object.entries(config.executors ?? {})) {
|
|
9400
|
+
if (!isRecord6(executor)) continue;
|
|
9401
|
+
if (executor.type === "http_handler") {
|
|
9402
|
+
const urlEnv = typeof executor.url_env === "string" ? executor.url_env : "";
|
|
9403
|
+
const auth = isRecord6(executor.auth) ? executor.auth : void 0;
|
|
9404
|
+
const tokenEnv = typeof auth?.token_env === "string" ? auth.token_env : void 0;
|
|
9405
|
+
const signingSecretEnv = typeof executor.signing_secret_env === "string" ? executor.signing_secret_env : void 0;
|
|
9406
|
+
lines.push(` - ${name}: http_handler`);
|
|
9407
|
+
if (urlEnv) lines.push(` url env: ${urlEnv} (${process2.env[urlEnv] ? "set" : "missing"})`);
|
|
9408
|
+
if (tokenEnv) lines.push(` bearer token env: ${tokenEnv} (${process2.env[tokenEnv] ? "set" : "missing"})`);
|
|
9409
|
+
if (signingSecretEnv) lines.push(` signing secret env: ${signingSecretEnv} (${process2.env[signingSecretEnv] ? "set" : "missing"})`);
|
|
9410
|
+
if (!signingSecretEnv) lines.push(" signing secret env: not configured (recommended unless loopback-only)");
|
|
9411
|
+
} else if (executor.type === "command_handler") {
|
|
9412
|
+
const commandEnv = typeof executor.command_env === "string" ? executor.command_env : "";
|
|
9413
|
+
lines.push(` - ${name}: command_handler`);
|
|
9414
|
+
if (commandEnv) lines.push(` command env: ${commandEnv} (${process2.env[commandEnv] ? "set" : "missing"})`);
|
|
9415
|
+
}
|
|
9416
|
+
}
|
|
9417
|
+
return lines;
|
|
9418
|
+
}
|
|
9419
|
+
function configResultFormat(config) {
|
|
9420
|
+
return config.result_format === 2 ? "v2" : config.result_format === 1 ? "v1" : "default";
|
|
9421
|
+
}
|
|
9422
|
+
function formatUpNextCommands(config, configPath, storePath) {
|
|
9423
|
+
const firstTool = (config.capabilities ?? [])[0]?.name ?? "<capability>";
|
|
9424
|
+
const hasHandlers = Object.keys(config.executors ?? {}).length > 0;
|
|
9425
|
+
return [
|
|
9426
|
+
` - Preview tools: ${cliCommandName()} tools preview --config ${configPath} --store ${storePath}`,
|
|
9427
|
+
` - Smoke call: ${cliCommandName()} smoke call ${firstTool} --sample --config ${configPath} --store ${storePath}`,
|
|
9428
|
+
` - List proposals: ${cliCommandName()} proposals list --store ${storePath}`,
|
|
9429
|
+
` - Show proposal: ${cliCommandName()} proposals show latest --store ${storePath}`,
|
|
9430
|
+
` - Approve proposal: ${cliCommandName()} proposals approve latest --yes --store ${storePath}`,
|
|
9431
|
+
` - Apply approved proposal: ${cliCommandName()} apply latest --config ${configPath} --store ${storePath}`,
|
|
9432
|
+
` - Replay: ${cliCommandName()} replay show latest --store ${storePath}`,
|
|
9433
|
+
` - Tail events: ${cliCommandName()} events tail --store ${storePath}`,
|
|
9434
|
+
` - Direct writeback doctor: ${cliCommandName()} doctor --config ${configPath} --check-writeback`,
|
|
9435
|
+
...hasHandlers ? [` - Handler doctor: ${cliCommandName()} doctor --config ${configPath} --check-handlers`] : []
|
|
9436
|
+
];
|
|
9437
|
+
}
|
|
9134
9438
|
async function runnerCommand(args) {
|
|
9135
9439
|
const [subcommand, ...rest] = args;
|
|
9136
9440
|
if (subcommand === "start") return start(rest);
|
|
9441
|
+
if (subcommand === "up") return up(rest);
|
|
9137
9442
|
if (subcommand === "doctor") return doctor(rest);
|
|
9138
9443
|
usage();
|
|
9139
9444
|
return 2;
|
|
@@ -9172,7 +9477,7 @@ async function cloudConnect(args) {
|
|
|
9172
9477
|
return 1;
|
|
9173
9478
|
}
|
|
9174
9479
|
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.
|
|
9480
|
+
const runnerVersion = String(parsed.cloud.runner_version || process2.env.npm_package_version || "0.1.0-alpha.16").trim();
|
|
9176
9481
|
const engines = normalizeEngines(parsed.cloud.engines);
|
|
9177
9482
|
const capabilities = normalizeCapabilities(parsed.cloud.capabilities);
|
|
9178
9483
|
const client = new ControlPlaneClient({
|
|
@@ -9277,21 +9582,28 @@ async function handlerTemplate(args) {
|
|
|
9277
9582
|
return 0;
|
|
9278
9583
|
}
|
|
9279
9584
|
const output = outputArg(args) ?? definition.fileName;
|
|
9280
|
-
await
|
|
9281
|
-
if (name === "command" || output.endsWith(".mjs") || output.endsWith(".js")) {
|
|
9282
|
-
await fs3.chmod(path3.resolve(output), 493).catch(() => void 0);
|
|
9283
|
-
}
|
|
9585
|
+
await writeHandlerTemplateFile(name, output, args.includes("--force"));
|
|
9284
9586
|
process2.stdout.write(`created ${output}
|
|
9285
9587
|
`);
|
|
9286
|
-
process2.stdout.write(
|
|
9588
|
+
process2.stdout.write(`${handlerSecurityWarning}
|
|
9589
|
+
`);
|
|
9287
9590
|
return 0;
|
|
9288
9591
|
}
|
|
9592
|
+
async function writeHandlerTemplateFile(name, output, force) {
|
|
9593
|
+
const definition = handlerTemplateDefinitions[name];
|
|
9594
|
+
await writeFileGuarded(output, definition.content, force);
|
|
9595
|
+
if (name === "command" || output.endsWith(".mjs") || output.endsWith(".js")) {
|
|
9596
|
+
await fs3.chmod(path3.resolve(output), 493).catch(() => void 0);
|
|
9597
|
+
}
|
|
9598
|
+
}
|
|
9289
9599
|
function formatHandlerTemplateList() {
|
|
9290
9600
|
return [
|
|
9291
9601
|
"Synapsor app-owned writeback handler templates",
|
|
9292
9602
|
"",
|
|
9293
9603
|
...Object.entries(handlerTemplateDefinitions).map(([name, definition]) => `- ${name}: ${definition.description}`),
|
|
9294
9604
|
"",
|
|
9605
|
+
handlerSecurityWarning,
|
|
9606
|
+
"",
|
|
9295
9607
|
"Examples:",
|
|
9296
9608
|
` ${cliCommandName()} handler template node-fastify --output ./synapsor-writeback-handler.mjs`,
|
|
9297
9609
|
` ${cliCommandName()} handler template python-fastapi --output ./synapsor_writeback_handler.py`,
|
|
@@ -10040,7 +10352,7 @@ async function assertNoActiveStoreLease(storePath, force, operation) {
|
|
|
10040
10352
|
await fs3.rm(storeLeasePath(resolved), { force: true });
|
|
10041
10353
|
return;
|
|
10042
10354
|
}
|
|
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.`;
|
|
10355
|
+
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
10356
|
if (!force) throw new Error(message);
|
|
10045
10357
|
process2.stderr.write(`Warning: ${message}
|
|
10046
10358
|
`);
|
|
@@ -10104,6 +10416,12 @@ function resultFormatOption(args) {
|
|
|
10104
10416
|
if (requested === "2" || requested === "v2") return 2;
|
|
10105
10417
|
throw new Error("--result-format must be v1, 1, v2, or 2");
|
|
10106
10418
|
}
|
|
10419
|
+
function normalizeResultFormatAnswer(value) {
|
|
10420
|
+
if (value === "1" || value === "v1") return "v1";
|
|
10421
|
+
if (value === "2" || value === "v2") return "v2";
|
|
10422
|
+
if (value === "default") return "default";
|
|
10423
|
+
throw new Error("--result-format must be default, v1, 1, v2, or 2");
|
|
10424
|
+
}
|
|
10107
10425
|
async function mcpAudit(args) {
|
|
10108
10426
|
const format = optionalArg(args, "--format") ?? (args.includes("--json") ? "json" : "text");
|
|
10109
10427
|
if (!["text", "json", "markdown"].includes(format)) {
|
|
@@ -12231,8 +12549,8 @@ async function prepareReferenceDemo(args) {
|
|
|
12231
12549
|
].join("\n"));
|
|
12232
12550
|
const down = spawnSync("docker", ["compose", "-f", composePath, "down", "-v", "--remove-orphans"], { stdio: "inherit", env: process2.env });
|
|
12233
12551
|
if (down.status !== 0) return down.status ?? 1;
|
|
12234
|
-
const
|
|
12235
|
-
if (
|
|
12552
|
+
const up2 = spawnSync("docker", ["compose", "-f", composePath, "up", "-d"], { stdio: "inherit", env: process2.env });
|
|
12553
|
+
if (up2.status !== 0) return up2.status ?? 1;
|
|
12236
12554
|
await waitForReferenceDemoDatabase();
|
|
12237
12555
|
await fs3.copyFile(path3.join(demoDir, "synapsor.runner.json"), configPath);
|
|
12238
12556
|
process2.stdout.write([
|
|
@@ -13311,7 +13629,7 @@ function starterCloudConfig() {
|
|
|
13311
13629
|
base_url_env: "SYNAPSOR_CLOUD_BASE_URL",
|
|
13312
13630
|
runner_token_env: "SYNAPSOR_RUNNER_TOKEN",
|
|
13313
13631
|
runner_id: "synapsor_runner_local",
|
|
13314
|
-
runner_version: "0.1.0-alpha.
|
|
13632
|
+
runner_version: "0.1.0-alpha.16",
|
|
13315
13633
|
project_id: "token_scope",
|
|
13316
13634
|
adapter_id: "mcp.your_adapter",
|
|
13317
13635
|
source_id: "src_replace_me",
|
|
@@ -13346,6 +13664,7 @@ function isKnownTopLevelCommand(command) {
|
|
|
13346
13664
|
"propose",
|
|
13347
13665
|
"audit",
|
|
13348
13666
|
"start",
|
|
13667
|
+
"up",
|
|
13349
13668
|
"runner",
|
|
13350
13669
|
"cloud",
|
|
13351
13670
|
"mcp",
|
|
@@ -13388,6 +13707,7 @@ Usage:
|
|
|
13388
13707
|
Commands:
|
|
13389
13708
|
inspect Inspect a Postgres/MySQL schema
|
|
13390
13709
|
start Start guided own-database setup, or no-arg legacy worker polling
|
|
13710
|
+
up Bring up local review mode guidance/server
|
|
13391
13711
|
init Generate a Synapsor capability contract
|
|
13392
13712
|
mcp Serve safe semantic tools over MCP
|
|
13393
13713
|
onboard One-command own-database setup
|
|
@@ -13411,6 +13731,7 @@ Commands:
|
|
|
13411
13731
|
|
|
13412
13732
|
Examples:
|
|
13413
13733
|
${cmd} start --from-env DATABASE_URL
|
|
13734
|
+
${cmd} up --config ./synapsor.runner.json --store ./.synapsor/local.db --dry-run
|
|
13414
13735
|
${cmd} onboard db --from-env DATABASE_URL
|
|
13415
13736
|
${cmd} inspect --from-env DATABASE_URL
|
|
13416
13737
|
${cmd} init --wizard --from-env DATABASE_URL
|
|
@@ -13420,6 +13741,26 @@ Examples:
|
|
|
13420
13741
|
${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
13421
13742
|
${cmd} propose billing.propose_late_fee_waiver --sample
|
|
13422
13743
|
${cmd} audit ./synapsor.runner.json
|
|
13744
|
+
`,
|
|
13745
|
+
up: `Usage:
|
|
13746
|
+
${cmd} up --config ./synapsor.runner.json --store ./.synapsor/local.db [--transport stdio|streamable-http]
|
|
13747
|
+
${cmd} up --config ./synapsor.runner.json --store ./.synapsor/local.db --transport streamable-http --port 8766 --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
|
|
13748
|
+
${cmd} up --config ./synapsor.runner.json --store ./.synapsor/local.db --handler-check --dry-run
|
|
13749
|
+
|
|
13750
|
+
Validate the local Runner config and store, summarize model-facing tools,
|
|
13751
|
+
explain direct SQL versus app-owned executor writeback, and print the next
|
|
13752
|
+
smoke/approve/apply/replay commands.
|
|
13753
|
+
|
|
13754
|
+
With --transport stdio, \`${cmd} up\` prints MCP client wiring because stdio is
|
|
13755
|
+
launched by the client. With --transport streamable-http and without --dry-run,
|
|
13756
|
+
it starts the standard Streamable HTTP MCP server.
|
|
13757
|
+
|
|
13758
|
+
Options:
|
|
13759
|
+
--alias-mode canonical|openai|both
|
|
13760
|
+
--result-format v1|v2
|
|
13761
|
+
--handler-check
|
|
13762
|
+
--open-ui
|
|
13763
|
+
--dry-run
|
|
13423
13764
|
`,
|
|
13424
13765
|
start: `Usage:
|
|
13425
13766
|
${cmd} start --from-env DATABASE_URL [--schema public] [--mode read_only|shadow|review]
|
|
@@ -17,6 +17,13 @@ The model-facing MCP tool only creates a proposal. Approval happens outside MCP.
|
|
|
17
17
|
After approval, Runner calls your `http_handler` or `command_handler`, records
|
|
18
18
|
the receipt, and includes the result in replay.
|
|
19
19
|
|
|
20
|
+
> **Important:** your app handler owns the final business write. Runner creates
|
|
21
|
+
> the proposal and calls your handler only after approval, but your handler must
|
|
22
|
+
> still enforce tenant/scope checks, expected-version or conflict guards,
|
|
23
|
+
> idempotency keys, allowed business actions, transaction/rollback, and safe
|
|
24
|
+
> error receipts. If you skip those checks, you can reintroduce cross-tenant
|
|
25
|
+
> writes, lost updates, or duplicate writes. Keep handler credentials out of MCP.
|
|
26
|
+
|
|
20
27
|
A handler is your application endpoint or script. It is not a second Synapsor
|
|
21
28
|
package that users need to install. Install `@synapsor/runner`, then generate
|
|
22
29
|
or copy a handler template only when your approved write needs app-owned
|
package/docs/handler-helper.md
CHANGED
|
@@ -17,6 +17,13 @@ The model-facing MCP tool still creates a proposal only. A human/operator
|
|
|
17
17
|
approves outside MCP. After approval, Runner sends the structured writeback
|
|
18
18
|
request to your handler.
|
|
19
19
|
|
|
20
|
+
> **Important:** your app handler owns the final business write. Runner creates
|
|
21
|
+
> the proposal and calls your handler only after approval, but your handler must
|
|
22
|
+
> still enforce tenant/scope checks, expected-version or conflict guards,
|
|
23
|
+
> idempotency keys, allowed business actions, transaction/rollback, and safe
|
|
24
|
+
> error receipts. If you skip those checks, you can reintroduce cross-tenant
|
|
25
|
+
> writes, lost updates, or duplicate writes. Keep handler credentials out of MCP.
|
|
26
|
+
|
|
20
27
|
## Scope
|
|
21
28
|
|
|
22
29
|
Current alpha scope:
|
package/docs/release-notes.md
CHANGED
|
@@ -11,6 +11,40 @@ npx -y -p @synapsor/runner@alpha synapsor-runner demo --quick
|
|
|
11
11
|
The OSS runner command is `synapsor-runner`. The `synapsor` command is reserved
|
|
12
12
|
for the Synapsor Cloud CLI.
|
|
13
13
|
|
|
14
|
+
## 0.1.0-alpha.16
|
|
15
|
+
|
|
16
|
+
### Review-Mode Bring-Up
|
|
17
|
+
|
|
18
|
+
- Added `synapsor-runner up` as the local review-mode orientation command. It
|
|
19
|
+
validates the config/store, checks active store leases, summarizes
|
|
20
|
+
model-facing tools, identifies direct SQL versus app-owned executor writeback
|
|
21
|
+
paths, and prints the next smoke, approval, apply, replay, UI, and doctor
|
|
22
|
+
commands.
|
|
23
|
+
- `up --dry-run` gives the full checklist without starting a server.
|
|
24
|
+
- `up --transport streamable-http` starts the standard MCP Streamable HTTP
|
|
25
|
+
server after the same validation and guidance.
|
|
26
|
+
- `up --handler-check` runs the redacted handler env/reachability doctor path
|
|
27
|
+
before serving.
|
|
28
|
+
- The guided wizard now writes model-facing capability descriptions,
|
|
29
|
+
per-argument descriptions, returns hints, and can opt into
|
|
30
|
+
`result_format: 2`.
|
|
31
|
+
- `result_format: 2` gives MCP clients a stable envelope with `ok`, `summary`,
|
|
32
|
+
`data`, `proposal`, `error`, `evidence`, `source_database_changed`, and
|
|
33
|
+
`_meta.canonical_capability`.
|
|
34
|
+
- `tools list`, `tools list --aliases`, and
|
|
35
|
+
`mcp client-config --include-instructions` help users inspect exposed tools
|
|
36
|
+
and generate client snippets without source reading.
|
|
37
|
+
|
|
38
|
+
### Handler Security
|
|
39
|
+
|
|
40
|
+
- Generated handler templates, template-list output, app-owned writeback docs,
|
|
41
|
+
and examples now explicitly warn that the app handler owns the final business
|
|
42
|
+
write. Handlers must re-check tenant/scope, expected-version or conflict
|
|
43
|
+
guard, idempotency, allowed business action, transaction/rollback, and safe
|
|
44
|
+
error receipts before mutating application state.
|
|
45
|
+
- The guided review-mode wizard can now write a starter handler template when
|
|
46
|
+
the app-owned HTTP or command handler path is selected.
|
|
47
|
+
|
|
14
48
|
## 0.1.0-alpha.15
|
|
15
49
|
|
|
16
50
|
### Handler Wording Clarification
|
|
@@ -217,5 +251,5 @@ After publishing an alpha, verify the public package from a clean temporary
|
|
|
217
251
|
directory:
|
|
218
252
|
|
|
219
253
|
```bash
|
|
220
|
-
./scripts/verify-published-alpha.sh 0.1.0-alpha.
|
|
254
|
+
./scripts/verify-published-alpha.sh 0.1.0-alpha.16
|
|
221
255
|
```
|
package/docs/release-policy.md
CHANGED
|
@@ -5,7 +5,7 @@ or an exact version:
|
|
|
5
5
|
|
|
6
6
|
```bash
|
|
7
7
|
npx -y -p @synapsor/runner@alpha synapsor-runner demo --quick
|
|
8
|
-
npm install -g @synapsor/runner@0.1.0-alpha.
|
|
8
|
+
npm install -g @synapsor/runner@0.1.0-alpha.16
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
Do not rely on the untagged `latest` dist-tag until a stable release is
|
|
@@ -38,12 +38,26 @@ A stable `0.1.0` release should only be tagged after:
|
|
|
38
38
|
- npm README commands match the published package;
|
|
39
39
|
- `synapsor-runner demo --quick` works from a clean directory;
|
|
40
40
|
- own-database onboarding works from a clean directory;
|
|
41
|
+
- one-command review-mode `synapsor-runner up` is verified from a clean
|
|
42
|
+
directory and clearly prints model-facing tools, writeback path, handler
|
|
43
|
+
requirements, and next commands;
|
|
44
|
+
- review-mode wizard output is verified for one read capability plus one
|
|
45
|
+
proposal capability;
|
|
46
|
+
- handler template security warnings are verified in docs, CLI output, and
|
|
47
|
+
generated templates;
|
|
41
48
|
- stdio MCP and Streamable HTTP MCP are both verified;
|
|
42
49
|
- OpenAI alias mode is verified;
|
|
43
50
|
- direct SQL writeback requirements are documented and tested;
|
|
44
51
|
- app-owned executor requirements are documented and tested;
|
|
45
52
|
- local evidence/proposal/receipt/replay inspection works;
|
|
46
53
|
- current limitations are accurate.
|
|
54
|
+
- at least one external developer can follow the README without reading source;
|
|
55
|
+
- there are no known docs/code mismatches around transport, credentials,
|
|
56
|
+
receipt tables, or handler expectations.
|
|
57
|
+
|
|
58
|
+
Serious alpha users should pin an exact alpha version in package.json, CI, and
|
|
59
|
+
MCP client snippets. Use `@alpha` only when intentionally testing the moving
|
|
60
|
+
preview channel.
|
|
47
61
|
|
|
48
62
|
## Result Envelope Migration
|
|
49
63
|
|
|
@@ -115,6 +115,13 @@ The handler receives proposal fields, the exact patch, evidence metadata,
|
|
|
115
115
|
guards, and an idempotency key. It does not receive arbitrary model SQL or DB
|
|
116
116
|
credentials from Synapsor Runner.
|
|
117
117
|
|
|
118
|
+
> **Important:** your app handler owns the final business write. Runner creates
|
|
119
|
+
> the proposal and calls your handler only after approval, but your handler must
|
|
120
|
+
> still enforce tenant/scope checks, expected-version or conflict guards,
|
|
121
|
+
> idempotency keys, allowed business actions, transaction/rollback, and safe
|
|
122
|
+
> error receipts. If you skip those checks, you can reintroduce cross-tenant
|
|
123
|
+
> writes, lost updates, or duplicate writes. Keep handler credentials out of MCP.
|
|
124
|
+
|
|
118
125
|
When `signing_secret_env` is set, Runner signs the exact JSON body with HMAC
|
|
119
126
|
SHA-256 and sends:
|
|
120
127
|
|
|
@@ -220,6 +227,12 @@ npx -y -p @synapsor/runner@alpha synapsor-runner handler template command \
|
|
|
220
227
|
The command receives the same structured JSON request on stdin and should print
|
|
221
228
|
a JSON receipt body on stdout.
|
|
222
229
|
|
|
230
|
+
> **Important:** command handlers have the same responsibility as HTTP
|
|
231
|
+
> handlers. Re-check tenant/scope, expected-version or conflict guard,
|
|
232
|
+
> idempotency, allowed business action, transaction/rollback, and safe error
|
|
233
|
+
> receipt before mutating state. Otherwise the script can reintroduce
|
|
234
|
+
> cross-tenant writes, lost updates, or duplicate writes.
|
|
235
|
+
|
|
223
236
|
Use `examples/app-owned-writeback/command-handler.mjs` as a starting point when
|
|
224
237
|
your safest apply path is an app script or job runner.
|
|
225
238
|
|
|
@@ -14,6 +14,13 @@ The model-facing MCP tool still only creates a proposal. Approval happens
|
|
|
14
14
|
outside MCP. After approval, `synapsor-runner apply` sends a structured request
|
|
15
15
|
to your handler, and the handler returns an execution receipt for replay.
|
|
16
16
|
|
|
17
|
+
> **Important:** your app handler owns the final business write. Runner creates
|
|
18
|
+
> the proposal and calls your handler only after approval, but your handler must
|
|
19
|
+
> still enforce tenant/scope checks, expected-version or conflict guards,
|
|
20
|
+
> idempotency keys, allowed business actions, transaction/rollback, and safe
|
|
21
|
+
> error receipts. If you skip those checks, you can reintroduce cross-tenant
|
|
22
|
+
> writes, lost updates, or duplicate writes. Keep handler credentials out of MCP.
|
|
23
|
+
|
|
17
24
|
## Config Snippet
|
|
18
25
|
|
|
19
26
|
Add an executor and point one proposal capability at it:
|
|
@@ -26,6 +26,15 @@ if (request.dry_run) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/*
|
|
29
|
+
* IMPORTANT: your app handler owns the final business write.
|
|
30
|
+
* Runner creates the proposal and calls your handler only after approval,
|
|
31
|
+
* but your handler must still enforce tenant/scope, expected-version or
|
|
32
|
+
* conflict guard, idempotency key, allowed business action,
|
|
33
|
+
* transaction/rollback, and safe error receipt.
|
|
34
|
+
*
|
|
35
|
+
* If you skip those checks, you can reintroduce cross-tenant writes,
|
|
36
|
+
* lost updates, or duplicate writes. Keep handler credentials out of MCP.
|
|
37
|
+
*
|
|
29
38
|
* Put your app-owned command transaction here.
|
|
30
39
|
*
|
|
31
40
|
* Examples:
|
|
@@ -28,6 +28,15 @@ app.post("/synapsor/writeback", async (request, reply) => {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/*
|
|
31
|
+
* IMPORTANT: your app handler owns the final business write.
|
|
32
|
+
* Runner creates the proposal and calls your handler only after approval,
|
|
33
|
+
* but your handler must still enforce tenant/scope, expected-version or
|
|
34
|
+
* conflict guard, idempotency key, allowed business action,
|
|
35
|
+
* transaction/rollback, and safe error receipt.
|
|
36
|
+
*
|
|
37
|
+
* If you skip those checks, you can reintroduce cross-tenant writes,
|
|
38
|
+
* lost updates, or duplicate writes. Keep handler credentials out of MCP.
|
|
39
|
+
*
|
|
31
40
|
* Put your app-owned transaction here.
|
|
32
41
|
*
|
|
33
42
|
* Examples:
|
|
@@ -36,6 +36,15 @@ def writeback(request: HandlerRequest, authorization: str | None = Header(defaul
|
|
|
36
36
|
"details": {"dry_run": True},
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
# IMPORTANT: your app handler owns the final business write.
|
|
40
|
+
# Runner creates the proposal and calls your handler only after approval,
|
|
41
|
+
# but your handler must still enforce tenant/scope, expected-version or
|
|
42
|
+
# conflict guard, idempotency key, allowed business action,
|
|
43
|
+
# transaction/rollback, and safe error receipt.
|
|
44
|
+
#
|
|
45
|
+
# If you skip those checks, you can reintroduce cross-tenant writes,
|
|
46
|
+
# lost updates, or duplicate writes. Keep handler credentials out of MCP.
|
|
47
|
+
#
|
|
39
48
|
# Put your app-owned transaction here.
|
|
40
49
|
#
|
|
41
50
|
# Examples:
|
|
@@ -26,6 +26,13 @@ App-owned rich writeback:
|
|
|
26
26
|
The model never receives `execute_sql`, approval tools, commit/apply tools,
|
|
27
27
|
database URLs, or write credentials.
|
|
28
28
|
|
|
29
|
+
> **Important:** the app handler owns the final business write. Runner creates
|
|
30
|
+
> the proposal and calls the handler only after approval, but the handler must
|
|
31
|
+
> still enforce tenant/scope checks, expected-version or conflict guards,
|
|
32
|
+
> idempotency keys, allowed business actions, transaction/rollback, and safe
|
|
33
|
+
> error receipts. If those checks are skipped, the app can reintroduce
|
|
34
|
+
> cross-tenant writes, lost updates, or duplicate writes.
|
|
35
|
+
|
|
29
36
|
## Run
|
|
30
37
|
|
|
31
38
|
From the repository root:
|
|
@@ -54,6 +54,12 @@ async function shutdown() {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
async function applyAccountCredit(job, tx) {
|
|
57
|
+
/*
|
|
58
|
+
* IMPORTANT: this app handler owns the final business write.
|
|
59
|
+
* The helper has already verified auth, tenant scope, expected version,
|
|
60
|
+
* idempotency, and transaction wrapping before this function runs. Keep that
|
|
61
|
+
* pattern if you replace the helper or move this logic into your app.
|
|
62
|
+
*/
|
|
57
63
|
const amountCents = Number(job.patch.credit_requested_cents);
|
|
58
64
|
const reason = String(job.patch.credit_reason || "approved account credit");
|
|
59
65
|
if (!Number.isInteger(amountCents) || amountCents <= 0) {
|
package/package.json
CHANGED
|
@@ -24,7 +24,12 @@
|
|
|
24
24
|
"object_name": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
|
|
25
25
|
"inspect_tool_name": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)+$" },
|
|
26
26
|
"proposal_tool_name": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)+$" },
|
|
27
|
+
"inspect_description": { "type": "string", "minLength": 1 },
|
|
28
|
+
"proposal_description": { "type": "string", "minLength": 1 },
|
|
29
|
+
"inspect_returns_hint": { "type": "string", "minLength": 1 },
|
|
30
|
+
"proposal_returns_hint": { "type": "string", "minLength": 1 },
|
|
27
31
|
"lookup_arg": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
|
|
32
|
+
"result_format": { "enum": [1, 2] },
|
|
28
33
|
"visible_columns": {
|
|
29
34
|
"type": "array",
|
|
30
35
|
"items": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
|