@synapsor/runner 0.1.0-alpha.9 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +189 -0
  2. package/README.md +949 -164
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/runner.mjs +2982 -238
  6. package/docs/README.md +90 -15
  7. package/docs/app-owned-executors.md +38 -0
  8. package/docs/capability-authoring.md +265 -0
  9. package/docs/cloud-mode.md +24 -0
  10. package/docs/current-scope.md +29 -0
  11. package/docs/dependency-license-inventory.md +35 -0
  12. package/docs/doctor.md +98 -0
  13. package/docs/getting-started-own-database.md +131 -46
  14. package/docs/handler-helper.md +228 -0
  15. package/docs/http-mcp.md +85 -17
  16. package/docs/licensing.md +36 -0
  17. package/docs/local-mode.md +44 -25
  18. package/docs/mcp-audit.md +8 -8
  19. package/docs/mcp-client-setup.md +59 -21
  20. package/docs/openai-agents-sdk.md +57 -0
  21. package/docs/recipes.md +6 -6
  22. package/docs/release-notes.md +348 -0
  23. package/docs/release-policy.md +125 -0
  24. package/docs/result-envelope-v2.md +151 -0
  25. package/docs/rfcs/001-result-envelope-v2.md +143 -0
  26. package/docs/rfcs/002-app-owned-handler-helper.md +161 -0
  27. package/docs/rfcs/003-integrator-feedback-teardown.md +97 -0
  28. package/docs/store-lifecycle.md +83 -0
  29. package/docs/troubleshooting-first-run.md +6 -6
  30. package/docs/use-your-own-database.md +18 -0
  31. package/docs/writeback-executors.md +92 -1
  32. package/examples/app-owned-writeback/README.md +128 -0
  33. package/examples/app-owned-writeback/business-actions.md +221 -0
  34. package/examples/app-owned-writeback/command-handler.mjs +55 -0
  35. package/examples/app-owned-writeback/node-fastify-handler.mjs +64 -0
  36. package/examples/app-owned-writeback/python-fastapi-handler.py +66 -0
  37. package/examples/claude-desktop-postgres/Makefile +6 -0
  38. package/examples/claude-desktop-postgres/README.md +40 -0
  39. package/examples/cursor-postgres/Makefile +6 -0
  40. package/examples/cursor-postgres/README.md +30 -0
  41. package/examples/mcp-postgres-billing-app-handler/README.md +94 -0
  42. package/examples/mcp-postgres-billing-app-handler/app-handler.mjs +123 -0
  43. package/examples/mcp-postgres-billing-app-handler/docker-compose.yml +13 -0
  44. package/examples/mcp-postgres-billing-app-handler/schema.sql +59 -0
  45. package/examples/mcp-postgres-billing-app-handler/scripts/run-demo.sh +100 -0
  46. package/examples/mcp-postgres-billing-app-handler/seed.sql +39 -0
  47. package/examples/mcp-postgres-billing-app-handler/synapsor-handler.mjs +437 -0
  48. package/examples/mcp-postgres-billing-app-handler/synapsor.runner.json +158 -0
  49. package/examples/mysql-refund-agent/Makefile +4 -0
  50. package/examples/mysql-refund-agent/README.md +36 -0
  51. package/examples/openai-agents-http/Makefile +6 -0
  52. package/examples/openai-agents-http/README.md +33 -12
  53. package/examples/openai-agents-http/agent.py +29 -65
  54. package/examples/openai-agents-stdio/Makefile +6 -0
  55. package/examples/openai-agents-stdio/README.md +24 -6
  56. package/examples/openai-agents-stdio/agent.py +4 -2
  57. package/examples/raw-sql-vs-synapsor/Makefile +11 -0
  58. package/examples/raw-sql-vs-synapsor/README.md +41 -0
  59. package/examples/reference-support-billing-app/README.md +16 -16
  60. package/examples/reference-support-billing-app/mcp-client.generic.json +1 -1
  61. package/examples/support-billing-agent/Makefile +19 -0
  62. package/examples/support-billing-agent/README.md +89 -0
  63. package/examples/support-billing-agent/app/README.md +13 -0
  64. package/examples/support-billing-agent/db/schema.sql +91 -0
  65. package/examples/support-billing-agent/db/seed.sql +43 -0
  66. package/examples/support-billing-agent/docker-compose.yml +13 -0
  67. package/examples/support-billing-agent/scripts/run-demo.sh +15 -0
  68. package/examples/support-billing-agent/synapsor.runner.json +233 -0
  69. package/fixtures/benchmark/mcp-efficiency.json +53 -0
  70. package/fixtures/benchmark/mcp-efficiency.txt +25 -0
  71. package/fixtures/protocol/MANIFEST.json +54 -0
  72. package/fixtures/protocol/change-set.late-fee-waiver.v1.json +72 -0
  73. package/fixtures/protocol/execution-receipt.applied.v1.json +14 -0
  74. package/fixtures/protocol/execution-receipt.conflict.v1.json +15 -0
  75. package/fixtures/protocol/runner-registration.v1.json +22 -0
  76. package/fixtures/protocol/writeback-job.late-fee-waiver.v1.json +44 -0
  77. package/package.json +27 -4
  78. package/schemas/change-set.v1.schema.json +140 -0
  79. package/schemas/execution-receipt.v1.schema.json +34 -0
  80. package/schemas/onboarding-selection.v1.schema.json +132 -0
  81. package/schemas/runner-registration.v1.schema.json +48 -0
  82. package/schemas/synapsor.app-handler-receipt.v1.json +39 -0
  83. package/schemas/synapsor.app-handler-request.v1.json +119 -0
  84. package/schemas/synapsor.runner.schema.json +415 -0
  85. package/schemas/writeback-job.v1.schema.json +121 -0
@@ -14,11 +14,11 @@ tools from your selections.
14
14
 
15
15
  ## Fast path
16
16
 
17
- Set one read-only database URL and run the wrapper from this repo:
17
+ Set one read-only database URL and run the public guided path:
18
18
 
19
19
  ```bash
20
20
  export DATABASE_URL="<postgres-or-mysql-read-url>"
21
- ./scripts/use-your-db.sh
21
+ npx -y -p @synapsor/runner synapsor-runner start --from-env DATABASE_URL
22
22
  ```
23
23
 
24
24
  That command does the useful local mini-Synapsor path:
@@ -30,6 +30,8 @@ inspect your schema
30
30
  -> optionally choose proposal/writeback rules
31
31
  -> generate synapsor.runner.json
32
32
  -> preview MCP tools exposed to the model
33
+ -> run a local smoke check of the tool boundary
34
+ -> optionally write .synapsor/smoke-input.json for one real row
33
35
  -> print mcp serve and local UI commands
34
36
  ```
35
37
 
@@ -37,10 +39,23 @@ It does not print your database URL, put the URL in MCP client config, expose
37
39
  `execute_sql`, expose approval/commit tools, or give the model write
38
40
  credentials.
39
41
 
42
+ `start --from-env` is the shortest public command for first-run onboarding.
43
+ `onboard db --from-env DATABASE_URL` is the same explicit path if you prefer the
44
+ older command name in scripts.
45
+
46
+ During the wizard, provide the optional sample object id if you know one safe
47
+ row in the selected table. Runner writes `./.synapsor/smoke-input.json` with
48
+ that id and prints the exact `smoke call` command. If you skip it, use
49
+ `--json '{"<lookup_arg>":"<real_id>"}'` when you are ready to test one real
50
+ row.
51
+
40
52
  The rest of this page shows the same flow step by step using the public
41
53
  `synapsor-runner ...` CLI. From a source checkout, use `./bin/synapsor-runner ...` if the
42
54
  global binary is not linked yet.
43
55
 
56
+ From a source checkout, `./scripts/use-your-db.sh` runs the same kind of
57
+ guided flow plus local repository checks.
58
+
44
59
  ## 1. Put the read URL in an environment variable
45
60
 
46
61
  Do not pass connection strings on the command line.
@@ -81,7 +96,7 @@ other trusted database TLS setup.
81
96
  ## 2. Inspect metadata
82
97
 
83
98
  ```bash
84
- npx -y -p @synapsor/runner@alpha synapsor-runner inspect \
99
+ npx -y -p @synapsor/runner synapsor-runner inspect \
85
100
  --engine auto \
86
101
  --from-env DATABASE_URL \
87
102
  --schema public
@@ -90,13 +105,13 @@ npx -y -p @synapsor/runner@alpha synapsor-runner inspect \
90
105
  For a disposable staging URL, this also works:
91
106
 
92
107
  ```bash
93
- npx -y -p @synapsor/runner@alpha synapsor-runner inspect "$DATABASE_URL" --engine auto --schema public
108
+ npx -y -p @synapsor/runner synapsor-runner inspect "$DATABASE_URL" --engine auto --schema public
94
109
  ```
95
110
 
96
111
  For automation:
97
112
 
98
113
  ```bash
99
- npx -y -p @synapsor/runner@alpha synapsor-runner inspect \
114
+ npx -y -p @synapsor/runner synapsor-runner inspect \
100
115
  --engine postgres \
101
116
  --from-env DATABASE_URL \
102
117
  --schema public \
@@ -113,9 +128,9 @@ to your staging table, primary key, tenant key, conflict column, visible fields,
113
128
  allowed write fields, and business limits.
114
129
 
115
130
  ```bash
116
- npx -y -p @synapsor/runner@alpha synapsor-runner recipes list
117
- npx -y -p @synapsor/runner@alpha synapsor-runner recipes show billing.late_fee_waiver
118
- npx -y -p @synapsor/runner@alpha synapsor-runner recipes init billing.late_fee_waiver --output synapsor.runner.json
131
+ npx -y -p @synapsor/runner synapsor-runner recipes list
132
+ npx -y -p @synapsor/runner synapsor-runner recipes show billing.late_fee_waiver
133
+ npx -y -p @synapsor/runner synapsor-runner recipes init billing.late_fee_waiver --output synapsor.runner.json
119
134
  ```
120
135
 
121
136
  Use a recipe when the shape is close. Use the guided wizard or explicit flags
@@ -130,7 +145,7 @@ only the capabilities in your generated `synapsor.runner.json`.
130
145
  In an interactive terminal, run the guided wizard:
131
146
 
132
147
  ```bash
133
- npx -y -p @synapsor/runner@alpha synapsor-runner init --from-env DATABASE_URL --mode read_only --wizard
148
+ npx -y -p @synapsor/runner synapsor-runner init --from-env DATABASE_URL --mode read_only --wizard
134
149
  ```
135
150
 
136
151
  The generated context and capabilities are based on your selections. Synapsor
@@ -143,7 +158,12 @@ trusted context -> capability -> MCP tool
143
158
 
144
159
  You choose the source object, trusted tenant/principal bindings, namespace,
145
160
  object name, lookup argument, visible fields, proposal fields, guards, and
146
- approval role.
161
+ approval role. You can also provide an optional real object id so Runner writes
162
+ `./.synapsor/smoke-input.json` for the first local tool call. If the read URL
163
+ env var and trusted tenant/principal env vars are already set, the wizard also
164
+ attempts that smoke call immediately and stores the evidence/query audit in the
165
+ local ledger. If not, it prints the exact smoke command to run after you set
166
+ the env vars from `.env.example`.
147
167
 
148
168
  Start with `read_only` to prove safe database reads first. Use `--mode review`
149
169
  when you are ready to create proposal tools and guarded writeback setup.
@@ -157,7 +177,12 @@ The wizard:
157
177
  - asks which tenant/scope column and backend session env vars are trusted;
158
178
  - asks you to confirm primary key, conflict/version column, visible columns,
159
179
  mode, semantic capability names, and proposal patch mappings;
160
- - previews the MCP tools and what is not exposed;
180
+ - asks review-mode users to choose direct guarded SQL writeback, an app-owned
181
+ HTTP handler, or an app-owned command handler;
182
+ - previews the MCP tools and what is not exposed, then lets you revise visible
183
+ fields or capability names before writing files;
184
+ - attempts a first smoke call when you supplied a real object id and the
185
+ required trusted env vars are present;
161
186
  - writes the generated config, `.env.example`, and MCP client snippets only
162
187
  after final confirmation.
163
188
 
@@ -174,7 +199,7 @@ If you already know the reviewed table/action, generate config directly from
174
199
  metadata and explicit flags:
175
200
 
176
201
  ```bash
177
- npx -y -p @synapsor/runner@alpha synapsor-runner init \
202
+ npx -y -p @synapsor/runner synapsor-runner init \
178
203
  --from-env DATABASE_URL \
179
204
  --engine postgres \
180
205
  --schema public \
@@ -184,23 +209,54 @@ npx -y -p @synapsor/runner@alpha synapsor-runner init \
184
209
  --mode review \
185
210
  --visible-columns id,tenant_id,late_fee_cents,waiver_reason,updated_at \
186
211
  --allowed-columns late_fee_cents,waiver_reason \
187
- --patch-fixed late_fee_cents=0 \
188
- --patch-from-arg waiver_reason=reason \
189
- --numeric-bound late_fee_cents=0:5500 \
212
+ --tenant-column tenant_id \
213
+ --id-arg invoice_id \
214
+ --patch late_fee_cents=fixed:0,waiver_reason=arg:reason \
215
+ --patch-bounds late_fee_cents=0:5500 \
190
216
  --write-url-env SYNAPSOR_DATABASE_WRITE_URL
191
217
  ```
192
218
 
219
+ If you omit `--namespace`, Runner derives the namespace from the table name
220
+ instead of defaulting to `source.*`. Add `--read-tool` and `--proposal-tool`
221
+ when you want exact capability names in the generated contract.
222
+
223
+ For app-owned writeback, replace the direct writer env with a handler executor:
224
+
225
+ ```bash
226
+ npx -y -p @synapsor/runner synapsor-runner init \
227
+ --from-env DATABASE_URL \
228
+ --engine postgres \
229
+ --schema public \
230
+ --table invoices \
231
+ --namespace billing \
232
+ --object-name invoice \
233
+ --mode review \
234
+ --tenant-column tenant_id \
235
+ --id-arg invoice_id \
236
+ --patch late_fee_cents=fixed:0,waiver_reason=arg:reason \
237
+ --writeback http_handler \
238
+ --handler-url-env APP_WRITEBACK_URL \
239
+ --handler-token-env APP_WRITEBACK_TOKEN \
240
+ --emit-handler
241
+ ```
242
+
243
+ Handler-owned configs mark the Runner source as read-only unless you explicitly
244
+ pass `--write-url-env`, so `config validate` does not warn that direct SQL
245
+ writeback is disabled.
246
+
247
+ Use `--writeback command_handler --handler-command-env APP_WRITEBACK_COMMAND`
248
+ when your app-owned writer is a local command/script instead of HTTP.
249
+
193
250
  Or generate from a saved inspection snapshot without reconnecting:
194
251
 
195
252
  ```bash
196
- npx -y -p @synapsor/runner@alpha synapsor-runner init \
253
+ npx -y -p @synapsor/runner synapsor-runner init \
197
254
  --inspection-json schema-inspection.json \
198
255
  --table invoices \
199
256
  --namespace billing \
200
257
  --object-name invoice \
201
258
  --mode review \
202
- --patch-fixed late_fee_cents=0 \
203
- --patch-from-arg waiver_reason=reason
259
+ --patch late_fee_cents=fixed:0,waiver_reason=arg:reason
204
260
  ```
205
261
 
206
262
  The command uses inspected metadata for primary-key, tenant-key, conflict-column,
@@ -208,15 +264,15 @@ and default-visible-column suggestions. If a suggestion is ambiguous or missing,
208
264
  pass explicit flags such as `--primary-key`, `--tenant-key`, and
209
265
  `--conflict-column`.
210
266
 
211
- Review mode requires at least one explicit `--patch-fixed` or
212
- `--patch-from-arg` mapping. Use `--mode read_only` if you only want an inspect
213
- tool.
267
+ Review mode requires at least one explicit `--patch` mapping, or the older
268
+ `--patch-fixed` / `--patch-from-arg` flags. Use `--mode read_only` if you only
269
+ want an inspect tool.
214
270
 
215
271
  For bounded business actions, add reviewed value guards:
216
272
 
217
273
  ```bash
218
- --numeric-bound credit_cents=0:10000
219
- --transition-guard status=open:pending_review
274
+ --patch-bounds credit_cents=0:10000
275
+ --status-guards status=open:pending_review
220
276
  ```
221
277
 
222
278
  `--numeric-bound` keeps a proposed numeric column inside a fixed range before a
@@ -234,6 +290,7 @@ Create `onboarding-selection.json` from one table and one safe business action.
234
290
  "version": 1,
235
291
  "engine": "postgres",
236
292
  "mode": "review",
293
+ "result_format": 2,
237
294
  "read_url_env": "SYNAPSOR_DATABASE_READ_URL",
238
295
  "write_url_env": "SYNAPSOR_DATABASE_WRITE_URL",
239
296
  "schema": "public",
@@ -243,6 +300,8 @@ Create `onboarding-selection.json` from one table and one safe business action.
243
300
  "conflict_column": "updated_at",
244
301
  "namespace": "billing",
245
302
  "object_name": "invoice",
303
+ "read_tool": "billing.inspect_invoice",
304
+ "proposal_tool": "billing.propose_late_fee_waiver",
246
305
  "visible_columns": ["id", "tenant_id", "late_fee_cents", "waiver_reason", "updated_at"],
247
306
  "allowed_columns": ["late_fee_cents", "waiver_reason"],
248
307
  "patch": {
@@ -261,7 +320,7 @@ contain database URLs or passwords.
261
320
  ## 6. Generate runner files
262
321
 
263
322
  ```bash
264
- npx -y -p @synapsor/runner@alpha synapsor-runner init \
323
+ npx -y -p @synapsor/runner synapsor-runner init \
265
324
  --spec onboarding-selection.json \
266
325
  --non-interactive
267
326
  ```
@@ -281,20 +340,30 @@ files.
281
340
  Generate or refresh MCP client snippets later with:
282
341
 
283
342
  ```bash
284
- npx -y -p @synapsor/runner@alpha synapsor-runner mcp config generic --config ./synapsor.runner.json --store ./.synapsor/local.db
285
- npx -y -p @synapsor/runner@alpha synapsor-runner mcp config claude-desktop --config ./synapsor.runner.json --store ./.synapsor/local.db
286
- npx -y -p @synapsor/runner@alpha synapsor-runner mcp config cursor --config ./synapsor.runner.json --store ./.synapsor/local.db
287
- npx -y -p @synapsor/runner@alpha synapsor-runner tools preview --config ./synapsor.runner.json --store ./.synapsor/local.db
343
+ npx -y -p @synapsor/runner synapsor-runner mcp config generic --config ./synapsor.runner.json --store ./.synapsor/local.db
344
+ npx -y -p @synapsor/runner synapsor-runner mcp config claude-desktop --config ./synapsor.runner.json --store ./.synapsor/local.db
345
+ npx -y -p @synapsor/runner synapsor-runner mcp config cursor --config ./synapsor.runner.json --store ./.synapsor/local.db
346
+ npx -y -p @synapsor/runner synapsor-runner tools preview --config ./synapsor.runner.json --store ./.synapsor/local.db
347
+ ```
348
+
349
+ Call one generated tool locally before wiring an MCP client:
350
+
351
+ ```bash
352
+ npx -y -p @synapsor/runner synapsor-runner smoke call --config ./synapsor.runner.json --store ./.synapsor/local.db
288
353
  ```
289
354
 
355
+ `smoke call` uses the same runtime as the MCP server. It records evidence and
356
+ query audit for read tools, or creates a proposal for proposal tools, then
357
+ prints the commands to inspect evidence/proposals/replay from the local store.
358
+
290
359
  The snippets contain the local command and args. They must not contain database
291
360
  URLs, passwords, approval tools, commit tools, or write credentials.
292
361
 
293
362
  ## 7. Validate the config
294
363
 
295
364
  ```bash
296
- npx -y -p @synapsor/runner@alpha synapsor-runner config validate --config synapsor.runner.json
297
- npx -y -p @synapsor/runner@alpha synapsor-runner config show --config synapsor.runner.json --redacted
365
+ npx -y -p @synapsor/runner synapsor-runner config validate --config synapsor.runner.json
366
+ npx -y -p @synapsor/runner synapsor-runner config show --config synapsor.runner.json --redacted
298
367
  ```
299
368
 
300
369
  The config stores environment-variable names, not connection-string values.
@@ -302,7 +371,7 @@ The config stores environment-variable names, not connection-string values.
302
371
  Run doctor after setting the referenced environment variables:
303
372
 
304
373
  ```bash
305
- npx -y -p @synapsor/runner@alpha synapsor-runner doctor --config synapsor.runner.json
374
+ npx -y -p @synapsor/runner synapsor-runner doctor --config synapsor.runner.json
306
375
  ```
307
376
 
308
377
  Doctor validates config shape, trusted context env vars, source env vars,
@@ -310,7 +379,7 @@ read/write credential separation, table/column metadata when the read URL is
310
379
  available, and the semantic MCP tool boundary. Use JSON for automation:
311
380
 
312
381
  ```bash
313
- npx -y -p @synapsor/runner@alpha synapsor-runner doctor --config synapsor.runner.json --json
382
+ npx -y -p @synapsor/runner synapsor-runner doctor --config synapsor.runner.json --json
314
383
  ```
315
384
 
316
385
  ## 8. Serve semantic MCP tools
@@ -320,25 +389,41 @@ Use stdio when a local MCP client can launch Synapsor Runner:
320
389
  ```bash
321
390
  export SYNAPSOR_TENANT_ID="acme"
322
391
  export SYNAPSOR_PRINCIPAL="local_operator"
323
- npx -y -p @synapsor/runner@alpha synapsor-runner mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db
392
+ npx -y -p @synapsor/runner synapsor-runner mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db
324
393
  ```
325
394
 
326
- Use HTTP when an app/server agent connects to a long-running Runner process:
395
+ Use Streamable HTTP when an app/server agent connects through a standard HTTP
396
+ MCP client:
327
397
 
328
398
  ```bash
329
399
  export SYNAPSOR_TENANT_ID="acme"
330
400
  export SYNAPSOR_PRINCIPAL="local_operator"
331
401
  export SYNAPSOR_RUNNER_HTTP_TOKEN="dev-local-token"
332
402
 
333
- npx -y -p @synapsor/runner@alpha synapsor-runner mcp serve-http \
403
+ npx -y -p @synapsor/runner synapsor-runner up --serve \
404
+ --config ./synapsor.runner.json \
405
+ --store ./.synapsor/local.db \
406
+ --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
407
+ ```
408
+
409
+ `up --serve` runs the review-mode checklist first, then starts Streamable HTTP.
410
+ Use `--dry-run` for the checklist only, or `--with-handler` when the config uses
411
+ an app-owned executor and you want Runner to check the handler endpoint before
412
+ serving.
413
+
414
+ The lower-level MCP command starts the same transport directly:
415
+
416
+ ```bash
417
+ npx -y -p @synapsor/runner synapsor-runner mcp serve-streamable-http \
334
418
  --config ./synapsor.runner.json \
335
419
  --store ./.synapsor/local.db \
336
420
  --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
337
421
  ```
338
422
 
339
- HTTP defaults to `127.0.0.1:8765` and requires bearer auth by default. Use
340
- private networking/TLS before exposing it beyond localhost. See
341
- [HTTP MCP](http-mcp.md).
423
+ Streamable HTTP defaults to `127.0.0.1:8766` and requires bearer auth by
424
+ default. Use private networking/TLS before exposing it beyond localhost. See
425
+ [HTTP MCP](http-mcp.md). If you want the smaller JSON-RPC bridge instead, use
426
+ `synapsor-runner mcp serve-http`.
342
427
 
343
428
  The model-facing MCP server exposes semantic tools such as:
344
429
 
@@ -357,10 +442,10 @@ tools, commit tools, database URLs, write credentials, or tenant authority.
357
442
  Proposal tools leave the source database unchanged. Review locally:
358
443
 
359
444
  ```bash
360
- npx -y -p @synapsor/runner@alpha synapsor-runner proposals list --store ./.synapsor/local.db
361
- npx -y -p @synapsor/runner@alpha synapsor-runner proposals show wrp_123 --store ./.synapsor/local.db
362
- npx -y -p @synapsor/runner@alpha synapsor-runner proposals approve wrp_123 --store ./.synapsor/local.db --actor local_reviewer --yes
363
- npx -y -p @synapsor/runner@alpha synapsor-runner proposals writeback-job wrp_123 --store ./.synapsor/local.db --output job.json
445
+ npx -y -p @synapsor/runner synapsor-runner proposals list --store ./.synapsor/local.db
446
+ npx -y -p @synapsor/runner synapsor-runner proposals show wrp_123 --store ./.synapsor/local.db
447
+ npx -y -p @synapsor/runner synapsor-runner proposals approve wrp_123 --store ./.synapsor/local.db --actor local_reviewer --yes
448
+ npx -y -p @synapsor/runner synapsor-runner proposals writeback-job wrp_123 --store ./.synapsor/local.db --output job.json
364
449
  ```
365
450
 
366
451
  Apply through the trusted worker path with a separate writer credential:
@@ -368,7 +453,7 @@ Apply through the trusted worker path with a separate writer credential:
368
453
  ```bash
369
454
  export SYNAPSOR_DATABASE_WRITE_URL="<postgres-or-mysql-write-url>"
370
455
  SYNAPSOR_ENGINE=postgres \
371
- npx -y -p @synapsor/runner@alpha synapsor-runner apply --job job.json --config synapsor.runner.json --store ./.synapsor/local.db
456
+ npx -y -p @synapsor/runner synapsor-runner apply --job job.json --config synapsor.runner.json --store ./.synapsor/local.db
372
457
  ```
373
458
 
374
459
  For `apply --job ... --config ...`, Runner reads the write credential from the
@@ -382,7 +467,7 @@ from environment variables, and the handler receives a structured proposal/job
382
467
  payload, not arbitrary model SQL:
383
468
 
384
469
  ```bash
385
- npx -y -p @synapsor/runner@alpha synapsor-runner apply --proposal wrp_123 --config synapsor.runner.json --store ./.synapsor/local.db
470
+ npx -y -p @synapsor/runner synapsor-runner apply --proposal wrp_123 --config synapsor.runner.json --store ./.synapsor/local.db
386
471
  ```
387
472
 
388
473
  See [Writeback Executors](writeback-executors.md).
@@ -390,8 +475,8 @@ See [Writeback Executors](writeback-executors.md).
390
475
  Replay afterward:
391
476
 
392
477
  ```bash
393
- npx -y -p @synapsor/runner@alpha synapsor-runner replay show wrp_123 --store ./.synapsor/local.db
394
- npx -y -p @synapsor/runner@alpha synapsor-runner replay export wrp_123 --store ./.synapsor/local.db --output replay.json
478
+ npx -y -p @synapsor/runner synapsor-runner replay show wrp_123 --store ./.synapsor/local.db
479
+ npx -y -p @synapsor/runner synapsor-runner replay export wrp_123 --store ./.synapsor/local.db --output replay.json
395
480
  ```
396
481
 
397
482
  ## Boundary
@@ -0,0 +1,228 @@
1
+ # App-Owned Handler Helper
2
+
3
+ Use the TypeScript handler helper when an approved Synapsor proposal should be
4
+ executed by your application service, not by Runner's direct SQL writer.
5
+
6
+ This is optional support code for your app-owned handler. Users install
7
+ `@synapsor/runner`; they do not install a separate handler package in the
8
+ current alpha.
9
+
10
+ The helper is the safe-by-default path for rich writes such as:
11
+
12
+ - inserting account-credit, refund-review, ticket, or ledger rows;
13
+ - updating multiple related rows inside your app transaction;
14
+ - applying business rules that belong in your application service.
15
+
16
+ The model-facing MCP tool still creates a proposal only. A human/operator
17
+ approves outside MCP. After approval, Runner sends the structured writeback
18
+ request to your handler.
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
+
27
+ ## Scope
28
+
29
+ Current alpha scope:
30
+
31
+ - TypeScript helper in `packages/handler`;
32
+ - bearer token verification;
33
+ - optional HMAC verification over the raw request body;
34
+ - typed request parsing;
35
+ - action dispatch;
36
+ - idempotency receipt lookup;
37
+ - transaction wrapper;
38
+ - `SELECT ... FOR UPDATE` target-row lock;
39
+ - tenant guard;
40
+ - expected-version stale-row guard;
41
+ - safe applied/conflict/failed receipts;
42
+ - no raw driver errors in HTTP responses.
43
+
44
+ Python helper is planned. For now, Python handlers should follow the documented
45
+ request/receipt schema and the FastAPI template in `examples/app-owned-writeback`.
46
+
47
+ ## Distribution Status
48
+
49
+ The helper implementation exists in this source repo under `packages/handler`
50
+ and is used by the app-owned executor example and tests. It is not published as
51
+ a standalone npm package yet.
52
+
53
+ If you installed `@synapsor/runner` from npm, use one of these alpha paths:
54
+
55
+ - generate a starter handler with `synapsor-runner handler template ...`;
56
+ - copy from `examples/app-owned-writeback/`;
57
+ - run `examples/mcp-postgres-billing-app-handler/`, which includes a bundled
58
+ `synapsor-handler.mjs` shim inside the runner package.
59
+
60
+ ## Schemas
61
+
62
+ Published schemas:
63
+
64
+ - `schemas/synapsor.app-handler-request.v1.json`
65
+ - `schemas/synapsor.app-handler-receipt.v1.json`
66
+
67
+ The helper accepts both the new `protocol_version: "1.0"` shape and the current
68
+ Runner `schema_version: "synapsor.handler-writeback.v1"` request shape during
69
+ the alpha migration.
70
+
71
+ ## TypeScript Usage From A Source Checkout
72
+
73
+ The source checkout has an internal helper under `packages/handler`. It is not
74
+ an npm install path. Import it by workspace-relative path while developing this
75
+ repository, or use the bundled example shim in the packaged runner.
76
+
77
+ ```ts
78
+ import { createWritebackHandler } from "../packages/handler/src/index.js";
79
+
80
+ export const handler = createWritebackHandler({
81
+ tokenEnv: "SYNAPSOR_APP_HANDLER_TOKEN",
82
+ signingSecretEnv: "SYNAPSOR_APP_HANDLER_SIGNING_SECRET",
83
+ source: {
84
+ engine: "postgres",
85
+ writeUrlEnv: "SYNAPSOR_APP_WRITE_URL",
86
+ receiptTable: { schema: "synapsor", table: "handler_receipts" }
87
+ },
88
+ capabilities: {
89
+ "support.propose_plan_credit": async (job, tx) => {
90
+ const creditId = `CR-${job.proposalId.slice(-12)}`;
91
+
92
+ await tx.insert("credits", {
93
+ id: creditId,
94
+ tenant_id: job.tenantId,
95
+ invoice_id: job.objectId,
96
+ amount_cents: Number(job.patch.credit_requested_cents),
97
+ reason: String(job.patch.credit_reason),
98
+ created_by: job.principal
99
+ });
100
+
101
+ await tx.update("invoices", {
102
+ id: job.objectId,
103
+ tenant_id: job.tenantId
104
+ }, {
105
+ credited_cents:
106
+ Number(job.row.credited_cents ?? 0) +
107
+ Number(job.patch.credit_requested_cents)
108
+ });
109
+
110
+ return {
111
+ rowsAffected: 2,
112
+ effects: [{ type: "db.insert", table: "credits", id: creditId }]
113
+ };
114
+ }
115
+ }
116
+ });
117
+ ```
118
+
119
+ Mount the returned handler at your app route, for example
120
+ `POST /synapsor/writeback`.
121
+
122
+ The handler author writes only the business effect. The helper owns the safety
123
+ loop around that effect.
124
+
125
+ ## What The Helper Enforces
126
+
127
+ The helper checks these before your business function can mutate state:
128
+
129
+ - the bearer token matches the configured environment variable;
130
+ - the optional HMAC signature is valid and fresh;
131
+ - the request protocol is supported;
132
+ - the action maps to a configured capability function;
133
+ - the target row exists inside the trusted tenant;
134
+ - the row version still matches the proposal's expected version;
135
+ - the idempotency key was not already applied.
136
+
137
+ If the row is missing or belongs to another tenant, the helper returns:
138
+
139
+ ```json
140
+ {
141
+ "status": "conflict",
142
+ "rows_affected": 0,
143
+ "source_database_mutated": false,
144
+ "safe_error_code": "ROW_NOT_FOUND_OR_WRONG_TENANT"
145
+ }
146
+ ```
147
+
148
+ If the row changed after proposal creation, the helper returns:
149
+
150
+ ```json
151
+ {
152
+ "status": "conflict",
153
+ "rows_affected": 0,
154
+ "source_database_mutated": false,
155
+ "safe_error_code": "ROW_CHANGED_AFTER_PROPOSAL"
156
+ }
157
+ ```
158
+
159
+ If your business function throws, the helper rolls back the transaction and
160
+ returns a safe failed receipt. Raw driver and exception text are not exposed to
161
+ the caller.
162
+
163
+ ## Runner-Side Signing Config
164
+
165
+ Configure the matching `http_handler` executor with the same signing-secret env
166
+ name:
167
+
168
+ ```json
169
+ {
170
+ "executors": {
171
+ "billing_handler": {
172
+ "type": "http_handler",
173
+ "url_env": "BILLING_WRITEBACK_URL",
174
+ "method": "POST",
175
+ "auth": {
176
+ "type": "bearer_env",
177
+ "token_env": "BILLING_WRITEBACK_TOKEN"
178
+ },
179
+ "signing_secret_env": "SYNAPSOR_APP_HANDLER_SIGNING_SECRET",
180
+ "timeout_ms": 5000
181
+ }
182
+ }
183
+ }
184
+ ```
185
+
186
+ When this field is set, Runner signs the exact request body and sends
187
+ `X-Synapsor-Signature`, `X-Synapsor-Issued-At`,
188
+ `X-Synapsor-Proposal-Id`, and `Idempotency-Key`. The helper verifies those
189
+ headers before parsing or applying the writeback request.
190
+
191
+ ## Signing
192
+
193
+ For loopback-only development, bearer auth may be enough. For any handler that
194
+ is reachable outside the local process, enable HMAC:
195
+
196
+ ```ts
197
+ createWritebackHandler({
198
+ tokenEnv: "SYNAPSOR_APP_HANDLER_TOKEN",
199
+ signingSecretEnv: "SYNAPSOR_APP_HANDLER_SIGNING_SECRET",
200
+ // ...
201
+ });
202
+ ```
203
+
204
+ Runner sends:
205
+
206
+ ```text
207
+ Authorization: Bearer <token>
208
+ X-Synapsor-Signature: sha256=<hmac>
209
+ X-Synapsor-Issued-At: <iso timestamp>
210
+ X-Synapsor-Proposal-Id: wrp_...
211
+ Idempotency-Key: wrp_...
212
+ ```
213
+
214
+ The HMAC is computed over the raw body. The helper enforces a short issued-at
215
+ skew window.
216
+
217
+ ## Receipt Storage
218
+
219
+ The helper's Postgres adapter stores idempotency receipts in a receipt table.
220
+ Prefer a dedicated schema, for example:
221
+
222
+ ```sql
223
+ CREATE SCHEMA IF NOT EXISTS synapsor;
224
+ GRANT USAGE, CREATE ON SCHEMA synapsor TO app_writeback_user;
225
+ ```
226
+
227
+ If your application already has a receipt/idempotency table, implement the
228
+ `WritebackHandlerDatabase` interface and pass it as `database`.