@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.
Files changed (66) hide show
  1. package/README.md +426 -19
  2. package/TRADEMARKS.md +23 -0
  3. package/dist/cli.d.ts +4 -0
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +20 -8723
  6. package/dist/runner.mjs +12958 -0
  7. package/docs/README.md +53 -0
  8. package/docs/app-owned-executors.md +21 -0
  9. package/docs/cloud-mode.md +24 -0
  10. package/docs/current-scope.md +24 -0
  11. package/docs/dependency-license-inventory.md +35 -0
  12. package/docs/getting-started-own-database.md +460 -0
  13. package/docs/http-mcp.md +276 -0
  14. package/docs/licensing.md +36 -0
  15. package/docs/limitations.md +95 -0
  16. package/docs/local-mode.md +351 -0
  17. package/docs/mcp-audit.md +152 -0
  18. package/docs/mcp-client-setup.md +270 -0
  19. package/docs/openai-agents-sdk.md +57 -0
  20. package/docs/recipes.md +61 -0
  21. package/docs/release-notes.md +158 -0
  22. package/docs/security-boundary.md +94 -0
  23. package/docs/troubleshooting-first-run.md +248 -0
  24. package/docs/use-your-own-database.md +18 -0
  25. package/docs/writeback-executors.md +220 -0
  26. package/examples/app-owned-writeback/README.md +120 -0
  27. package/examples/app-owned-writeback/business-actions.md +221 -0
  28. package/examples/app-owned-writeback/command-handler.mjs +46 -0
  29. package/examples/app-owned-writeback/node-fastify-handler.mjs +55 -0
  30. package/examples/app-owned-writeback/python-fastapi-handler.py +57 -0
  31. package/examples/dangerous-mcp-tools.json +88 -0
  32. package/examples/mcp-postgres-billing-app-handler/README.md +82 -0
  33. package/examples/mcp-postgres-billing-app-handler/app-handler.mjs +197 -0
  34. package/examples/mcp-postgres-billing-app-handler/docker-compose.yml +13 -0
  35. package/examples/mcp-postgres-billing-app-handler/schema.sql +59 -0
  36. package/examples/mcp-postgres-billing-app-handler/scripts/run-demo.sh +99 -0
  37. package/examples/mcp-postgres-billing-app-handler/seed.sql +39 -0
  38. package/examples/mcp-postgres-billing-app-handler/synapsor.runner.json +157 -0
  39. package/examples/openai-agents-http/README.md +64 -0
  40. package/examples/openai-agents-http/agent.py +54 -0
  41. package/examples/openai-agents-http/requirements.txt +1 -0
  42. package/examples/openai-agents-stdio/README.md +66 -0
  43. package/examples/openai-agents-stdio/agent.py +72 -0
  44. package/examples/openai-agents-stdio/requirements.txt +1 -0
  45. package/examples/reference-support-billing-app/README.md +137 -0
  46. package/examples/reference-support-billing-app/docker-compose.yml +13 -0
  47. package/examples/reference-support-billing-app/mcp-client.generic.json +11 -0
  48. package/examples/reference-support-billing-app/schema.sql +68 -0
  49. package/examples/reference-support-billing-app/scripts/run-demo.sh +7 -0
  50. package/examples/reference-support-billing-app/seed.sql +33 -0
  51. package/examples/reference-support-billing-app/synapsor.runner.json +241 -0
  52. package/fixtures/benchmark/mcp-efficiency.json +53 -0
  53. package/fixtures/benchmark/mcp-efficiency.txt +25 -0
  54. package/fixtures/protocol/MANIFEST.json +54 -0
  55. package/fixtures/protocol/change-set.late-fee-waiver.v1.json +72 -0
  56. package/fixtures/protocol/execution-receipt.applied.v1.json +14 -0
  57. package/fixtures/protocol/execution-receipt.conflict.v1.json +15 -0
  58. package/fixtures/protocol/runner-registration.v1.json +22 -0
  59. package/fixtures/protocol/writeback-job.late-fee-waiver.v1.json +44 -0
  60. package/package.json +14 -4
  61. package/recipes/accounts.trial_extension.json +42 -0
  62. package/recipes/billing.late_fee_waiver.json +46 -0
  63. package/recipes/credits.account_credit.json +45 -0
  64. package/recipes/orders.refund_review.json +57 -0
  65. package/recipes/support.ticket_resolution.json +51 -0
  66. 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.