@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
@@ -56,6 +56,19 @@ application schema, create a dedicated schema/database for receipts where your
56
56
  database policy allows it, or use `http_handler`/`command_handler` so your
57
57
  application owns receipt storage and business writes.
58
58
 
59
+ Use the helper commands before enabling direct SQL writeback:
60
+
61
+ ```bash
62
+ npx -y -p @synapsor/runner synapsor-runner writeback doctor --config ./synapsor.runner.json
63
+ npx -y -p @synapsor/runner synapsor-runner writeback migration --engine postgres
64
+ npx -y -p @synapsor/runner synapsor-runner writeback grants --engine postgres --writer-role app_writer
65
+ ```
66
+
67
+ `writeback doctor --check-db` connects with the configured writer credential and
68
+ checks the receipt table path. That check can create the receipt table if the
69
+ writer has `CREATE`, so run it only against staging/disposable databases or
70
+ after reviewing the printed migration/grants.
71
+
59
72
  ## `http_handler`
60
73
 
61
74
  Use `http_handler` when your application/API should own business execution.
@@ -75,6 +88,7 @@ literal values.
75
88
  "type": "bearer_env",
76
89
  "token_env": "SYNAPSOR_BILLING_HANDLER_TOKEN"
77
90
  },
91
+ "signing_secret_env": "SYNAPSOR_BILLING_HANDLER_SIGNING_SECRET",
78
92
  "timeout_ms": 5000
79
93
  }
80
94
  },
@@ -91,7 +105,7 @@ literal values.
91
105
  Run after approval:
92
106
 
93
107
  ```bash
94
- npx -y -p @synapsor/runner@alpha synapsor-runner apply \
108
+ npx -y -p @synapsor/runner synapsor-runner apply \
95
109
  --proposal wrp_123 \
96
110
  --config ./synapsor.runner.json \
97
111
  --store ./.synapsor/local.db
@@ -101,6 +115,24 @@ The handler receives proposal fields, the exact patch, evidence metadata,
101
115
  guards, and an idempotency key. It does not receive arbitrary model SQL or DB
102
116
  credentials from Synapsor Runner.
103
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
+
125
+ When `signing_secret_env` is set, Runner signs the exact JSON body with HMAC
126
+ SHA-256 and sends:
127
+
128
+ - `X-Synapsor-Signature: sha256=...`
129
+ - `X-Synapsor-Issued-At: ...`
130
+ - `X-Synapsor-Proposal-Id: ...`
131
+ - `Idempotency-Key: ...`
132
+
133
+ Use signing for any handler that is not strictly loopback-only and protected by
134
+ another trusted boundary.
135
+
104
136
  Handler responses:
105
137
 
106
138
  ```json
@@ -126,6 +158,56 @@ receipt is stored in replay.
126
158
  Use your application/API for business logic. Use Synapsor Runner for proposal,
127
159
  approval, evidence, policy boundary, and replay.
128
160
 
161
+ For TypeScript services, use the source-level helper in `packages/handler`.
162
+ It verifies bearer/HMAC auth, parses the request, locks the target row with the
163
+ tenant guard, checks the expected version, handles idempotency, wraps the
164
+ business effect in a transaction, and returns safe receipts without raw driver
165
+ errors. See [Handler Helper](handler-helper.md).
166
+
167
+ This is the recommended path for writes that are richer than the current
168
+ `sql_update` scope, such as:
169
+
170
+ - creating a refund review;
171
+ - inserting an account credit row;
172
+ - opening a support ticket;
173
+ - updating multiple related rows in one app transaction.
174
+
175
+ Starter templates are in:
176
+
177
+ ```text
178
+ examples/app-owned-writeback/
179
+ ```
180
+
181
+ Concrete business-action examples are in:
182
+
183
+ ```text
184
+ examples/app-owned-writeback/business-actions.md
185
+ ```
186
+
187
+ The full disposable Postgres account-credit demo is in:
188
+
189
+ ```text
190
+ examples/mcp-postgres-billing-app-handler/
191
+ ```
192
+
193
+ It proves the rich-write path end to end: the model creates a proposal, the
194
+ source DB is unchanged before approval, the app-owned handler inserts an
195
+ `account_credits` row after approval, retry is idempotent, and replay stores the
196
+ handler receipt.
197
+
198
+ Or generate one into your app:
199
+
200
+ ```bash
201
+ npx -y -p @synapsor/runner synapsor-runner handler template node-fastify \
202
+ --output ./synapsor-writeback-handler.mjs
203
+
204
+ npx -y -p @synapsor/runner synapsor-runner handler template python-fastapi \
205
+ --output ./synapsor_writeback_handler.py
206
+
207
+ npx -y -p @synapsor/runner synapsor-runner handler template command \
208
+ --output ./synapsor-command-handler.mjs
209
+ ```
210
+
129
211
  ## `command_handler`
130
212
 
131
213
  `command_handler` is a local integration path for scripts:
@@ -145,6 +227,15 @@ approval, evidence, policy boundary, and replay.
145
227
  The command receives the same structured JSON request on stdin and should print
146
228
  a JSON receipt body on stdout.
147
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
+
236
+ Use `examples/app-owned-writeback/command-handler.mjs` as a starting point when
237
+ your safest apply path is an app script or job runner.
238
+
148
239
  ## Safety Boundary
149
240
 
150
241
  Executor secrets are never exposed over MCP. The model never receives:
@@ -0,0 +1,128 @@
1
+ # App-Owned Writeback Templates
2
+
3
+ Use app-owned writeback when an approved Synapsor proposal should be executed
4
+ by your application service instead of Runner writing SQL directly.
5
+
6
+ This is the right path for rich business actions:
7
+
8
+ - create a refund review;
9
+ - insert an account credit row;
10
+ - open a support ticket;
11
+ - update multiple related rows through your app service.
12
+
13
+ The model-facing MCP tool still only creates a proposal. Approval happens
14
+ outside MCP. After approval, `synapsor-runner apply` sends a structured request
15
+ to your handler, and the handler returns an execution receipt for replay.
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
+
24
+ ## Config Snippet
25
+
26
+ Add an executor and point one proposal capability at it:
27
+
28
+ ```json
29
+ {
30
+ "executors": {
31
+ "app_writeback_api": {
32
+ "type": "http_handler",
33
+ "url_env": "SYNAPSOR_APP_WRITEBACK_URL",
34
+ "method": "POST",
35
+ "auth": {
36
+ "type": "bearer_env",
37
+ "token_env": "SYNAPSOR_APP_WRITEBACK_TOKEN"
38
+ },
39
+ "signing_secret_env": "SYNAPSOR_APP_WRITEBACK_SIGNING_SECRET",
40
+ "timeout_ms": 5000
41
+ }
42
+ },
43
+ "capabilities": [
44
+ {
45
+ "name": "refunds.propose_refund_review",
46
+ "kind": "proposal",
47
+ "executor": "app_writeback_api"
48
+ }
49
+ ]
50
+ }
51
+ ```
52
+
53
+ Run after approval:
54
+
55
+ ```bash
56
+ export SYNAPSOR_APP_WRITEBACK_URL="http://127.0.0.1:8787/synapsor/writeback"
57
+ export SYNAPSOR_APP_WRITEBACK_TOKEN="dev-handler-token"
58
+
59
+ synapsor-runner apply \
60
+ --proposal wrp_... \
61
+ --config ./synapsor.runner.json \
62
+ --store ./.synapsor/local.db
63
+ ```
64
+
65
+ ## Handler Request Shape
66
+
67
+ Your handler receives JSON like:
68
+
69
+ ```json
70
+ {
71
+ "schema_version": "synapsor.handler-writeback.v1",
72
+ "writeback_job_id": "hwb_wrp_...",
73
+ "proposal_id": "wrp_...",
74
+ "idempotency_key": "wrp_...",
75
+ "change_set": {
76
+ "action": "refunds.propose_refund_review",
77
+ "scope": {
78
+ "tenant_id": "acme",
79
+ "object_id": "INV-3001"
80
+ },
81
+ "before": {},
82
+ "patch": {},
83
+ "after": {},
84
+ "guards": {},
85
+ "evidence": {
86
+ "bundle_id": "ev_..."
87
+ }
88
+ },
89
+ "executor": "app_writeback_api",
90
+ "dry_run": false
91
+ }
92
+ ```
93
+
94
+ Do not trust request fields blindly. Your service should re-check tenant,
95
+ principal, authorization, idempotency, row versions, and business rules before
96
+ mutating state.
97
+
98
+ ## Handler Response Shape
99
+
100
+ Return one terminal receipt:
101
+
102
+ ```json
103
+ {
104
+ "status": "applied",
105
+ "rows_affected": 1,
106
+ "previous_version": "2026-06-20T14:31:08Z",
107
+ "new_version": "2026-06-20T14:34:19Z",
108
+ "source_database_mutated": true
109
+ }
110
+ ```
111
+
112
+ Allowed statuses:
113
+
114
+ - `applied`
115
+ - `already_applied`
116
+ - `conflict`
117
+ - `failed`
118
+
119
+ ## Templates
120
+
121
+ - `node-fastify-handler.mjs`: HTTP handler template for a Node/Fastify service.
122
+ - `python-fastapi-handler.py`: HTTP handler template for a Python/FastAPI service.
123
+ - `command-handler.mjs`: local command handler template for scripts.
124
+ - `business-actions.md`: concrete examples for refund reviews, account
125
+ credits, support tickets, and multi-row app transactions.
126
+
127
+ Each template returns safe demo receipts and marks where your application
128
+ should run its own transaction.
@@ -0,0 +1,221 @@
1
+ # App-Owned Business Action Examples
2
+
3
+ These examples show the kinds of approved proposals an app-owned handler can
4
+ apply. They are intentionally not direct SQL examples. Your application service
5
+ owns the write transaction, re-checks authorization and row/version guards, and
6
+ returns a terminal receipt to Synapsor Runner.
7
+
8
+ Each request uses the same boundary:
9
+
10
+ ```text
11
+ model-facing MCP tool -> proposal
12
+ human/operator approval -> app-owned handler
13
+ handler transaction -> applied/conflict/failed receipt
14
+ local replay
15
+ ```
16
+
17
+ The handler must not trust request fields blindly. Re-check tenant,
18
+ principal/role, idempotency, row versions, and business policy before mutating
19
+ state.
20
+
21
+ ## Create A Refund Review
22
+
23
+ Use this when an agent may request a refund review, but your application must
24
+ create the review record through normal business logic.
25
+
26
+ Capability:
27
+
28
+ ```text
29
+ refunds.propose_refund_review(order_id, reason, requested_amount_cents)
30
+ ```
31
+
32
+ Handler request:
33
+
34
+ ```json
35
+ {
36
+ "schema_version": "synapsor.handler-writeback.v1",
37
+ "writeback_job_id": "hwb_wrp_refund_001",
38
+ "proposal_id": "wrp_refund_001",
39
+ "idempotency_key": "wrp_refund_001",
40
+ "change_set": {
41
+ "action": "refunds.propose_refund_review",
42
+ "scope": {
43
+ "tenant_id": "acme",
44
+ "principal": "support_lead@example.com",
45
+ "object_type": "order",
46
+ "object_id": "ORD-3001"
47
+ },
48
+ "before": {
49
+ "order_status": "delivered",
50
+ "refund_review_id": null
51
+ },
52
+ "patch": {
53
+ "requested_amount_cents": 2500,
54
+ "reason": "duplicate charge"
55
+ },
56
+ "after": {
57
+ "refund_review_status": "pending_review"
58
+ },
59
+ "guards": {
60
+ "expected_version": {
61
+ "column": "updated_at",
62
+ "value": "2026-06-20T14:31:08Z"
63
+ }
64
+ },
65
+ "evidence": {
66
+ "bundle_id": "ev_refund_001"
67
+ }
68
+ },
69
+ "executor": "app_writeback_api",
70
+ "dry_run": false
71
+ }
72
+ ```
73
+
74
+ Handler transaction sketch:
75
+
76
+ ```text
77
+ BEGIN
78
+ verify principal can request refunds for tenant acme
79
+ verify order ORD-3001 still belongs to tenant acme
80
+ verify order.updated_at still matches expected_version
81
+ verify no receipt exists for idempotency_key
82
+ INSERT INTO refund_reviews (...)
83
+ INSERT INTO synapsor_app_receipts (...)
84
+ COMMIT
85
+ ```
86
+
87
+ Receipt:
88
+
89
+ ```json
90
+ {
91
+ "status": "applied",
92
+ "rows_affected": 1,
93
+ "new_object_id": "RR-9001",
94
+ "source_database_mutated": true
95
+ }
96
+ ```
97
+
98
+ ## Insert An Account Credit Row
99
+
100
+ Use this when the safe write is an append-only ledger/accounting operation.
101
+
102
+ Capability:
103
+
104
+ ```text
105
+ credits.propose_account_credit(customer_id, amount_cents, reason)
106
+ ```
107
+
108
+ Handler transaction sketch:
109
+
110
+ ```text
111
+ BEGIN
112
+ verify customer belongs to trusted tenant
113
+ verify amount is within app policy
114
+ verify idempotency_key has not already created a credit
115
+ INSERT INTO account_credits (...)
116
+ UPDATE customers SET credit_balance_cents = credit_balance_cents + amount
117
+ INSERT INTO synapsor_app_receipts (...)
118
+ COMMIT
119
+ ```
120
+
121
+ Conflict receipt if the app policy no longer allows the credit:
122
+
123
+ ```json
124
+ {
125
+ "status": "conflict",
126
+ "safe_error_code": "CREDIT_POLICY_CHANGED",
127
+ "source_database_mutated": false,
128
+ "details": {
129
+ "reason": "customer credit limit changed after proposal"
130
+ }
131
+ }
132
+ ```
133
+
134
+ ## Open A Support Ticket
135
+
136
+ Use this when the agent may propose opening a ticket, but ticket creation must
137
+ go through your helpdesk/application service.
138
+
139
+ Capability:
140
+
141
+ ```text
142
+ support.propose_open_ticket(customer_id, subject, body)
143
+ ```
144
+
145
+ Handler transaction sketch:
146
+
147
+ ```text
148
+ BEGIN
149
+ verify principal can open support tickets for tenant
150
+ verify customer belongs to tenant
151
+ validate subject/body against app policy
152
+ INSERT INTO support_tickets (...)
153
+ INSERT INTO ticket_events (...)
154
+ INSERT INTO synapsor_app_receipts (...)
155
+ COMMIT
156
+ ```
157
+
158
+ Receipt:
159
+
160
+ ```json
161
+ {
162
+ "status": "applied",
163
+ "rows_affected": 2,
164
+ "new_object_id": "T-9100",
165
+ "source_database_mutated": true
166
+ }
167
+ ```
168
+
169
+ ## Update Multiple Related Rows
170
+
171
+ Use this when an approved business action spans several tables and should stay
172
+ inside your normal application transaction.
173
+
174
+ Capability:
175
+
176
+ ```text
177
+ subscriptions.propose_trial_extension(customer_id, extension_days, reason)
178
+ ```
179
+
180
+ Handler transaction sketch:
181
+
182
+ ```text
183
+ BEGIN
184
+ verify tenant/principal authorization
185
+ SELECT subscription FOR UPDATE
186
+ verify expected subscription version
187
+ UPDATE subscriptions SET trial_ends_at = ...
188
+ INSERT INTO customer_events (...)
189
+ INSERT INTO billing_notes (...)
190
+ INSERT INTO synapsor_app_receipts (...)
191
+ COMMIT
192
+ ```
193
+
194
+ Receipt:
195
+
196
+ ```json
197
+ {
198
+ "status": "applied",
199
+ "rows_affected": 3,
200
+ "previous_version": "2026-06-20T14:31:08Z",
201
+ "new_version": "2026-06-20T14:34:19Z",
202
+ "source_database_mutated": true
203
+ }
204
+ ```
205
+
206
+ ## Idempotent Retry
207
+
208
+ If the same `idempotency_key` reaches your handler again after a successful
209
+ apply, return `already_applied` and the original receipt details rather than
210
+ running the transaction again.
211
+
212
+ ```json
213
+ {
214
+ "status": "already_applied",
215
+ "rows_affected": 0,
216
+ "source_database_mutated": false,
217
+ "details": {
218
+ "original_receipt_id": "rct_abc123"
219
+ }
220
+ }
221
+ ```
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+
3
+ const chunks = [];
4
+ for await (const chunk of process.stdin) chunks.push(chunk);
5
+
6
+ const request = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
7
+ const changeSet = request.change_set || {};
8
+
9
+ if (!request.proposal_id || !request.idempotency_key || !changeSet.scope?.tenant_id) {
10
+ process.stdout.write(JSON.stringify({
11
+ status: "failed",
12
+ safe_error_code: "BAD_WRITEBACK_REQUEST",
13
+ source_database_mutated: false,
14
+ }));
15
+ process.exit(0);
16
+ }
17
+
18
+ if (request.dry_run) {
19
+ process.stdout.write(JSON.stringify({
20
+ status: "applied",
21
+ rows_affected: 0,
22
+ source_database_mutated: false,
23
+ details: { dry_run: true },
24
+ }));
25
+ process.exit(0);
26
+ }
27
+
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
+ *
38
+ * Put your app-owned command transaction here.
39
+ *
40
+ * Examples:
41
+ * - call an internal service;
42
+ * - enqueue a review job;
43
+ * - run an app migration-safe script that uses your normal ORM.
44
+ *
45
+ * Re-check tenant/principal authorization, idempotency, row/version guards,
46
+ * and business policy before mutating application state.
47
+ */
48
+
49
+ process.stdout.write(JSON.stringify({
50
+ status: "applied",
51
+ rows_affected: 1,
52
+ previous_version: String(changeSet.guards?.expected_version?.value || ""),
53
+ new_version: new Date().toISOString(),
54
+ source_database_mutated: true,
55
+ }));
@@ -0,0 +1,64 @@
1
+ import Fastify from "fastify";
2
+
3
+ const port = Number(process.env.PORT || 8787);
4
+ const expectedToken = process.env.SYNAPSOR_APP_WRITEBACK_TOKEN || "dev-handler-token";
5
+
6
+ const app = Fastify({ logger: true });
7
+
8
+ app.post("/synapsor/writeback", async (request, reply) => {
9
+ const auth = request.headers.authorization || "";
10
+ if (auth !== `Bearer ${expectedToken}`) {
11
+ return reply.code(401).send({ status: "failed", error_code: "UNAUTHORIZED" });
12
+ }
13
+
14
+ const body = request.body || {};
15
+ const changeSet = body.change_set || {};
16
+
17
+ if (!body.proposal_id || !body.idempotency_key || !changeSet.scope?.tenant_id) {
18
+ return reply.code(400).send({ status: "failed", error_code: "BAD_WRITEBACK_REQUEST" });
19
+ }
20
+
21
+ if (body.dry_run) {
22
+ return {
23
+ status: "applied",
24
+ rows_affected: 0,
25
+ source_database_mutated: false,
26
+ details: { dry_run: true },
27
+ };
28
+ }
29
+
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
+ *
40
+ * Put your app-owned transaction here.
41
+ *
42
+ * Examples:
43
+ * - insert a refund_review row;
44
+ * - insert an account_credit row;
45
+ * - open a support_ticket row;
46
+ * - update invoice + ledger rows together.
47
+ *
48
+ * Re-check:
49
+ * - tenant and principal authorization;
50
+ * - idempotency_key has not already been applied;
51
+ * - row/version guards still match;
52
+ * - requested business action is allowed by your app policy.
53
+ */
54
+
55
+ return {
56
+ status: "applied",
57
+ rows_affected: 1,
58
+ previous_version: String(changeSet.guards?.expected_version?.value || ""),
59
+ new_version: new Date().toISOString(),
60
+ source_database_mutated: true,
61
+ };
62
+ });
63
+
64
+ app.listen({ host: "127.0.0.1", port });
@@ -0,0 +1,66 @@
1
+ import os
2
+ from datetime import datetime, timezone
3
+
4
+ from fastapi import FastAPI, Header, HTTPException
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class HandlerRequest(BaseModel):
9
+ schema_version: str
10
+ writeback_job_id: str
11
+ proposal_id: str
12
+ idempotency_key: str
13
+ change_set: dict
14
+ executor: str | None = None
15
+ dry_run: bool = False
16
+
17
+
18
+ app = FastAPI()
19
+ EXPECTED_TOKEN = os.environ.get("SYNAPSOR_APP_WRITEBACK_TOKEN", "dev-handler-token")
20
+
21
+
22
+ @app.post("/synapsor/writeback")
23
+ def writeback(request: HandlerRequest, authorization: str | None = Header(default=None)):
24
+ if authorization != f"Bearer {EXPECTED_TOKEN}":
25
+ raise HTTPException(status_code=401, detail="UNAUTHORIZED")
26
+
27
+ scope = request.change_set.get("scope", {})
28
+ if not scope.get("tenant_id"):
29
+ raise HTTPException(status_code=400, detail="BAD_WRITEBACK_REQUEST")
30
+
31
+ if request.dry_run:
32
+ return {
33
+ "status": "applied",
34
+ "rows_affected": 0,
35
+ "source_database_mutated": False,
36
+ "details": {"dry_run": True},
37
+ }
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
+ #
48
+ # Put your app-owned transaction here.
49
+ #
50
+ # Examples:
51
+ # - insert a refund_review row;
52
+ # - insert an account_credit row;
53
+ # - open a support_ticket row;
54
+ # - update invoice + ledger rows together.
55
+ #
56
+ # Re-check tenant/principal authorization, idempotency, row/version guards,
57
+ # and business policy before mutating application state.
58
+
59
+ expected = request.change_set.get("guards", {}).get("expected_version", {})
60
+ return {
61
+ "status": "applied",
62
+ "rows_affected": 1,
63
+ "previous_version": str(expected.get("value", "")),
64
+ "new_version": datetime.now(timezone.utc).isoformat(),
65
+ "source_database_mutated": True,
66
+ }
@@ -0,0 +1,6 @@
1
+ .PHONY: config
2
+
3
+ config:
4
+ cd ../.. && corepack pnpm runner mcp config claude-desktop \
5
+ --config ./examples/mcp-postgres-billing/synapsor.runner.json \
6
+ --store ./.synapsor/local.db