@synapsor/runner 0.1.0-alpha.11 → 0.1.0-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +166 -23
- package/dist/cli.d.ts.map +1 -1
- package/dist/runner.mjs +855 -66
- package/docs/README.md +21 -0
- package/docs/app-owned-executors.md +5 -0
- package/docs/capability-authoring.md +265 -0
- package/docs/doctor.md +98 -0
- package/docs/handler-helper.md +200 -0
- package/docs/local-mode.md +13 -2
- package/docs/release-notes.md +47 -2
- package/docs/release-policy.md +86 -0
- package/docs/result-envelope-v2.md +148 -0
- package/docs/rfcs/001-result-envelope-v2.md +143 -0
- package/docs/rfcs/002-app-owned-handler-helper.md +161 -0
- package/docs/rfcs/003-integrator-feedback-teardown.md +97 -0
- package/docs/store-lifecycle.md +83 -0
- package/docs/writeback-executors.md +18 -0
- package/examples/app-owned-writeback/README.md +1 -0
- package/examples/mcp-postgres-billing-app-handler/README.md +6 -2
- package/examples/mcp-postgres-billing-app-handler/app-handler.mjs +77 -149
- package/examples/mcp-postgres-billing-app-handler/scripts/run-demo.sh +1 -0
- package/examples/mcp-postgres-billing-app-handler/synapsor-handler.mjs +437 -0
- package/examples/mcp-postgres-billing-app-handler/synapsor.runner.json +1 -0
- package/package.json +2 -1
- package/schemas/change-set.v1.schema.json +140 -0
- package/schemas/execution-receipt.v1.schema.json +34 -0
- package/schemas/onboarding-selection.v1.schema.json +125 -0
- package/schemas/runner-registration.v1.schema.json +48 -0
- package/schemas/synapsor.app-handler-receipt.v1.json +39 -0
- package/schemas/synapsor.app-handler-request.v1.json +119 -0
- package/schemas/synapsor.runner.schema.json +412 -0
- package/schemas/writeback-job.v1.schema.json +121 -0
package/docs/README.md
CHANGED
|
@@ -16,6 +16,18 @@ detail.
|
|
|
16
16
|
service for app/server agents.
|
|
17
17
|
- [OpenAI Agents SDK](openai-agents-sdk.md): use Streamable HTTP MCP with
|
|
18
18
|
OpenAI-safe tool aliases.
|
|
19
|
+
- [Capability Authoring](capability-authoring.md): define read/proposal
|
|
20
|
+
capabilities, model-facing descriptions, result envelopes, trusted context,
|
|
21
|
+
and writeback guards. JSON Schema:
|
|
22
|
+
`../schemas/synapsor.runner.schema.json`.
|
|
23
|
+
- [Result Envelope v2](result-envelope-v2.md): the opt-in
|
|
24
|
+
`ok`/`summary`/`data`/`proposal`/`error` response shape for MCP tools.
|
|
25
|
+
- [Handler Helper](handler-helper.md): TypeScript helper for safe app-owned
|
|
26
|
+
rich-write handlers.
|
|
27
|
+
- RFC source context:
|
|
28
|
+
[001 result envelope](rfcs/001-result-envelope-v2.md),
|
|
29
|
+
[002 handler helper](rfcs/002-app-owned-handler-helper.md),
|
|
30
|
+
[003 integrator teardown](rfcs/003-integrator-feedback-teardown.md).
|
|
19
31
|
|
|
20
32
|
## Safety And Operations
|
|
21
33
|
|
|
@@ -25,14 +37,20 @@ detail.
|
|
|
25
37
|
- [Cloud Mode](cloud-mode.md): what stays local and what Cloud-linked mode adds.
|
|
26
38
|
- [Release Notes](release-notes.md): alpha behavior, breaking changes, and the
|
|
27
39
|
stable release policy.
|
|
40
|
+
- [Release Policy](release-policy.md): alpha expectations, stable gates,
|
|
41
|
+
result envelope migration, and publish verification.
|
|
28
42
|
- [Licensing](licensing.md): Apache-2.0 scope, trademark boundary, and what is
|
|
29
43
|
not included in this runner repo.
|
|
30
44
|
- [Dependency License Inventory](dependency-license-inventory.md): current
|
|
31
45
|
dependency license summary for release review.
|
|
32
46
|
- [Troubleshooting First Run](troubleshooting-first-run.md): common setup
|
|
33
47
|
failures and fixes.
|
|
48
|
+
- [Doctor](doctor.md): redacted setup checks, handler probes, direct SQL
|
|
49
|
+
writeback probes, and receipt-table guidance.
|
|
34
50
|
- [Local Mode](local-mode.md): local store, proposals, approval, replay, and
|
|
35
51
|
writeback flow.
|
|
52
|
+
- [Store Lifecycle](store-lifecycle.md): active-store leases, prune safety,
|
|
53
|
+
deleted-store behavior, and concurrent server guardrails.
|
|
36
54
|
|
|
37
55
|
## Features
|
|
38
56
|
|
|
@@ -42,6 +60,9 @@ detail.
|
|
|
42
60
|
for approved proposals.
|
|
43
61
|
- [App-Owned Executors](app-owned-executors.md): short entry point for rich
|
|
44
62
|
business transactions handled by your app.
|
|
63
|
+
- `synapsor-runner events tail`: local lifecycle events such as
|
|
64
|
+
`proposal_created`, `proposal_approved`, `writeback_applied`, and
|
|
65
|
+
`writeback_conflict`.
|
|
45
66
|
|
|
46
67
|
Useful examples:
|
|
47
68
|
|
|
@@ -19,3 +19,8 @@ the receipt, and includes the result in replay.
|
|
|
19
19
|
|
|
20
20
|
Do not use generic SQL for rich business transactions. Let the model propose,
|
|
21
21
|
let Synapsor Runner approve/replay, and let your app execute the transaction.
|
|
22
|
+
|
|
23
|
+
For TypeScript services, prefer the first-party helper in `packages/handler`.
|
|
24
|
+
It enforces bearer/HMAC auth, tenant scope, expected-version guards,
|
|
25
|
+
idempotency, transaction rollback, and safe receipt formatting around your
|
|
26
|
+
business effect. See [Handler Helper](handler-helper.md).
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# Capability Authoring
|
|
2
|
+
|
|
3
|
+
Use `synapsor.runner.json` to define the database actions an MCP client can see.
|
|
4
|
+
The model sees semantic capabilities such as `billing.inspect_invoice`, not raw
|
|
5
|
+
SQL, table names, write credentials, approval tools, or commit tools.
|
|
6
|
+
|
|
7
|
+
For editor validation, use the JSON Schema:
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
schemas/synapsor.runner.schema.json
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Minimal Shape
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"version": 1,
|
|
18
|
+
"mode": "review",
|
|
19
|
+
"result_format": 2,
|
|
20
|
+
"storage": { "sqlite_path": "./.synapsor/local.db" },
|
|
21
|
+
"sources": {
|
|
22
|
+
"app_postgres": {
|
|
23
|
+
"engine": "postgres",
|
|
24
|
+
"read_url_env": "DATABASE_URL",
|
|
25
|
+
"write_url_env": "SYNAPSOR_DATABASE_WRITE_URL",
|
|
26
|
+
"statement_timeout_ms": 3000
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"trusted_context": {
|
|
30
|
+
"provider": "environment",
|
|
31
|
+
"values": {
|
|
32
|
+
"tenant_id_env": "SYNAPSOR_TENANT_ID",
|
|
33
|
+
"principal_env": "SYNAPSOR_PRINCIPAL"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"capabilities": []
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`result_format: 2` makes every MCP tool call return one envelope:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"ok": true,
|
|
45
|
+
"summary": "Created proposal wrp_123. Source database changed: no.",
|
|
46
|
+
"action": "billing.propose_late_fee_waiver",
|
|
47
|
+
"kind": "proposal",
|
|
48
|
+
"data": null,
|
|
49
|
+
"proposal": {},
|
|
50
|
+
"error": null,
|
|
51
|
+
"evidence": {
|
|
52
|
+
"bundle_id": "ev_123",
|
|
53
|
+
"note": "audit/replay handle; you do not need to act on it during this turn"
|
|
54
|
+
},
|
|
55
|
+
"source_database_changed": false,
|
|
56
|
+
"_meta": {
|
|
57
|
+
"canonical_capability": "billing.propose_late_fee_waiver"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Use `--result-format v2` on `mcp serve` or `mcp serve-streamable-http` if you
|
|
63
|
+
want to opt in from the command line instead of config.
|
|
64
|
+
|
|
65
|
+
## Read Capability
|
|
66
|
+
|
|
67
|
+
Read capabilities inspect one scoped row or view and save evidence/query-audit
|
|
68
|
+
records locally.
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"name": "billing.inspect_invoice",
|
|
73
|
+
"kind": "read",
|
|
74
|
+
"description": "Inspect one invoice in the trusted tenant before proposing a waiver or credit.",
|
|
75
|
+
"returns_hint": "Returns invoice amount, late fee, status, policy facts, and an audit evidence handle.",
|
|
76
|
+
"source": "app_postgres",
|
|
77
|
+
"target": {
|
|
78
|
+
"schema": "public",
|
|
79
|
+
"table": "invoices",
|
|
80
|
+
"primary_key": "id",
|
|
81
|
+
"tenant_key": "tenant_id"
|
|
82
|
+
},
|
|
83
|
+
"args": {
|
|
84
|
+
"invoice_id": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"required": true,
|
|
87
|
+
"max_length": 128,
|
|
88
|
+
"description": "Invoice id, e.g. INV-3001."
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"lookup": { "id_from_arg": "invoice_id" },
|
|
92
|
+
"visible_columns": ["id", "tenant_id", "status", "late_fee_cents", "updated_at"],
|
|
93
|
+
"evidence": "required",
|
|
94
|
+
"max_rows": 1
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Model-facing descriptions matter. They should explain when to use the tool and
|
|
99
|
+
what the result contains. Runner also adds evidence-handle guidance so the model
|
|
100
|
+
does not waste a turn trying to call an audit handle.
|
|
101
|
+
|
|
102
|
+
## Proposal Capability
|
|
103
|
+
|
|
104
|
+
Proposal capabilities create an exact before/after diff. They do not mutate your
|
|
105
|
+
source database. Approval and writeback stay outside the model-facing MCP tool
|
|
106
|
+
surface.
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"name": "billing.propose_late_fee_waiver",
|
|
111
|
+
"kind": "proposal",
|
|
112
|
+
"description": "Propose waiving one invoice late fee after inspecting invoice and policy evidence.",
|
|
113
|
+
"returns_hint": "Returns a review-required proposal id, exact field diff, evidence handle, and source_database_changed:false.",
|
|
114
|
+
"source": "app_postgres",
|
|
115
|
+
"target": {
|
|
116
|
+
"schema": "public",
|
|
117
|
+
"table": "invoices",
|
|
118
|
+
"primary_key": "id",
|
|
119
|
+
"tenant_key": "tenant_id"
|
|
120
|
+
},
|
|
121
|
+
"args": {
|
|
122
|
+
"invoice_id": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"required": true,
|
|
125
|
+
"description": "Invoice id, e.g. INV-3001."
|
|
126
|
+
},
|
|
127
|
+
"reason": {
|
|
128
|
+
"type": "string",
|
|
129
|
+
"required": true,
|
|
130
|
+
"max_length": 500,
|
|
131
|
+
"description": "Business reason for the proposed waiver."
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
"lookup": { "id_from_arg": "invoice_id" },
|
|
135
|
+
"visible_columns": ["id", "tenant_id", "status", "late_fee_cents", "waiver_reason", "updated_at"],
|
|
136
|
+
"patch": {
|
|
137
|
+
"late_fee_cents": { "fixed": 0 },
|
|
138
|
+
"waiver_reason": { "from_arg": "reason" }
|
|
139
|
+
},
|
|
140
|
+
"allowed_columns": ["late_fee_cents", "waiver_reason"],
|
|
141
|
+
"numeric_bounds": {
|
|
142
|
+
"late_fee_cents": { "minimum": 0, "maximum": 10000 }
|
|
143
|
+
},
|
|
144
|
+
"conflict_guard": { "column": "updated_at" },
|
|
145
|
+
"approval": { "mode": "human", "required_role": "billing_lead" }
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Trusted Context
|
|
150
|
+
|
|
151
|
+
Tenant, principal, approval authority, source ids, and row-version authority
|
|
152
|
+
must come from trusted backend/session context, not from model arguments.
|
|
153
|
+
|
|
154
|
+
Good:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
"trusted_context": {
|
|
158
|
+
"provider": "environment",
|
|
159
|
+
"values": {
|
|
160
|
+
"tenant_id_env": "SYNAPSOR_TENANT_ID",
|
|
161
|
+
"principal_env": "SYNAPSOR_PRINCIPAL"
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Bad:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
"args": {
|
|
170
|
+
"tenant_id": { "type": "string" }
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Runner rejects model-facing trust-scope arguments.
|
|
175
|
+
|
|
176
|
+
## Direct SQL Writeback
|
|
177
|
+
|
|
178
|
+
Use direct SQL writeback only for simple bounded single-row `UPDATE` proposals.
|
|
179
|
+
Runner validates:
|
|
180
|
+
|
|
181
|
+
- fixed table and column names;
|
|
182
|
+
- primary-key targeting;
|
|
183
|
+
- tenant guard;
|
|
184
|
+
- `allowed_columns`;
|
|
185
|
+
- numeric bounds and transition guards;
|
|
186
|
+
- optimistic conflict guard such as `updated_at`;
|
|
187
|
+
- one affected row;
|
|
188
|
+
- idempotency receipt.
|
|
189
|
+
|
|
190
|
+
Runner does not expose generic SQL, model-generated SQL, DDL, INSERT, DELETE,
|
|
191
|
+
UPSERT, or multi-row writes.
|
|
192
|
+
|
|
193
|
+
Direct SQL writeback uses the source `write_url_env`, such as
|
|
194
|
+
`SYNAPSOR_DATABASE_WRITE_URL`. The writer needs permission for
|
|
195
|
+
`synapsor_writeback_receipts` or an administrator must pre-create and grant that
|
|
196
|
+
table.
|
|
197
|
+
|
|
198
|
+
## App-Owned Executors
|
|
199
|
+
|
|
200
|
+
Use an app-owned executor when an approved proposal needs richer business work:
|
|
201
|
+
creating a credit row, inserting an outbox event, updating multiple app tables,
|
|
202
|
+
or calling your own service.
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
"executors": {
|
|
206
|
+
"billing_handler": {
|
|
207
|
+
"type": "http_handler",
|
|
208
|
+
"url_env": "BILLING_WRITEBACK_URL",
|
|
209
|
+
"method": "POST",
|
|
210
|
+
"auth": {
|
|
211
|
+
"type": "bearer_env",
|
|
212
|
+
"token_env": "BILLING_WRITEBACK_TOKEN"
|
|
213
|
+
},
|
|
214
|
+
"signing_secret_env": "BILLING_WRITEBACK_SIGNING_SECRET",
|
|
215
|
+
"timeout_ms": 5000
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Then reference it from a proposal capability:
|
|
221
|
+
|
|
222
|
+
```json
|
|
223
|
+
{
|
|
224
|
+
"name": "billing.propose_account_credit",
|
|
225
|
+
"kind": "proposal",
|
|
226
|
+
"executor": "billing_handler"
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Approval still happens outside MCP. Runner sends the approved job to your
|
|
231
|
+
handler, and the handler returns an applied/conflict/failed receipt for replay.
|
|
232
|
+
See [App-Owned Executors](app-owned-executors.md) and
|
|
233
|
+
[Writeback Executors](writeback-executors.md).
|
|
234
|
+
|
|
235
|
+
## OpenAI Aliases
|
|
236
|
+
|
|
237
|
+
Canonical Synapsor names use dots, such as `billing.inspect_invoice`. Some
|
|
238
|
+
clients require function-safe names. Use:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
synapsor-runner mcp serve-streamable-http \
|
|
242
|
+
--config ./synapsor.runner.json \
|
|
243
|
+
--store ./.synapsor/local.db \
|
|
244
|
+
--auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN \
|
|
245
|
+
--alias-mode openai
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The model sees aliases such as `billing__inspect_invoice`. Runner includes the
|
|
249
|
+
canonical name in tool metadata and descriptions so audit/replay still use the
|
|
250
|
+
real capability name.
|
|
251
|
+
|
|
252
|
+
## Why Not `execute_sql`
|
|
253
|
+
|
|
254
|
+
`execute_sql(sql)` gives the model database authority. Synapsor Runner gives the
|
|
255
|
+
model proposal authority:
|
|
256
|
+
|
|
257
|
+
```text
|
|
258
|
+
model-facing MCP tool -> trusted context -> scoped read -> evidence -> proposal
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Commit authority stays outside the model:
|
|
262
|
+
|
|
263
|
+
```text
|
|
264
|
+
human/operator approval -> guarded writeback or app-owned handler -> receipt/replay
|
|
265
|
+
```
|
package/docs/doctor.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Doctor
|
|
2
|
+
|
|
3
|
+
Use `doctor` to check a local Runner setup without printing database URLs,
|
|
4
|
+
passwords, bearer tokens, signing secrets, or private keys.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
synapsor-runner doctor --config synapsor.runner.json
|
|
8
|
+
synapsor-runner doctor --config synapsor.runner.json --json
|
|
9
|
+
synapsor-runner doctor --config synapsor.runner.json --report --redact --output synapsor-doctor.md
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The default check validates:
|
|
13
|
+
|
|
14
|
+
- config shape;
|
|
15
|
+
- trusted context environment variables;
|
|
16
|
+
- read credential environment variables;
|
|
17
|
+
- read/write credential separation;
|
|
18
|
+
- reachable source metadata when the read env var is set;
|
|
19
|
+
- configured target tables and columns;
|
|
20
|
+
- MCP tool boundary, including absence of raw SQL and commit tools;
|
|
21
|
+
- local store stats.
|
|
22
|
+
|
|
23
|
+
## App-Owned Handler Checks
|
|
24
|
+
|
|
25
|
+
For `http_handler` executors, add `--check-handlers`:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
synapsor-runner doctor --config synapsor.runner.json --check-handlers
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This checks handler URL/token/signing-secret env vars and sends a reachability
|
|
32
|
+
probe to the handler endpoint. It does not apply a proposal and does not send a
|
|
33
|
+
writeback job.
|
|
34
|
+
|
|
35
|
+
Use `signing_secret_env` for non-loopback handler deployments so Runner signs
|
|
36
|
+
requests with:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
X-Synapsor-Signature
|
|
40
|
+
X-Synapsor-Issued-At
|
|
41
|
+
X-Synapsor-Proposal-Id
|
|
42
|
+
Idempotency-Key
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Direct SQL Writeback Checks
|
|
46
|
+
|
|
47
|
+
For direct `sql_update` writeback, add `--check-writeback` only after reviewing
|
|
48
|
+
the receipt-table DDL/grants:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
synapsor-runner doctor --config synapsor.runner.json --check-writeback
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This connects with the trusted writer env var named by `write_url_env` and
|
|
55
|
+
checks:
|
|
56
|
+
|
|
57
|
+
- writer database connectivity;
|
|
58
|
+
- `synapsor_writeback_receipts` permission through the adapter doctor;
|
|
59
|
+
- rollback-only access to each configured proposal target table;
|
|
60
|
+
- rollback-only update permission for configured allowed write columns.
|
|
61
|
+
|
|
62
|
+
The target-table probe uses fixed schema/table/column identifiers from the
|
|
63
|
+
reviewed config. It does not accept model SQL, user SQL, arbitrary table names,
|
|
64
|
+
or arbitrary column names. It runs inside a transaction and rolls back.
|
|
65
|
+
|
|
66
|
+
The receipt-table probe can create `synapsor_writeback_receipts` if the writer
|
|
67
|
+
has permission. If your policy does not allow Runner to create tables in the
|
|
68
|
+
application schema, pre-create the table and grant access:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
synapsor-runner writeback migration --engine postgres --schema synapsor
|
|
72
|
+
synapsor-runner writeback grants --engine postgres --schema synapsor --writer-role app_writer
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
For MySQL:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
synapsor-runner writeback migration --engine mysql --schema appdb
|
|
79
|
+
synapsor-runner writeback grants --engine mysql --schema appdb --writer-role "'app_writer'@'%'"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Use an app-owned `http_handler` or `command_handler` executor when your
|
|
83
|
+
application should own richer business writes or receipt storage.
|
|
84
|
+
|
|
85
|
+
## Redaction
|
|
86
|
+
|
|
87
|
+
Doctor output intentionally uses safe categories such as:
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
connection failed
|
|
91
|
+
authentication failed
|
|
92
|
+
permission denied
|
|
93
|
+
configured object not found
|
|
94
|
+
database probe failed
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Raw driver errors, connection strings, passwords, tokens, signing secrets, and
|
|
98
|
+
handler URLs are not printed in the report.
|
|
@@ -0,0 +1,200 @@
|
|
|
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
|
+
The helper is the safe-by-default path for rich writes such as:
|
|
7
|
+
|
|
8
|
+
- inserting account-credit, refund-review, ticket, or ledger rows;
|
|
9
|
+
- updating multiple related rows inside your app transaction;
|
|
10
|
+
- applying business rules that belong in your application service.
|
|
11
|
+
|
|
12
|
+
The model-facing MCP tool still creates a proposal only. A human/operator
|
|
13
|
+
approves outside MCP. After approval, Runner sends the structured writeback
|
|
14
|
+
request to your handler.
|
|
15
|
+
|
|
16
|
+
## Scope
|
|
17
|
+
|
|
18
|
+
Current alpha scope:
|
|
19
|
+
|
|
20
|
+
- TypeScript helper in `packages/handler`;
|
|
21
|
+
- bearer token verification;
|
|
22
|
+
- optional HMAC verification over the raw request body;
|
|
23
|
+
- typed request parsing;
|
|
24
|
+
- action dispatch;
|
|
25
|
+
- idempotency receipt lookup;
|
|
26
|
+
- transaction wrapper;
|
|
27
|
+
- `SELECT ... FOR UPDATE` target-row lock;
|
|
28
|
+
- tenant guard;
|
|
29
|
+
- expected-version stale-row guard;
|
|
30
|
+
- safe applied/conflict/failed receipts;
|
|
31
|
+
- no raw driver errors in HTTP responses.
|
|
32
|
+
|
|
33
|
+
Python helper is planned. For now, Python handlers should follow the documented
|
|
34
|
+
request/receipt schema and the FastAPI template in `examples/app-owned-writeback`.
|
|
35
|
+
|
|
36
|
+
## Schemas
|
|
37
|
+
|
|
38
|
+
Published schemas:
|
|
39
|
+
|
|
40
|
+
- `schemas/synapsor.app-handler-request.v1.json`
|
|
41
|
+
- `schemas/synapsor.app-handler-receipt.v1.json`
|
|
42
|
+
|
|
43
|
+
The helper accepts both the new `protocol_version: "1.0"` shape and the current
|
|
44
|
+
Runner `schema_version: "synapsor.handler-writeback.v1"` request shape during
|
|
45
|
+
the alpha migration.
|
|
46
|
+
|
|
47
|
+
## TypeScript Usage
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { createWritebackHandler } from "@synapsor/handler";
|
|
51
|
+
|
|
52
|
+
export const handler = createWritebackHandler({
|
|
53
|
+
tokenEnv: "SYNAPSOR_APP_HANDLER_TOKEN",
|
|
54
|
+
signingSecretEnv: "SYNAPSOR_APP_HANDLER_SIGNING_SECRET",
|
|
55
|
+
source: {
|
|
56
|
+
engine: "postgres",
|
|
57
|
+
writeUrlEnv: "SYNAPSOR_APP_WRITE_URL",
|
|
58
|
+
receiptTable: { schema: "synapsor", table: "handler_receipts" }
|
|
59
|
+
},
|
|
60
|
+
capabilities: {
|
|
61
|
+
"support.propose_plan_credit": async (job, tx) => {
|
|
62
|
+
const creditId = `CR-${job.proposalId.slice(-12)}`;
|
|
63
|
+
|
|
64
|
+
await tx.insert("credits", {
|
|
65
|
+
id: creditId,
|
|
66
|
+
tenant_id: job.tenantId,
|
|
67
|
+
invoice_id: job.objectId,
|
|
68
|
+
amount_cents: Number(job.patch.credit_requested_cents),
|
|
69
|
+
reason: String(job.patch.credit_reason),
|
|
70
|
+
created_by: job.principal
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await tx.update("invoices", {
|
|
74
|
+
id: job.objectId,
|
|
75
|
+
tenant_id: job.tenantId
|
|
76
|
+
}, {
|
|
77
|
+
credited_cents:
|
|
78
|
+
Number(job.row.credited_cents ?? 0) +
|
|
79
|
+
Number(job.patch.credit_requested_cents)
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
rowsAffected: 2,
|
|
84
|
+
effects: [{ type: "db.insert", table: "credits", id: creditId }]
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Mount the returned handler at your app route, for example
|
|
92
|
+
`POST /synapsor/writeback`.
|
|
93
|
+
|
|
94
|
+
The handler author writes only the business effect. The helper owns the safety
|
|
95
|
+
loop around that effect.
|
|
96
|
+
|
|
97
|
+
## What The Helper Enforces
|
|
98
|
+
|
|
99
|
+
The helper checks these before your business function can mutate state:
|
|
100
|
+
|
|
101
|
+
- the bearer token matches the configured environment variable;
|
|
102
|
+
- the optional HMAC signature is valid and fresh;
|
|
103
|
+
- the request protocol is supported;
|
|
104
|
+
- the action maps to a configured capability function;
|
|
105
|
+
- the target row exists inside the trusted tenant;
|
|
106
|
+
- the row version still matches the proposal's expected version;
|
|
107
|
+
- the idempotency key was not already applied.
|
|
108
|
+
|
|
109
|
+
If the row is missing or belongs to another tenant, the helper returns:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"status": "conflict",
|
|
114
|
+
"rows_affected": 0,
|
|
115
|
+
"source_database_mutated": false,
|
|
116
|
+
"safe_error_code": "ROW_NOT_FOUND_OR_WRONG_TENANT"
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
If the row changed after proposal creation, the helper returns:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"status": "conflict",
|
|
125
|
+
"rows_affected": 0,
|
|
126
|
+
"source_database_mutated": false,
|
|
127
|
+
"safe_error_code": "ROW_CHANGED_AFTER_PROPOSAL"
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
If your business function throws, the helper rolls back the transaction and
|
|
132
|
+
returns a safe failed receipt. Raw driver and exception text are not exposed to
|
|
133
|
+
the caller.
|
|
134
|
+
|
|
135
|
+
## Runner-Side Signing Config
|
|
136
|
+
|
|
137
|
+
Configure the matching `http_handler` executor with the same signing-secret env
|
|
138
|
+
name:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"executors": {
|
|
143
|
+
"billing_handler": {
|
|
144
|
+
"type": "http_handler",
|
|
145
|
+
"url_env": "BILLING_WRITEBACK_URL",
|
|
146
|
+
"method": "POST",
|
|
147
|
+
"auth": {
|
|
148
|
+
"type": "bearer_env",
|
|
149
|
+
"token_env": "BILLING_WRITEBACK_TOKEN"
|
|
150
|
+
},
|
|
151
|
+
"signing_secret_env": "SYNAPSOR_APP_HANDLER_SIGNING_SECRET",
|
|
152
|
+
"timeout_ms": 5000
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
When this field is set, Runner signs the exact request body and sends
|
|
159
|
+
`X-Synapsor-Signature`, `X-Synapsor-Issued-At`,
|
|
160
|
+
`X-Synapsor-Proposal-Id`, and `Idempotency-Key`. The helper verifies those
|
|
161
|
+
headers before parsing or applying the writeback request.
|
|
162
|
+
|
|
163
|
+
## Signing
|
|
164
|
+
|
|
165
|
+
For loopback-only development, bearer auth may be enough. For any handler that
|
|
166
|
+
is reachable outside the local process, enable HMAC:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
createWritebackHandler({
|
|
170
|
+
tokenEnv: "SYNAPSOR_APP_HANDLER_TOKEN",
|
|
171
|
+
signingSecretEnv: "SYNAPSOR_APP_HANDLER_SIGNING_SECRET",
|
|
172
|
+
// ...
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Runner sends:
|
|
177
|
+
|
|
178
|
+
```text
|
|
179
|
+
Authorization: Bearer <token>
|
|
180
|
+
X-Synapsor-Signature: sha256=<hmac>
|
|
181
|
+
X-Synapsor-Issued-At: <iso timestamp>
|
|
182
|
+
X-Synapsor-Proposal-Id: wrp_...
|
|
183
|
+
Idempotency-Key: wrp_...
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The HMAC is computed over the raw body. The helper enforces a short issued-at
|
|
187
|
+
skew window.
|
|
188
|
+
|
|
189
|
+
## Receipt Storage
|
|
190
|
+
|
|
191
|
+
The helper's Postgres adapter stores idempotency receipts in a receipt table.
|
|
192
|
+
Prefer a dedicated schema, for example:
|
|
193
|
+
|
|
194
|
+
```sql
|
|
195
|
+
CREATE SCHEMA IF NOT EXISTS synapsor;
|
|
196
|
+
GRANT USAGE, CREATE ON SCHEMA synapsor TO app_writeback_user;
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
If your application already has a receipt/idempotency table, implement the
|
|
200
|
+
`WritebackHandlerDatabase` interface and pass it as `database`.
|
package/docs/local-mode.md
CHANGED
|
@@ -273,13 +273,24 @@ or prune it without touching your source Postgres/MySQL database:
|
|
|
273
273
|
|
|
274
274
|
```bash
|
|
275
275
|
synapsor-runner store stats --store ./.synapsor/local.db
|
|
276
|
+
synapsor-runner events tail --store ./.synapsor/local.db
|
|
276
277
|
synapsor-runner store vacuum --store ./.synapsor/local.db
|
|
277
278
|
synapsor-runner store prune --store ./.synapsor/local.db --older-than 30d --dry-run
|
|
278
279
|
synapsor-runner store prune --store ./.synapsor/local.db --older-than 30d --yes
|
|
280
|
+
synapsor-runner store prune --store ./.synapsor/local.db --older-than 30d --yes --force
|
|
281
|
+
synapsor-runner store reset --store ./.synapsor/local.db --yes
|
|
279
282
|
```
|
|
280
283
|
|
|
281
|
-
`
|
|
282
|
-
|
|
284
|
+
`events tail` shows local lifecycle events already recorded in the SQLite
|
|
285
|
+
ledger, including proposal creation, approval/rejection, writeback jobs, and
|
|
286
|
+
writeback applied/conflict/failed receipts. Add `--follow` to keep polling a
|
|
287
|
+
running local store.
|
|
288
|
+
|
|
289
|
+
`store prune` defaults to dry-run. `store reset` requires `--yes` and removes
|
|
290
|
+
only the local SQLite ledger files. MCP server modes write a small active-store
|
|
291
|
+
lease next to the SQLite file; destructive store operations refuse while that
|
|
292
|
+
lease points at a live PID unless you pass `--force` after verifying the server
|
|
293
|
+
is stopped or stale.
|
|
283
294
|
|
|
284
295
|
## Boundary
|
|
285
296
|
|