@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
@@ -0,0 +1,143 @@
1
+ # Proposal: One Result Envelope for all Synapsor Runner tool results
2
+
3
+ Status: draft for synapsor-runner (OSS)
4
+ Author: external integrator feedback (built a full OpenAI Agents SDK + Postgres lab on alpha.6→alpha.11)
5
+ Goal: make every tool result (read, proposal, error) share one shape that is both
6
+ **machine-branchable** and **LLM-legible**, so agents behave reliably and client
7
+ code stops special-casing.
8
+
9
+ ## Why
10
+
11
+ Observed today (alpha.11), the shapes diverge:
12
+
13
+ ```jsonc
14
+ // read success
15
+ { "status": "ok", "action": "billing.inspect_invoice", "data": { ... },
16
+ "evidence_bundle_id": "ev_…", "trusted_context": { ... }, "source_database_changed": false }
17
+
18
+ // read not-found / tenant mismatch
19
+ { "ok": false, "code": "ROW_NOT_FOUND", "error": "The scoped capability read did not find exactly one authorized row." }
20
+
21
+ // proposal success
22
+ { "status": "review_required", "proposal_id": "wrp_…", "diff": { "late_fee_cents": { "before": 2500, "proposed": 0 } },
23
+ "source_database_changed": false }
24
+ ```
25
+
26
+ Three different top-level keys (`status` vs `ok`), two different success vocabularies,
27
+ and raw infra strings leaking into `error`. An LLM driving these has to learn three
28
+ branches; in my live tests the model **stalled / misreported** ("database access issue")
29
+ when it hit the off-shape error path.
30
+
31
+ ## The envelope
32
+
33
+ Every tool returns exactly this top-level shape:
34
+
35
+ ```jsonc
36
+ {
37
+ "ok": true, // boolean — the ONLY field client code must branch on
38
+ "summary": "Invoice INV-3001: $100 + $25 late fee, status overdue.", // one-line NL for the model to read/echo
39
+ "action": "billing.inspect_invoice",
40
+ "kind": "read", // read | proposal
41
+ "data": { ... } | null, // read payload (the row), or null
42
+ "proposal": { ... } | null, // proposal payload (see below), or null
43
+ "error": { ... } | null, // populated iff ok=false (see below)
44
+ "evidence": { "bundle_id": "ev_…", "note": "audit handle; you do not need to act on it" } | null,
45
+ "source_database_changed": false, // ALWAYS present; true only after applied writeback
46
+ "_meta": { "tenant_id": "tenant_acme", "principal": "demo-operator", "provenance": "environment",
47
+ "canonical_capability": "billing.inspect_invoice" }
48
+ }
49
+ ```
50
+
51
+ Rules:
52
+ - `ok` is the single branch point. No more `status` vs `ok`.
53
+ - `summary` is mandatory and is the field the model is expected to read first and can
54
+ echo back to the user. Keep it short, factual, no internal ids unless useful.
55
+ - Exactly one of `data` / `proposal` is non-null on success; both null on error.
56
+ - `_meta` carries the trusted context and the canonical capability name (so OpenAI-safe
57
+ aliases like `billing__inspect_invoice` still expose their real name for reasoning/audit).
58
+
59
+ ### Proposal payload
60
+
61
+ ```jsonc
62
+ "proposal": {
63
+ "id": "wrp_…",
64
+ "state": "review_required", // review_required | approved | applied | conflict | rejected
65
+ "target": "invoices:INV-3001",
66
+ "diff": { "late_fee_cents": { "before": 2500, "proposed": 0 } },
67
+ "approval_required": true,
68
+ "writeback": { "mode": "direct_update" | "app_handler", "applied": false },
69
+ "next": "A human must approve outside this loop; nothing is committed yet."
70
+ }
71
+ ```
72
+
73
+ This part of today's output is already the best-designed — keep `diff.before/proposed`
74
+ and `approval_required` verbatim, just move it under `proposal`.
75
+
76
+ ### Error payload (safe + stable)
77
+
78
+ ```jsonc
79
+ "error": {
80
+ "code": "NOT_FOUND_IN_TENANT", // STABLE enum (below) — never a raw infra string
81
+ "message": "No invoice INV-9999 is visible in your tenant.", // safe, terse, actionable
82
+ "retryable": false
83
+ }
84
+ ```
85
+
86
+ Never surface raw driver text (`connect ECONNREFUSED 127.0.0.1:5433`) to the tool
87
+ caller — log that to the local ledger only. Leaking it is a small info disclosure
88
+ **and** degrades LLM behavior (the model parrots infra errors to the user).
89
+
90
+ ## Stable error code enum
91
+
92
+ | code | meaning | retryable |
93
+ |---|---|---|
94
+ | `NOT_FOUND_IN_TENANT` | lookup found 0 authorized rows (missing OR wrong tenant — do not distinguish, it's a scoping signal) | no |
95
+ | `INVALID_ARGUMENT` | arg failed schema/`numeric_bounds` | no |
96
+ | `POLICY_VIOLATION` | request outside an allowed bound/transition | no |
97
+ | `CAPABILITY_NOT_FOUND` | unknown tool name | no |
98
+ | `VERSION_CONFLICT` | row changed since the agent saw it (stale-row guard) | no (re-inspect first) |
99
+ | `MULTI_ROW_BLOCKED` | a write would touch ≠1 row | no |
100
+ | `APPROVAL_REQUIRED` | attempted to apply without approval | no |
101
+ | `TEMPORARILY_UNAVAILABLE` | DB/handler unreachable or timed out | yes |
102
+ | `INTERNAL` | anything else (details only in ledger) | maybe |
103
+
104
+ Keep this list small and documented; the model can be told "on `VERSION_CONFLICT`,
105
+ re-inspect then re-propose" and act correctly.
106
+
107
+ ## Tool descriptions (ships with the envelope, same impact)
108
+
109
+ The envelope fixes *results*; descriptions fix *whether the model calls the right
110
+ tool at all*. Today they're generic ("Read public.outage_events through a reviewed
111
+ Synapsor capability…"). Let capability config carry model-facing text:
112
+
113
+ ```jsonc
114
+ {
115
+ "name": "support.inspect_outage",
116
+ "description": "Look up an outage event: its time window, affected plan, and the credit policy that governs waivers/credits. Use this before deciding whether a waiver or credit is justified.",
117
+ "args": {
118
+ "outage_id": { "type": "string", "description": "Outage/incident id, e.g. OUT-9001 (often referenced in the support ticket)." }
119
+ },
120
+ "returns_hint": "Returns the outage window, affected_plan, and credit_policy."
121
+ }
122
+ ```
123
+
124
+ In my live runs, the difference between a reliable agent and one that stalled before
125
+ proposing was exactly this: whether the outage tool's description told the model it
126
+ returns the *policy*. Surface `description` + per-arg `description` + `returns_hint`
127
+ in `tools/list`. Fall back to today's auto-text only when the author omits them.
128
+
129
+ ## Migration
130
+
131
+ - Add `"result_format": 2` to `synapsor.runner.json` (or a server flag
132
+ `--result-format v2`); default stays v1 for one minor cycle, then flips.
133
+ - During transition the server can **dual-emit**: v2 envelope with a `legacy` mirror
134
+ of the old keys, so existing parsers don't break.
135
+ - Document a one-line mapping: `status:"ok"` → `ok:true`; `status:"review_required"`
136
+ → `ok:true, proposal.state:"review_required"`; top-level `code/error` → `error.{code,message}`.
137
+
138
+ ## Acceptance
139
+
140
+ - All of `tools/call` (read + proposal) and tool-level failures return the envelope.
141
+ - `ok` alone is sufficient to branch in client code.
142
+ - No raw driver/infra strings appear in any `error.message`.
143
+ - `tools/list` exposes author-supplied `description` / arg `description` / `returns_hint`.
@@ -0,0 +1,161 @@
1
+ # Proposal: An app-owned writeback handler helper (safe-by-default executors)
2
+
3
+ Status: draft for synapsor-runner (OSS)
4
+ Goal: make `http_handler` / `command_handler` executors **safe by default**. Today a
5
+ handler author must re-implement tenant scoping, the stale-row (`expected_version`)
6
+ guard, and idempotency by hand — "match the example." That's a security-critical loop
7
+ to leave to copy-paste. Ship a tiny helper that enforces the guards and hands the
8
+ developer only the business write.
9
+
10
+ ## The problem, concretely
11
+
12
+ I wrote a working credit handler for the lab. To be correct it had to, in order:
13
+ re-auth the bearer token, reject the wrong `action`, extract tenant/object/version
14
+ from a loosely-typed `change_set` (with 3 fallback paths each, copied from the
15
+ example), check idempotency, `SELECT … FOR UPDATE`, compare a normalized
16
+ `expected_version`, INSERT + UPDATE, and format a receipt with the right status
17
+ vocabulary. **Every one of those is a place to introduce a vulnerability** (skip the
18
+ version check → lost-update; skip tenant → cross-tenant write; trust body tenant
19
+ without signature → spoofing). Most integrators will get at least one wrong.
20
+
21
+ ## The contract (formalize what's currently implicit)
22
+
23
+ Request the runner POSTs to an `http_handler` (make this a published, versioned schema):
24
+
25
+ ```jsonc
26
+ {
27
+ "protocol_version": "1.0",
28
+ "proposal_id": "wrp_…",
29
+ "idempotency_key": "wrp_…:INV-3001",
30
+ "issued_at": "2026-06-28T…Z",
31
+ "signature": "sha256=…", // NEW: HMAC over the raw body (see Security)
32
+ "change_set": {
33
+ "action": "support.propose_plan_credit",
34
+ "scope": { "tenant_id": "tenant_acme", "object_id": "INV-3001" },
35
+ "principal": { "id": "human-reviewer" },
36
+ "target": { "schema": "public", "table": "invoices", "primary_key": { "column": "id", "value": "INV-3001" } },
37
+ "patch": { "credit_requested_cents": 1500, "credit_reason": "outage credit" },
38
+ "guards": { "tenant": { "column": "tenant_id", "value": "tenant_acme" },
39
+ "expected_version": { "column": "updated_at", "value": "2026-05-16T00:00:00Z" } }
40
+ }
41
+ }
42
+ ```
43
+
44
+ Receipt the handler must return (today's status vocabulary, kept):
45
+
46
+ ```jsonc
47
+ { "status": "applied" | "already_applied" | "conflict" | "failed",
48
+ "rows_affected": 2,
49
+ "source_database_mutated": true,
50
+ "previous_version": "2026-05-16T00:00:00Z",
51
+ "new_version": "2026-06-28T…Z",
52
+ "safe_error_code": "ROW_CHANGED_AFTER_PROPOSAL", // on conflict/failed
53
+ "details": { "effects": [ { "type": "db.insert", "table": "credits", "id": "CR-…" } ] } }
54
+ ```
55
+
56
+ ## Helper API (TypeScript — first-party, since the runner is TS/Node)
57
+
58
+ ```ts
59
+ import { createWritebackHandler } from "../packages/handler/src/index.js";
60
+
61
+ export const handler = createWritebackHandler({
62
+ // 1. Authenticity: helper verifies bearer AND the HMAC signature for you.
63
+ tokenEnv: "SYNAPSOR_APP_HANDLER_TOKEN",
64
+ signingSecretEnv: "SYNAPSOR_APP_HANDLER_SIGNING_SECRET", // optional but recommended
65
+
66
+ // 2. Bind one apply() per capability. The helper has ALREADY:
67
+ // - verified auth + signature + protocol_version
68
+ // - matched the action
69
+ // - parsed scope/target/patch/guards into a typed `job`
70
+ // - opened a transaction, taken `SELECT … FOR UPDATE` on the target row,
71
+ // enforced tenant match + expected_version (stale-row), and short-circuited
72
+ // idempotency via the receipts table.
73
+ // You only write business effects with the provided tx; throw to roll back.
74
+ capabilities: {
75
+ "support.propose_plan_credit": async (job, tx) => {
76
+ const creditId = `CR-${job.proposalId.slice(-12)}`;
77
+ await tx.insert("credits", {
78
+ id: creditId, tenant_id: job.tenantId, invoice_id: job.objectId,
79
+ customer_id: job.row.customer_id, amount_cents: job.patch.credit_requested_cents,
80
+ reason: job.patch.credit_reason, created_by: job.principal,
81
+ });
82
+ await tx.update("invoices", job.objectId, {
83
+ credited_cents: job.row.credited_cents + job.patch.credit_requested_cents,
84
+ });
85
+ return { effects: [{ type: "db.insert", table: "credits", id: creditId }] };
86
+ },
87
+ },
88
+
89
+ // 3. DB binding (helper owns the tx + FOR UPDATE + version compare + receipt write).
90
+ source: { engine: "postgres", writeUrlEnv: "SYNAPSOR_APP_WRITE_URL" },
91
+ });
92
+ // handler is a (req,res) you mount at POST /synapsor/writeback, or an express/fastify route.
93
+ ```
94
+
95
+ The helper turns conflict/idempotency/auth into framework concerns. The author writes
96
+ **only** the INSERT/UPDATE and returns `effects`; status/`rows_affected`/version
97
+ bookkeeping/receipt shape are produced by the helper.
98
+
99
+ ## Helper API (Python reference — handlers are often the app, not Node)
100
+
101
+ ```python
102
+ from synapsor_handler import writeback_handler, Job, Tx # pip install synapsor-handler
103
+
104
+ @writeback_handler(
105
+ token_env="SYNAPSOR_APP_HANDLER_TOKEN",
106
+ signing_secret_env="SYNAPSOR_APP_HANDLER_SIGNING_SECRET",
107
+ write_url_env="SYNAPSOR_APP_WRITE_URL",
108
+ )
109
+ def support_propose_plan_credit(job: Job, tx: Tx):
110
+ credit_id = f"CR-{job.proposal_id[-12:]}"
111
+ tx.insert("credits", id=credit_id, tenant_id=job.tenant_id, invoice_id=job.object_id,
112
+ customer_id=job.row["customer_id"], amount_cents=job.patch["credit_requested_cents"],
113
+ reason=job.patch["credit_reason"], created_by=job.principal)
114
+ tx.update("invoices", job.object_id,
115
+ credited_cents=job.row["credited_cents"] + job.patch["credit_requested_cents"])
116
+ return {"effects": [{"type": "db.insert", "table": "credits", "id": credit_id}]}
117
+
118
+ # Mount as a FastAPI/Flask route: app.post("/synapsor/writeback")(support_propose_plan_credit.asgi)
119
+ ```
120
+
121
+ `Job` is fully typed/validated: `proposal_id`, `idempotency_key`, `tenant_id`,
122
+ `object_id`, `principal`, `patch`, and `row` (the locked current row). `Tx` only
123
+ exposes scoped `insert`/`update`/`query` against the configured write URL.
124
+
125
+ ## What the helper guarantees (so the author can't forget)
126
+
127
+ 1. **Authenticity** — bearer + HMAC signature over the raw body. Without signing,
128
+ a handler trusts body-supplied `tenant_id`; with it, spoofing a writeback requires
129
+ the secret, not just network reach.
130
+ 2. **Tenant scope** — the locked-row `SELECT` always includes `tenant_id = scope.tenant_id`.
131
+ 3. **Stale-row guard** — `expected_version` compared at second precision (matching the
132
+ runner's own `versionValuesMatch`); mismatch → `conflict`, auto-rollback.
133
+ 4. **Idempotency** — a receipts/dedup row keyed by `idempotency_key`; replay → `already_applied`, no double write.
134
+ 5. **Atomicity** — author effects + receipt commit in one tx; any throw rolls back and returns a safe `failed`.
135
+ 6. **Safe receipts** — never leaks raw driver errors; maps exceptions to `safe_error_code`.
136
+
137
+ ## Security notes
138
+
139
+ - **Sign requests.** Add `signature = HMAC_SHA256(signing_secret, raw_body)` and a
140
+ short `issued_at` skew window. Document it as recommended for any handler not on loopback.
141
+ - The handler's DB credential should still be least-privilege (in the lab: `synapsor_app`
142
+ = SELECT/UPDATE invoices + SELECT/INSERT credits, nothing else). The helper doesn't
143
+ replace DB perms; it complements them.
144
+ - Receipts table/dedup store: the helper should create-or-require it and, on permission
145
+ error, print the exact `GRANT`/DDL (same gap as the direct-writeback receipts table).
146
+
147
+ ## Why this matters for adoption
148
+
149
+ App-owned executors are the answer to "rich writes" (INSERT/multi-row/events) — the
150
+ thing the runner deliberately won't do itself. But that answer is only safe if the
151
+ handler is safe, and right now safety is the integrator's homework. A first-party
152
+ helper makes the secure path the easy path, which is exactly the framing the whole
153
+ product is built on: don't hand people a footgun, hand them a reviewed capability.
154
+
155
+ ## Acceptance
156
+
157
+ - `createWritebackHandler` (TS) + `synapsor_handler` (Python) enforce auth, signature,
158
+ tenant, version, idempotency, atomicity with no author code.
159
+ - A handler written with the helper passes the same conflict/idempotency/tenant tests
160
+ the runner ships for direct writeback.
161
+ - Request/receipt schemas are published and versioned (`protocol_version`).
@@ -0,0 +1,97 @@
1
+ here's the detailed teardown of the OSS synapsor-runner specifically, split into (A) what trips up a serious dev, and (B) how an LLM actually experiences it. Everything below is from things I hit building and live-testing the lab, not theory. I'll keep Cloud-domain stuff out of it (RBAC, central ledger, etc. — those belong in hosted).
2
+
3
+ A. Limitations for a serious dev/user
4
+
5
+ 1. Config authoring is the real onboarding cost.
6
+ synapsor.runner.json is powerful but you mostly hand-write it. init --wizard exists, but multi-capability, executor-backed configs are manual, and there's no published JSON schema or one-page field reference — I learned the shape by running init and reading dist/runner.mjs. A serious dev wants: a versioned JSON Schema (so editors autocomplete + validate), and a "capability authoring" doc covering read vs proposal, patch (fixed/from_arg), allowed_columns, numeric_bounds, conflict_guard, executor.
7
+
8
+ 2. Env-var sprawl + historical credential ambiguity.
9
+ There are a lot: SYNAPSOR_DATABASE_READ_URL, _WRITE_URL, legacy SYNAPSOR_DATABASE_URL, TENANT_ID, PRINCIPAL, RUNNER_HTTP_TOKEN, plus per-executor *_HANDLER_URL/_TOKEN. The apply-uses-SYNAPSOR_DATABASE_URL-vs-write_url_env thing cost me real debugging on alpha.6 (now documented). doctor is good but only checks presence; it doesn't prove the write path (attempt a rolled-back probe write with the writer cred) or handler reachability.
10
+
11
+ 3. Store ↔ server lifecycle is a footgun.
12
+ The SQLite store is shared state, and the running server holds it open. Deleting/resetting the store under a live server gives corrupt/confusing behavior — I hit "database access issue" surfaced to the agent because I reset the store without restarting the server. There's no lock, warning, or coordinated reset. Also: running serve-http + serve-streamable-http on one store simultaneously is asking for contention, with no guardrail.
13
+
14
+ 4. The receipt-table permission gotcha.
15
+ Direct writeback does CREATE TABLE IF NOT EXISTS synapsor_writeback_receipts, which a least-privilege writer can't do (PG15+ no CREATE on public). Now documented, but doctor could detect it and print the exact GRANT/DDL (or a flag to pre-create). I had to invent the dedicated-schema trick myself.
16
+
17
+ 5. App-owned handlers re-implement security by hand.
18
+ The executor contract is "match the example": you must re-check tenant, parse change_set, enforce the expected_version stale-row guard, and do idempotency yourself. It's easy to write an insecure handler (skip the version check, forget tenant scope). There's no published handler SDK/helper and no request signing — the only auth is a bearer token, so the handler trusts the POST body's tenant/version. A first-party handler helper (verify + parse + enforce guards, optional HMAC signature) would remove a whole class of mistakes.
19
+
20
+ 6. Versioning discipline.
21
+ @alpha is a moving tag and behavior changed meaningfully across alpha.6→11 (transport, arg types string→number, credential resolution). I had to pin and bump six times. A stable channel + changelog + semver promise is table stakes before serious devs build on it.
22
+
23
+ 7. Two serve modes are easy to confuse.
24
+ serve-http (lightweight JSON-RPC, not real MCP) vs serve-streamable-http (spec MCP) on different ports, plus --alias-mode. I pointed the SDK at the wrong one. Consider: make serve-streamable-http the headline, rename serve-http to something like serve-bridge/--legacy-jsonrpc, and have mcp client-config always pair the client with the matching server command (it does for openai-agents — good).
25
+
26
+ 8. Observability is CLI-only.
27
+ replay/activity are genuinely nice, but there's no structured event stream/webhook for proposal.created/approved/applied. Even a local webhook would let people build a review UI or Slack-notify a reviewer without polling. (The full ledger is Cloud's job; a local event hook isn't.)
28
+
29
+ B. How easy is it for an LLM to use/understand?
30
+
31
+ This is where you have the most leverage, and I have direct evidence.
32
+
33
+ 1. Tool descriptions are the single biggest reliability lever — and they're currently generic.
34
+ On the JSON-RPC path my agent was reliable because my function-tool docstrings were rich ("Read an outage event — window, affected plan, credit policy; use this to decide if a waiver is justified"). On the native streamable path the model stalled and refused to propose, and a big reason was the auto-generated description: "Read public.outage_events through a reviewed Synapsor capability with trusted tenant context and evidence." That tells the model the plumbing, not what the tool is for or what it returns. The model didn't realize the outage tool gives it the policy it needed.
35
+ → Let capability config carry a model-facing description + per-arg descriptions + an optional "returns/when to use" hint, and surface them in tools/list. This alone would have made my streamable runs reliable. Right now authors can't easily improve what the model sees.
36
+
37
+ 2. Inconsistent result envelopes hurt the model (and the code).
38
+ Success = {status:"ok", data:{...}, evidence_bundle_id, trusted_context, ...}. Not-found = {ok:false, code, error}. Two different shapes for the same tool means the model (and my client) must branch on multiple keys. → One envelope always: {ok: true/false, data?, error?, summary}, where summary is a one-line natural-language result the model can echo to the user. The proposal result (proposal_id, diff: {before, proposed}, source_database_changed:false) is the best-designed part — model-legible and unambiguous; mirror that everywhere.
39
+
40
+ 3. Leaky/raw errors confuse the model.
41
+ A failed read surfaced connect ECONNREFUSED 127.0.0.1:5433 straight into the agent, which then told the user "database access issue." Raw infra errors are both a small info leak and bad for LLM behavior. → Safe, terse, actionable tool errors ("temporarily unavailable, retry later" / "not found in your tenant"), with details only in the local ledger.
42
+
43
+ 4. Dotted names vs aliases.
44
+ alias-mode openai correctly makes names valid (billing__inspect_invoice), but the alias diverges from any example/instruction that uses the canonical dotted name — so prompts that say "call propose_late_fee_waiver" don't match the tool the model sees. I had to make my agent instructions tool-name-agnostic. → Keep openai-safe aliases as the default for the openai-agents config (you do), and put the canonical name + purpose in the description so the model can still reason about it.
45
+
46
+ 5. Evidence handles are slightly confusing in-loop.
47
+ The model gets evidence_bundle_id but can't really do anything with it during the turn (it's for replay). Without a hint, a model may try to "use" it. A one-line "this is an audit handle; you don't need to act on it" in the description avoids wasted reasoning.
48
+
49
+ 6. No recommended agent-instruction snippet.
50
+ The biggest gap for first-time LLM integrators: the propose-first pattern needs the model to inspect all evidence before proposing and follow referenced IDs. I had to write that system prompt myself (and rewrite it once for the streamable path). → mcp client-config could optionally emit a recommended instruction block ("gather evidence first; follow referenced ids like OUT-…; propose only within policy; you cannot write or approve"). That would make day-1 agent behavior reliable instead of something each integrator rediscovers.
51
+
52
+ Net on LLM-friendliness: the conceptual model is excellent for LLMs — propose-only tools are inherently safe and easy for a model to call. The surface (generic descriptions, mixed envelopes, raw errors, alias/name divergence) is where reliability leaks. These are all fixable in the OSS runner and would noticeably raise the floor on agent reliability.
53
+
54
+ C. If I were prioritizing your OSS updates
55
+
56
+ P0 (adoption + LLM reliability):
57
+ 1. Author-supplied rich tool/arg descriptions in config, surfaced in tools/list.
58
+ 2. One consistent result envelope + safe terse errors + a summary field.
59
+ 3. Published config JSON Schema + a capability/executor authoring page.
60
+
61
+ P1 (trust + safety):
62
+ 4. Stable release channel + changelog/semver.
63
+ 5. Handler helper/SDK (parse + enforce tenant/version/idempotency, optional HMAC) so app-owned writeback is safe-by-default.
64
+ 6. doctor that probes the real write path + handler and emits exact GRANT/DDL for receipts; store/server lifecycle guardrails (warn on store-removed-under-server).
65
+
66
+ P2 (polish):
67
+ 7. Optional recommended agent-instruction snippet from mcp client-config.
68
+ 8. Local lifecycle webhook/event stream for proposals.
69
+
70
+
71
+
72
+ ---------------
73
+ 01-result-envelope-spec.md
74
+
75
+ One envelope for every tool result, so ok is the only thing client code (and the model) must branch on:
76
+
77
+ jsonc
78
+ { "ok": true, "summary": "<one-line NL the model reads/echoes>", "kind": "read|proposal",
79
+ "data": {…}|null, "proposal": {…}|null, "error": {"code","message","retryable"}|null,
80
+ "evidence": {…}|null, "source_database_changed": false, "_meta": {tenant, principal, canonical_capability} }
81
+
82
+ Key decisions, all grounded in what broke in my live runs:
83
+ - Kills the status vs ok / mixed-shape problem (today success uses status:"ok", errors use ok:false — two branches).
84
+ - Mandatory summary field — the LLM reads it first; fixed my "model parroted infra error" issue.
85
+ - Safe, stable error.code enum (9 codes, table included) — never raw ECONNREFUSED-style strings, which both leak and degrade agent behavior.
86
+ - Author-supplied description / per-arg description / returns_hint surfaced in tools/list — this is the single biggest LLM lever; the streamable stall I hit was the outage tool's generic description not telling the model it returns the policy.
87
+ - Migration path: result_format: 2 flag + dual-emit + old→new mapping table.
88
+
89
+ 02-handler-helper-interface.md
90
+
91
+ A first-party helper so app-owned executors are safe by default instead of "match the example."
92
+ - Formalizes (and versions) the request/receipt contract that's currently implicit.
93
+ - createWritebackHandler (TS) + synapsor_handler (Python — since real handlers are app code) where the author writes only the INSERT/UPDATE and returns effects; the helper enforces auth + HMAC signature, tenant scope, expected_version stale-row guard, idempotency, atomicity, safe receipts for them.
94
+ - Calls out request signing (today a handler trusts body-supplied tenant_id behind only a bearer token) and the receipts-table GRANT/DDL gap.
95
+ - Rationale tied to your own thesis: rich writes are the executor's job, but that's only safe if the handler is safe — so make the secure path the easy path.
96
+
97
+ Both are sized to drop into the repo as RFCs. If you want, next I can: turn the error-code enum into a concrete TypeScript type + a v1→v2 adapter shim, or sketch the Job/Tx type definitions for the handler helper so they're ready to implement.
@@ -0,0 +1,83 @@
1
+ # Store Lifecycle
2
+
3
+ Synapsor Runner keeps local evidence, query audit, proposals, receipts, replay,
4
+ and lifecycle events in a SQLite store.
5
+
6
+ Default path:
7
+
8
+ ```bash
9
+ ./.synapsor/local.db
10
+ ```
11
+
12
+ ## Server leases
13
+
14
+ MCP server modes write a small lease file next to the store:
15
+
16
+ ```text
17
+ <store>.lease.json
18
+ ```
19
+
20
+ The lease records the server pid, mode, transport, and start time. Destructive
21
+ store operations refuse to run while that lease points at a live process.
22
+
23
+ Use `--force` only after you have stopped the server or verified the lease is
24
+ stale.
25
+
26
+ ## Prune safely
27
+
28
+ Preview first:
29
+
30
+ ```bash
31
+ synapsor-runner store prune --store ./.synapsor/local.db --older-than 30d --dry-run
32
+ ```
33
+
34
+ Apply after review:
35
+
36
+ ```bash
37
+ synapsor-runner store prune --store ./.synapsor/local.db --older-than 30d --yes
38
+ ```
39
+
40
+ Override an active/stale lease:
41
+
42
+ ```bash
43
+ synapsor-runner store prune --store ./.synapsor/local.db --older-than 30d --yes --force
44
+ ```
45
+
46
+ ## Reset the local ledger
47
+
48
+ Reset deletes only the local SQLite ledger files:
49
+
50
+ ```bash
51
+ synapsor-runner store reset --store ./.synapsor/local.db --yes
52
+ ```
53
+
54
+ It removes:
55
+
56
+ ```text
57
+ local.db
58
+ local.db-wal
59
+ local.db-shm
60
+ local.db.lease.json
61
+ ```
62
+
63
+ It never touches your source Postgres/MySQL database. Like prune, reset refuses
64
+ while an active server lease exists unless you pass `--force` after verifying
65
+ the server is stopped or the lease is stale.
66
+
67
+ ## Deleted store under a running server
68
+
69
+ If the store file disappears while a server is still running, model-facing tool
70
+ calls fail safely with `TEMPORARILY_UNAVAILABLE`. Runner does not expose raw
71
+ SQLite paths, corruption text, or filesystem errors to the model.
72
+
73
+ Fix:
74
+
75
+ 1. Stop the running MCP server.
76
+ 2. Recreate the store by rerunning the demo/setup or restore the previous store.
77
+ 3. Restart the MCP server.
78
+
79
+ ## Concurrent server modes
80
+
81
+ Running multiple server transports against the same SQLite store can cause
82
+ contention and confusing local state. Runner refuses concurrent server leases by
83
+ default. Use `--allow-concurrent-store` only for controlled local debugging.
@@ -3,13 +3,13 @@
3
3
  Run the friendly doctor first:
4
4
 
5
5
  ```bash
6
- npx -y -p @synapsor/runner@alpha synapsor-runner doctor --first-run
6
+ npx -y -p @synapsor/runner synapsor-runner doctor --first-run
7
7
  ```
8
8
 
9
9
  Use JSON for automation:
10
10
 
11
11
  ```bash
12
- npx -y -p @synapsor/runner@alpha synapsor-runner doctor --first-run --json
12
+ npx -y -p @synapsor/runner synapsor-runner doctor --first-run --json
13
13
  ```
14
14
 
15
15
  ## Docker Missing
@@ -128,13 +128,13 @@ Own-database MCP setup needs a reviewed config before serving tools.
128
128
  Fix:
129
129
 
130
130
  ```bash
131
- npx -y -p @synapsor/runner@alpha synapsor-runner init --from-env DATABASE_URL --mode review --wizard
131
+ npx -y -p @synapsor/runner synapsor-runner init --from-env DATABASE_URL --mode review --wizard
132
132
  ```
133
133
 
134
134
  Or pass an example config:
135
135
 
136
136
  ```bash
137
- npx -y -p @synapsor/runner@alpha synapsor-runner tools preview --config ./examples/mcp-postgres-billing/synapsor.runner.json --store ./.synapsor/local.db
137
+ npx -y -p @synapsor/runner synapsor-runner tools preview --config ./examples/mcp-postgres-billing/synapsor.runner.json --store ./.synapsor/local.db
138
138
  ```
139
139
 
140
140
  ## SQLite Store Missing
@@ -180,7 +180,7 @@ Fix:
180
180
 
181
181
  ```bash
182
182
  export SYNAPSOR_DATABASE_READ_URL="<read-only-url>"
183
- npx -y -p @synapsor/runner@alpha synapsor-runner doctor --config synapsor.runner.json
183
+ npx -y -p @synapsor/runner synapsor-runner doctor --config synapsor.runner.json
184
184
  ```
185
185
 
186
186
  ## Read/Write Credential Split Failed
@@ -216,7 +216,7 @@ Fix:
216
216
  Regenerate the snippet:
217
217
 
218
218
  ```bash
219
- npx -y -p @synapsor/runner@alpha synapsor-runner mcp config claude-desktop \
219
+ npx -y -p @synapsor/runner synapsor-runner mcp config claude-desktop \
220
220
  --absolute-paths \
221
221
  --config ./synapsor.runner.json \
222
222
  --store ./.synapsor/local.db
@@ -0,0 +1,18 @@
1
+ # Use Your Own Database
2
+
3
+ The canonical guide is [Connect Your Own Database](getting-started-own-database.md).
4
+
5
+ Use it when you want to point Synapsor Runner at a staging Postgres/MySQL
6
+ database, inspect schemas/tables, generate one reviewed context/capability, and
7
+ serve semantic MCP tools without exposing raw SQL or write credentials to the
8
+ model.
9
+
10
+ Short path:
11
+
12
+ ```bash
13
+ export DATABASE_URL="postgresql://readonly_user:password@host:5432/app?sslmode=require"
14
+ npx -y -p @synapsor/runner synapsor-runner start --from-env DATABASE_URL --schema public
15
+ ```
16
+
17
+ Runner stores environment-variable names in `synapsor.runner.json`, not database
18
+ URLs. Keep credentials in your shell, process manager, or secret manager.