@synapsor/runner 0.1.0-alpha.1 → 0.1.0-alpha.11
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 +426 -19
- package/TRADEMARKS.md +23 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +20 -8723
- package/dist/runner.mjs +12958 -0
- package/docs/README.md +53 -0
- package/docs/app-owned-executors.md +21 -0
- package/docs/cloud-mode.md +24 -0
- package/docs/current-scope.md +24 -0
- package/docs/dependency-license-inventory.md +35 -0
- package/docs/getting-started-own-database.md +460 -0
- package/docs/http-mcp.md +276 -0
- package/docs/licensing.md +36 -0
- package/docs/limitations.md +95 -0
- package/docs/local-mode.md +351 -0
- package/docs/mcp-audit.md +152 -0
- package/docs/mcp-client-setup.md +270 -0
- package/docs/openai-agents-sdk.md +57 -0
- package/docs/recipes.md +61 -0
- package/docs/release-notes.md +158 -0
- package/docs/security-boundary.md +94 -0
- package/docs/troubleshooting-first-run.md +248 -0
- package/docs/use-your-own-database.md +18 -0
- package/docs/writeback-executors.md +220 -0
- package/examples/app-owned-writeback/README.md +120 -0
- package/examples/app-owned-writeback/business-actions.md +221 -0
- package/examples/app-owned-writeback/command-handler.mjs +46 -0
- package/examples/app-owned-writeback/node-fastify-handler.mjs +55 -0
- package/examples/app-owned-writeback/python-fastapi-handler.py +57 -0
- package/examples/dangerous-mcp-tools.json +88 -0
- package/examples/mcp-postgres-billing-app-handler/README.md +82 -0
- package/examples/mcp-postgres-billing-app-handler/app-handler.mjs +197 -0
- package/examples/mcp-postgres-billing-app-handler/docker-compose.yml +13 -0
- package/examples/mcp-postgres-billing-app-handler/schema.sql +59 -0
- package/examples/mcp-postgres-billing-app-handler/scripts/run-demo.sh +99 -0
- package/examples/mcp-postgres-billing-app-handler/seed.sql +39 -0
- package/examples/mcp-postgres-billing-app-handler/synapsor.runner.json +157 -0
- package/examples/openai-agents-http/README.md +64 -0
- package/examples/openai-agents-http/agent.py +54 -0
- package/examples/openai-agents-http/requirements.txt +1 -0
- package/examples/openai-agents-stdio/README.md +66 -0
- package/examples/openai-agents-stdio/agent.py +72 -0
- package/examples/openai-agents-stdio/requirements.txt +1 -0
- package/examples/reference-support-billing-app/README.md +137 -0
- package/examples/reference-support-billing-app/docker-compose.yml +13 -0
- package/examples/reference-support-billing-app/mcp-client.generic.json +11 -0
- package/examples/reference-support-billing-app/schema.sql +68 -0
- package/examples/reference-support-billing-app/scripts/run-demo.sh +7 -0
- package/examples/reference-support-billing-app/seed.sql +33 -0
- package/examples/reference-support-billing-app/synapsor.runner.json +241 -0
- package/fixtures/benchmark/mcp-efficiency.json +53 -0
- package/fixtures/benchmark/mcp-efficiency.txt +25 -0
- package/fixtures/protocol/MANIFEST.json +54 -0
- package/fixtures/protocol/change-set.late-fee-waiver.v1.json +72 -0
- package/fixtures/protocol/execution-receipt.applied.v1.json +14 -0
- package/fixtures/protocol/execution-receipt.conflict.v1.json +15 -0
- package/fixtures/protocol/runner-registration.v1.json +22 -0
- package/fixtures/protocol/writeback-job.late-fee-waiver.v1.json +44 -0
- package/package.json +14 -4
- package/recipes/accounts.trial_extension.json +42 -0
- package/recipes/billing.late_fee_waiver.json +46 -0
- package/recipes/credits.account_credit.json +45 -0
- package/recipes/orders.refund_review.json +57 -0
- package/recipes/support.ticket_resolution.json +51 -0
- package/dist/bin.cjs +0 -13
|
@@ -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,46 @@
|
|
|
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
|
+
* Put your app-owned command transaction here.
|
|
30
|
+
*
|
|
31
|
+
* Examples:
|
|
32
|
+
* - call an internal service;
|
|
33
|
+
* - enqueue a review job;
|
|
34
|
+
* - run an app migration-safe script that uses your normal ORM.
|
|
35
|
+
*
|
|
36
|
+
* Re-check tenant/principal authorization, idempotency, row/version guards,
|
|
37
|
+
* and business policy before mutating application state.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
process.stdout.write(JSON.stringify({
|
|
41
|
+
status: "applied",
|
|
42
|
+
rows_affected: 1,
|
|
43
|
+
previous_version: String(changeSet.guards?.expected_version?.value || ""),
|
|
44
|
+
new_version: new Date().toISOString(),
|
|
45
|
+
source_database_mutated: true,
|
|
46
|
+
}));
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
* Put your app-owned transaction here.
|
|
32
|
+
*
|
|
33
|
+
* Examples:
|
|
34
|
+
* - insert a refund_review row;
|
|
35
|
+
* - insert an account_credit row;
|
|
36
|
+
* - open a support_ticket row;
|
|
37
|
+
* - update invoice + ledger rows together.
|
|
38
|
+
*
|
|
39
|
+
* Re-check:
|
|
40
|
+
* - tenant and principal authorization;
|
|
41
|
+
* - idempotency_key has not already been applied;
|
|
42
|
+
* - row/version guards still match;
|
|
43
|
+
* - requested business action is allowed by your app policy.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
status: "applied",
|
|
48
|
+
rows_affected: 1,
|
|
49
|
+
previous_version: String(changeSet.guards?.expected_version?.value || ""),
|
|
50
|
+
new_version: new Date().toISOString(),
|
|
51
|
+
source_database_mutated: true,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
app.listen({ host: "127.0.0.1", port });
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
# Put your app-owned transaction here.
|
|
40
|
+
#
|
|
41
|
+
# Examples:
|
|
42
|
+
# - insert a refund_review row;
|
|
43
|
+
# - insert an account_credit row;
|
|
44
|
+
# - open a support_ticket row;
|
|
45
|
+
# - update invoice + ledger rows together.
|
|
46
|
+
#
|
|
47
|
+
# Re-check tenant/principal authorization, idempotency, row/version guards,
|
|
48
|
+
# and business policy before mutating application state.
|
|
49
|
+
|
|
50
|
+
expected = request.change_set.get("guards", {}).get("expected_version", {})
|
|
51
|
+
return {
|
|
52
|
+
"status": "applied",
|
|
53
|
+
"rows_affected": 1,
|
|
54
|
+
"previous_version": str(expected.get("value", "")),
|
|
55
|
+
"new_version": datetime.now(timezone.utc).isoformat(),
|
|
56
|
+
"source_database_mutated": True,
|
|
57
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tools": [
|
|
3
|
+
{
|
|
4
|
+
"name": "execute_sql",
|
|
5
|
+
"description": "Execute arbitrary SQL against the application database.",
|
|
6
|
+
"inputSchema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"sql": { "type": "string" }
|
|
10
|
+
},
|
|
11
|
+
"required": ["sql"]
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "run_query",
|
|
16
|
+
"description": "Run any query and return database rows.",
|
|
17
|
+
"inputSchema": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"query": { "type": "string" },
|
|
21
|
+
"table": { "type": "string" },
|
|
22
|
+
"columns": {
|
|
23
|
+
"type": "array",
|
|
24
|
+
"items": { "type": "string" }
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"required": ["query"]
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "approve_refund",
|
|
32
|
+
"description": "Approve and issue a customer refund immediately.",
|
|
33
|
+
"inputSchema": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"properties": {
|
|
36
|
+
"refund_id": { "type": "string" },
|
|
37
|
+
"tenant_id": { "type": "string" },
|
|
38
|
+
"amount_cents": { "type": "number" }
|
|
39
|
+
},
|
|
40
|
+
"required": ["refund_id", "tenant_id", "amount_cents"]
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "update_customer",
|
|
45
|
+
"description": "Update a customer record directly.",
|
|
46
|
+
"inputSchema": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"properties": {
|
|
49
|
+
"customer_id": { "type": "string" },
|
|
50
|
+
"tenant_id": { "type": "string" },
|
|
51
|
+
"column": { "type": "string" },
|
|
52
|
+
"value": { "type": "string" }
|
|
53
|
+
},
|
|
54
|
+
"required": ["customer_id", "tenant_id", "column", "value"]
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "delete_order",
|
|
59
|
+
"description": "Delete an order from the database.",
|
|
60
|
+
"inputSchema": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": {
|
|
63
|
+
"order_id": { "type": "string" },
|
|
64
|
+
"tenant_id": { "type": "string" }
|
|
65
|
+
},
|
|
66
|
+
"required": ["order_id", "tenant_id"]
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "query_database",
|
|
71
|
+
"description": "Query arbitrary tables and columns from the database.",
|
|
72
|
+
"inputSchema": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"properties": {
|
|
75
|
+
"database": { "type": "string" },
|
|
76
|
+
"schema": { "type": "string" },
|
|
77
|
+
"table": { "type": "string" },
|
|
78
|
+
"columns": {
|
|
79
|
+
"type": "array",
|
|
80
|
+
"items": { "type": "string" }
|
|
81
|
+
},
|
|
82
|
+
"where": { "type": "string" }
|
|
83
|
+
},
|
|
84
|
+
"required": ["table"]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Postgres Billing App Handler
|
|
2
|
+
|
|
3
|
+
This example shows the two Synapsor Runner commit paths against one disposable
|
|
4
|
+
Postgres billing database.
|
|
5
|
+
|
|
6
|
+
Direct guarded SQL writeback:
|
|
7
|
+
|
|
8
|
+
- `billing.propose_late_fee_waiver`
|
|
9
|
+
- one-row `UPDATE`;
|
|
10
|
+
- tenant guard, allowed-column guard, conflict guard, idempotency receipt.
|
|
11
|
+
|
|
12
|
+
App-owned rich writeback:
|
|
13
|
+
|
|
14
|
+
- `billing.propose_account_credit`
|
|
15
|
+
- model-facing MCP only creates a proposal;
|
|
16
|
+
- approval happens outside MCP;
|
|
17
|
+
- Runner calls `billing_app_handler`;
|
|
18
|
+
- the app inserts an `account_credits` row and updates the invoice inside its
|
|
19
|
+
own transaction;
|
|
20
|
+
- Runner records the handler receipt and replay.
|
|
21
|
+
|
|
22
|
+
The model never receives `execute_sql`, approval tools, commit/apply tools,
|
|
23
|
+
database URLs, or write credentials.
|
|
24
|
+
|
|
25
|
+
## Run
|
|
26
|
+
|
|
27
|
+
From the repository root:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
examples/mcp-postgres-billing-app-handler/scripts/run-demo.sh
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Expected ending:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
App-owned billing handler demo passed.
|
|
37
|
+
Verified: proposal first, source unchanged before approval, account credit inserted by app handler, idempotent retry, replay.
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Manual Start
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
docker compose -f examples/mcp-postgres-billing-app-handler/docker-compose.yml up -d
|
|
44
|
+
|
|
45
|
+
export BILLING_APP_READ_URL="postgresql://synapsor_reader:synapsor_reader_password@localhost:55437/synapsor_billing_app_handler"
|
|
46
|
+
export BILLING_APP_WRITE_URL="postgresql://synapsor_writer:synapsor_writer_password@localhost:55437/synapsor_billing_app_handler"
|
|
47
|
+
export BILLING_APP_HANDLER_URL="http://127.0.0.1:8787/synapsor/writeback"
|
|
48
|
+
export BILLING_APP_HANDLER_TOKEN="dev-handler-token"
|
|
49
|
+
export SYNAPSOR_TENANT_ID="acme"
|
|
50
|
+
export SYNAPSOR_PRINCIPAL="local_billing_operator"
|
|
51
|
+
|
|
52
|
+
node examples/mcp-postgres-billing-app-handler/app-handler.mjs
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Then, in another terminal:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
synapsor-runner tools preview \
|
|
59
|
+
--config examples/mcp-postgres-billing-app-handler/synapsor.runner.json \
|
|
60
|
+
--store ./tmp/billing-app-handler/local.db
|
|
61
|
+
|
|
62
|
+
synapsor-runner propose billing.propose_account_credit \
|
|
63
|
+
--json '{"invoice_id":"INV-3001","amount_cents":2500,"reason":"support-approved credit"}' \
|
|
64
|
+
--config examples/mcp-postgres-billing-app-handler/synapsor.runner.json \
|
|
65
|
+
--store ./tmp/billing-app-handler/local.db
|
|
66
|
+
|
|
67
|
+
synapsor-runner proposals approve latest --yes --store ./tmp/billing-app-handler/local.db
|
|
68
|
+
synapsor-runner apply latest \
|
|
69
|
+
--config examples/mcp-postgres-billing-app-handler/synapsor.runner.json \
|
|
70
|
+
--store ./tmp/billing-app-handler/local.db
|
|
71
|
+
synapsor-runner replay show latest --store ./tmp/billing-app-handler/local.db
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Why This Exists
|
|
75
|
+
|
|
76
|
+
Direct Runner SQL writeback should stay intentionally narrow. It is good for
|
|
77
|
+
simple, bounded, single-row updates.
|
|
78
|
+
|
|
79
|
+
For richer business transactions such as creating credits, refund reviews,
|
|
80
|
+
ledger rows, tickets, events, or multi-row updates, keep execution in your
|
|
81
|
+
application service. Synapsor Runner still owns proposal creation, evidence,
|
|
82
|
+
approval boundary, idempotency, receipt storage, and replay.
|