@synapsor/runner 1.4.0 → 1.4.12

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.
@@ -0,0 +1,298 @@
1
+ # Bounded Set Writeback
2
+
3
+ Synapsor Runner can apply a deliberately narrow class of reviewed
4
+ multi-row database changes to PostgreSQL and MySQL:
5
+
6
+ - fixed-predicate set `UPDATE`;
7
+ - fixed-predicate set `DELETE`;
8
+ - exact-review batch `INSERT`.
9
+
10
+ This is not model-generated SQL and it is not a generic batch API. The model
11
+ can ask to run a reviewed business action, but it cannot choose a table,
12
+ column, operator, ordering, predicate, tenant, or unbounded set.
13
+
14
+ ## Safety invariant
15
+
16
+ The worst action available to a prompt-injected model is to request one
17
+ contract-authored rule, against the trusted tenant, up to the reviewer-authored
18
+ row and value caps. Before approval, Runner freezes the exact ordered members.
19
+ Apply later touches only those members in one transaction and records every
20
+ identity and bounded digest.
21
+
22
+ Every bounded set requires:
23
+
24
+ 1. a fixed typed selection for existing-row UPDATE/DELETE, or the complete
25
+ reviewed item array for batch INSERT;
26
+ 2. `MAX ROWS n`, where `1 <= n <= 100`;
27
+ 3. at least one aggregate `MAX TOTAL` bound;
28
+ 4. trusted tenant binding;
29
+ 5. exact version guards and integer advancement for set UPDATE;
30
+ 6. source-unique per-item identities for batch INSERT;
31
+ 7. human/operator approval outside MCP;
32
+ 8. one atomic source transaction and an exact affected-row count;
33
+ 9. source or Runner-ledger receipts with the documented crash semantics.
34
+
35
+ A row cap is a rejection threshold, never `LIMIT n` truncation. Proposal reads
36
+ fetch at most `n + 1`; finding the extra row rejects before proposal
37
+ persistence. Aggregate overflow also rejects before persistence.
38
+
39
+ ## DSL examples
40
+
41
+ Fixed set UPDATE:
42
+
43
+ ```sql
44
+ PROPOSE ACTION close_overdue UPDATE SET
45
+ SELECT WHERE status = 'overdue'
46
+ MAX ROWS 10
47
+ MAX TOTAL balance_cents BEFORE 50000
48
+ ALLOW WRITE status
49
+ PATCH status = 'closed'
50
+ ADVANCE VERSION version USING INTEGER INCREMENT
51
+ APPROVAL ROLE billing_reviewer
52
+ WRITEBACK DIRECT SQL
53
+ ```
54
+
55
+ The selection is a contract literal. It is not an argument and is never taken
56
+ from the model. The first release supports equality terms joined by `AND`.
57
+
58
+ Exact-review batch INSERT:
59
+
60
+ ```sql
61
+ ARG items ROWS MAX 10 REQUIRED
62
+ ITEM FIELD items.id STRING REQUIRED MAX LENGTH 128
63
+ ITEM FIELD items.external_id STRING REQUIRED MAX LENGTH 128
64
+ ITEM FIELD items.amount_cents NUMBER REQUIRED MIN 1 MAX 2500
65
+ ITEM FIELD items.reason STRING REQUIRED MAX LENGTH 500
66
+
67
+ PROPOSE ACTION create_credits INSERT SET
68
+ BATCH ITEMS FROM ARG items
69
+ MAX ROWS 10
70
+ MAX TOTAL amount_cents AFTER 25000
71
+ DEDUP KEY tenant_id = TRUSTED TENANT, id = ITEM id, external_id = ITEM external_id
72
+ ALLOW WRITE amount_cents, reason
73
+ PATCH amount_cents = ITEM amount_cents
74
+ PATCH reason = ITEM reason
75
+ APPROVAL ROLE billing_reviewer
76
+ WRITEBACK DIRECT SQL
77
+ ```
78
+
79
+ The reviewer sees every allowlisted candidate item before approval. The source
80
+ must enforce the declared identity with a primary or unique constraint.
81
+
82
+ Set DELETE uses `DELETE SET`, a fixed selection, `MAX ROWS`, `MAX TOTAL`, and
83
+ an exact conflict guard. It has no patch. Runner rejects hard DELETE if it
84
+ cannot prove trigger/FK visibility or detects write triggers or widening
85
+ cascades. Prefer a set UPDATE of `deleted_at` or status when possible.
86
+
87
+ ## Worked contract-to-apply example
88
+
89
+ This PostgreSQL example closes two overdue tickets. MySQL is supported with
90
+ the same contract semantics; change the source engine and schema/database name
91
+ to match the inspected MySQL object.
92
+
93
+ Create the fixture with an administrator account, then give the Runner reader
94
+ `SELECT` and its separate writer only `SELECT, UPDATE` on this table:
95
+
96
+ ```sql
97
+ CREATE TABLE public.service_tickets (
98
+ id bigint PRIMARY KEY,
99
+ tenant_id text NOT NULL,
100
+ status text NOT NULL,
101
+ cost_cents integer NOT NULL,
102
+ version integer NOT NULL
103
+ );
104
+
105
+ INSERT INTO public.service_tickets
106
+ (id, tenant_id, status, cost_cents, version)
107
+ VALUES
108
+ (3, 'acme', 'overdue', 8000, 1),
109
+ (4, 'acme', 'overdue', 15000, 1),
110
+ (99, 'globex', 'overdue', 49000, 1);
111
+ ```
112
+
113
+ Save this as `tickets.synapsor.sql`:
114
+
115
+ ```sql
116
+ CREATE AGENT CONTEXT local_operator
117
+ BIND tenant_id FROM ENVIRONMENT SYNAPSOR_TENANT_ID REQUIRED
118
+ BIND principal FROM ENVIRONMENT SYNAPSOR_PRINCIPAL REQUIRED
119
+ TENANT BINDING tenant_id
120
+ PRINCIPAL BINDING principal
121
+ END
122
+
123
+ CREATE CAPABILITY tickets.close_overdue
124
+ USING CONTEXT local_operator
125
+ SOURCE local_db
126
+ ON public.service_tickets
127
+ PRIMARY KEY id
128
+ TENANT KEY tenant_id
129
+ CONFLICT GUARD version
130
+ LOOKUP reason BY id
131
+ ARG reason STRING REQUIRED MAX LENGTH 100
132
+ ALLOW READ id, tenant_id, status, cost_cents, version
133
+ REQUIRE EVIDENCE
134
+ PROPOSE ACTION close_overdue UPDATE SET
135
+ SELECT WHERE status = 'overdue'
136
+ MAX ROWS 10
137
+ MAX TOTAL cost_cents BEFORE 50000
138
+ ALLOW WRITE status
139
+ PATCH status = 'closed'
140
+ ADVANCE VERSION version USING INTEGER INCREMENT
141
+ APPROVAL ROLE ops_manager
142
+ WRITEBACK DIRECT SQL
143
+ END
144
+ ```
145
+
146
+ Compile the contract and create `synapsor.runner.json`:
147
+
148
+ ```bash
149
+ synapsor-runner dsl compile ./tickets.synapsor.sql \
150
+ --out ./synapsor.contract.json --strict
151
+ synapsor-runner contract validate ./synapsor.contract.json
152
+ ```
153
+
154
+ ```json
155
+ {
156
+ "version": 1,
157
+ "mode": "review",
158
+ "storage": { "sqlite_path": "./.synapsor/local.db" },
159
+ "sources": {
160
+ "local_db": {
161
+ "engine": "postgres",
162
+ "read_url_env": "SYNAPSOR_DATABASE_READ_URL",
163
+ "write_url_env": "SYNAPSOR_DATABASE_WRITE_URL",
164
+ "statement_timeout_ms": 3000,
165
+ "receipts": { "authority": "runner_ledger" }
166
+ }
167
+ },
168
+ "contracts": ["./synapsor.contract.json"]
169
+ }
170
+ ```
171
+
172
+ Load database URLs from your shell or secret manager. The config stores only
173
+ environment-variable names. Trusted tenant/principal values are process-owned,
174
+ not tool arguments:
175
+
176
+ ```bash
177
+ export SYNAPSOR_TENANT_ID=acme
178
+ export SYNAPSOR_PRINCIPAL=local_operator
179
+
180
+ synapsor-runner config validate --config ./synapsor.runner.json
181
+ synapsor-runner doctor --config ./synapsor.runner.json --check-writeback
182
+ synapsor-runner propose tickets.close_overdue \
183
+ --json '{"reason":"reviewed overdue-ticket close"}' \
184
+ --config ./synapsor.runner.json --store ./.synapsor/local.db
185
+ synapsor-runner proposals show latest --store ./.synapsor/local.db --details
186
+ ```
187
+
188
+ At this point IDs `3` and `4` are frozen in the proposal, but both source rows
189
+ are still `overdue` at version `1`; the `globex` row is outside trusted tenant
190
+ scope. Approval and apply remain operator-side commands and are not MCP tools:
191
+
192
+ ```bash
193
+ synapsor-runner proposals approve latest --actor ops_manager --yes \
194
+ --store ./.synapsor/local.db
195
+ synapsor-runner apply latest --config ./synapsor.runner.json \
196
+ --store ./.synapsor/local.db
197
+ synapsor-runner receipts list --proposal <proposal_id> \
198
+ --store ./.synapsor/local.db
199
+ synapsor-runner replay show --proposal <proposal_id> --details \
200
+ --store ./.synapsor/local.db
201
+ ```
202
+
203
+ Exactly IDs `3` and `4` become `closed` at version `2` in one transaction.
204
+ The receipt/replay list both safe primary-key identities and their bounded
205
+ digests. ID `99` is unchanged.
206
+
207
+ To test drift on a freshly seeded copy, create the proposal, then use a
208
+ database-operator session to increment one frozen row's version before
209
+ approval. `apply` returns `SET_DRIFT_CONFLICT`; the other frozen row remains
210
+ unchanged because no partial set mutation is committed:
211
+
212
+ ```sql
213
+ UPDATE public.service_tickets SET version = version + 1 WHERE id = 4;
214
+ ```
215
+
216
+ The example uses `runner_ledger`, which creates no receipt table in the source
217
+ database. For one local process, the local store is the authority; a fleet must
218
+ use authoritative shared-Postgres `runtime_store`. `source_db` authority
219
+ instead writes the mutation and receipt atomically in one source transaction
220
+ and requires its fixed precreated or auto-migrated receipt table. See
221
+ [Guarded CRUD receipt authority](guarded-crud-writeback.md).
222
+
223
+ ## Proposal and apply behavior
224
+
225
+ Proposal creation stores:
226
+
227
+ - the exact ordered primary keys and trusted tenant;
228
+ - every exact expected version;
229
+ - allowlisted before/after values or bounded digests;
230
+ - reviewed row count and aggregate values;
231
+ - a digest over the complete frozen set;
232
+ - pending human/operator approval.
233
+
234
+ Member and set digests use SHA-256 over JSON whose object keys are sorted
235
+ recursively. Array order remains significant because it is the reviewed,
236
+ deterministic primary-key order. JSON strings, finite numbers, booleans, and
237
+ null retain their protocol representation; unsupported runtime objects and
238
+ non-finite values are rejected rather than stringified implicitly. Runner
239
+ 1.4.1 also verifies the narrowly known deterministic raw-object order emitted
240
+ by 1.4.0, reconstructed from the complete stored reviewed data, so unchanged
241
+ 1.4.0 proposals remain applyable without weakening the drift checks.
242
+
243
+ Apply locks the frozen rows in deterministic primary-key order, rechecks every
244
+ reviewed value/version, performs every member mutation in one transaction, and
245
+ requires the affected count to equal the frozen count. One missing/stale row or
246
+ one database error rolls back the whole set.
247
+
248
+ Set `sources.<name>.statement_timeout_ms` to bound source waits. PostgreSQL
249
+ uses transaction-local statement and lock timeouts. MySQL applies the value to
250
+ read/preflight execution and rounds it up to whole seconds for InnoDB lock
251
+ waits; MySQL does not offer the same general DML statement timeout as
252
+ PostgreSQL.
253
+
254
+ The protocol-v3 receipt and replay include every primary key plus the bounded
255
+ before/after or tombstone digests. Kept-out fields are never added merely to
256
+ support a set operation.
257
+
258
+ ## Receipt authority and ambiguity
259
+
260
+ `source_db` authority commits the mutation and receipt in the same source
261
+ transaction. It gives the strongest retry classification.
262
+
263
+ `runner_ledger` authority creates no source receipt table, but no atomic
264
+ transaction spans the source and Runner ledger. A process loss after source
265
+ commit can therefore produce `reconciliation_required`. Runner does not retry
266
+ or guess. The operator reconciliation path re-inspects only the frozen,
267
+ allowlisted identities and records an immutable decision.
268
+
269
+ ## Explicit boundary
270
+
271
+ Use an [app-owned executor](writeback-executors.md) for:
272
+
273
+ - model-supplied or free-form predicates;
274
+ - ranges, wildcards, ordering, subqueries, or dynamic identifiers;
275
+ - more than 100 rows or any unbounded batch;
276
+ - UPSERT/MERGE, DDL, stored procedures, or cross-table/database transactions;
277
+ - external API calls, events, files, emails, payments, or other side effects;
278
+ - a hard DELETE whose trigger/cascade effects cannot be proven bounded.
279
+
280
+ Policy auto-approval is not supported for bounded sets. Every set proposal
281
+ requires a verified human/operator decision.
282
+
283
+ ## Verification
284
+
285
+ Run the disposable-engine gate:
286
+
287
+ ```bash
288
+ corepack pnpm test:bounded-set
289
+ ```
290
+
291
+ It verifies both PostgreSQL and MySQL: the exact DSL/contract path under both
292
+ receipt authorities; cap+1 and aggregate rejection before persistence; exact
293
+ UPDATE/DELETE; batch INSERT; idempotent retry; independent version, predicate,
294
+ aggregate, writable-value, missing-member, and tenant drift; injected mid-set
295
+ rollback; insert dedup/atomicity; delete trigger/cascade refusal; exact receipt
296
+ members; Runner-ledger reconciliation; and 1/10/100-row bounds. See the
297
+ [local benchmark note](benchmarks/bounded-set-local.md) and the normative
298
+ [safety RFC](rfcs/005-bounded-set-writeback.md).
@@ -447,3 +447,9 @@ Commit authority stays outside the model:
447
447
  ```text
448
448
  human/operator approval -> guarded writeback or app-owned handler -> receipt/replay
449
449
  ```
450
+
451
+ If your application already exposes fixed parameterized functions, that is a
452
+ good foundation rather than an anti-pattern. Read [Why Synapsor Over Prompt And
453
+ Application Guardrails](why-synapsor-vs-app-guardrails.md) to decide whether
454
+ you also need Runner's shared contract, trusted context, evidence, approval,
455
+ receipt, replay, and compensation lifecycle.
@@ -368,7 +368,14 @@ npx -y -p @synapsor/runner synapsor-runner smoke call --config ./synapsor.runner
368
368
 
369
369
  `smoke call` uses the same runtime as the MCP server. It records evidence and
370
370
  query audit for read tools, or creates a proposal for proposal tools, then
371
- prints the commands to inspect evidence/proposals/replay from the local store.
371
+ prints the commands to inspect evidence/proposals/replay. With the default
372
+ storage topology those records are in local SQLite. With
373
+ `storage.shared_postgres.mode = "runtime_store"`, Runner `1.4.12` and later
374
+ write them to the authoritative shared Postgres ledger and include `--config`
375
+ in follow-up commands. The supplied `--store` path is compatibility plumbing
376
+ for those CLI commands; it does not receive an orphan proposal copy. If the
377
+ shared ledger is unavailable, `smoke call` fails safely and does not fall back
378
+ to SQLite.
372
379
 
373
380
  The snippets contain the local command and args. They must not contain database
374
381
  URLs, passwords, approval tools, commit tools, or write credentials.
@@ -340,6 +340,16 @@ from `url_env`, auto-runs the shared-ledger migration, and serializes runtime
340
340
  mutations with a transaction-scoped advisory lock. The MCP tools still expose no
341
341
  database URLs or write credentials to the model.
342
342
 
343
+ `smoke call` uses the same runtime-store resolver as MCP serving. Its evidence,
344
+ query audit, proposal, events, and replay records therefore land directly in
345
+ the shared Postgres ledger and are immediately visible to another Runner using
346
+ the same config. `--store` remains accepted so the printed follow-up CLI
347
+ commands have a compatibility/bridge path, but that SQLite path is not an
348
+ authoritative copy and normally is not created by the smoke call. Shared-ledger
349
+ connection failure returns the safe availability error and a nonzero exit; it
350
+ never causes a local fallback. This consistency fix is present in Runner
351
+ `1.4.12` and later.
352
+
343
353
  `runtime_store` covers MCP serving, CLI approval/apply, and supervised worker
344
354
  runs. For CLI mutations, Runner restores the shared ledger into a temporary
345
355
  local store while holding the Postgres advisory lock, runs the existing local
@@ -10,6 +10,47 @@ npx -y -p @synapsor/runner synapsor-runner demo --quick
10
10
  The OSS runner command is `synapsor-runner`. The `synapsor` command is reserved
11
11
  for the Synapsor Cloud CLI.
12
12
 
13
+ ## 1.4.12 (prepared, not published)
14
+
15
+ ### Runtime-store smoke-call consistency
16
+
17
+ - Fixes BUG-017, where `smoke call` could put proposal artifacts in the
18
+ requested local SQLite path even though the config selected authoritative
19
+ shared Postgres `runtime_store` mode.
20
+ - Smoke calls now use the same runtime storage resolver as MCP tool calls. A
21
+ second Runner can immediately inspect the proposal, evidence, query audit,
22
+ events, and replay from the shared ledger.
23
+ - Shared-ledger unavailability fails closed with a safe retryable error,
24
+ nonzero exit status, no credential leakage, and no local orphan proposal.
25
+ - Local SQLite and mirror modes retain their existing behavior; no source row
26
+ changes before the normal external approval/apply path.
27
+
28
+ Prepared package version: `@synapsor/runner@1.4.12`.
29
+ `@synapsor/dsl` remains `1.4.1`; `@synapsor/spec` remains `1.4.0`.
30
+
31
+ ## 1.4.1 (prepared, not published)
32
+
33
+ ### Bounded-set digest compatibility patch
34
+
35
+ - Contract-authored bounded-set proposals now use deterministic recursive
36
+ object-key ordering for member and set digest material.
37
+ - Valid proposals created by `1.4.0` remain applyable; the compatibility path
38
+ accepts only the known deterministic `1.4.0` serializations reconstructed
39
+ from the complete stored reviewed data.
40
+ - Genuine member, version, value, aggregate, membership, or tenant drift still
41
+ fails closed before source mutation on PostgreSQL and MySQL.
42
+ - The Runner package now includes the linked bounded-set guide and validates
43
+ all shipped local Markdown links while packaging.
44
+ - The DSL package description and README no longer label the current `1.4.x`
45
+ package as a `0.1 preview`. Canonical contract `spec_version: "0.1"` is
46
+ unchanged.
47
+ - Adds an honest prompt-and-application-guardrails decision guide covering SQL
48
+ authority, hand-built semantic tools, structural enforcement, build-vs-adopt
49
+ fit, and regulated-data boundaries.
50
+
51
+ Prepared package versions: `@synapsor/dsl@1.4.1` and
52
+ `@synapsor/runner@1.4.1`. `@synapsor/spec` remains `1.4.0`.
53
+
13
54
  ## 1.4.0 (prepared, not published)
14
55
 
15
56
  ### Reviewed Reversible Change Sets
@@ -0,0 +1,43 @@
1
+ # Result Envelope V3
2
+
3
+ Protocol v3 is used only for bounded set direct writeback. Protocol v1 and v2
4
+ remain accepted for legacy UPDATE and guarded single-row CRUD.
5
+
6
+ The public artifacts are:
7
+
8
+ - `synapsor.change-set.v3`;
9
+ - `synapsor.writeback-job.v3`;
10
+ - `synapsor.execution-receipt.v3`.
11
+
12
+ Each v3 object names one operation: `set_update`, `set_delete`, or
13
+ `batch_insert`. It carries the same frozen-set authority:
14
+
15
+ ```json
16
+ {
17
+ "max_rows": 10,
18
+ "row_count": 2,
19
+ "aggregate_bounds": [
20
+ { "column": "balance_cents", "measure": "before", "maximum": 20000, "actual": 15000 }
21
+ ],
22
+ "members": [
23
+ {
24
+ "primary_key": { "column": "id", "value": "INV-1" },
25
+ "expected_version": { "column": "version", "value": 1 },
26
+ "before_digest": "sha256:...",
27
+ "after_digest": "sha256:..."
28
+ }
29
+ ],
30
+ "set_digest": "sha256:..."
31
+ }
32
+ ```
33
+
34
+ The complete protocol fixtures live in [`fixtures/protocol`](../fixtures/protocol)
35
+ and the JSON Schemas live in [`schemas`](../schemas). The execution receipt
36
+ reports every `target_identity` and matching `member_effect`, plus receipt
37
+ authority, set digest, exact affected count, safe status/error, and receipt
38
+ hash. An applied result is invalid unless its affected count and member-effect
39
+ count match the frozen identity count.
40
+
41
+ `runner_ledger` may return `reconciliation_required` with an intent ID when a
42
+ cross-database crash window prevents proof of commit. That is a terminal
43
+ operator state, not a retry instruction or an `already_applied` guess.
@@ -20,6 +20,13 @@ trusted context, evidence handles, query audit, local inspection, and
20
20
  proposal-first writes. Proposal workflows add full replay across evidence,
21
21
  approval, writeback receipts, and events.
22
22
 
23
+ Prompt instructions and application validation are separate layers. Prompts
24
+ can guide behavior but cannot supply authorization. A fixed, parameterized
25
+ application tool can be a sound boundary for a small system; Runner adds a
26
+ portable reviewed contract and a consistent proposal, approval, receipt,
27
+ replay, and compensation lifecycle. See [Why Synapsor Over Prompt And
28
+ Application Guardrails](why-synapsor-vs-app-guardrails.md).
29
+
23
30
  The model-facing MCP server exposes reviewed semantic tools such as
24
31
  `billing.inspect_invoice` and `billing.propose_late_fee_waiver`.
25
32
 
@@ -12,6 +12,26 @@ Use JSON for automation:
12
12
  npx -y -p @synapsor/runner synapsor-runner doctor --first-run --json
13
13
  ```
14
14
 
15
+ ## Smoke Proposal Missing From Another Runner
16
+
17
+ What happened:
18
+
19
+ `smoke call` returned a proposal id, but `proposals list --config ...` on a
20
+ second Runner cannot find it.
21
+
22
+ Fix:
23
+
24
+ 1. Verify `synapsor-runner --version` is `1.4.12` or later.
25
+ 2. Confirm both commands use a config whose
26
+ `storage.shared_postgres.mode` is `runtime_store` and whose `url_env` and
27
+ `schema` identify the same ledger.
28
+ 3. Run `store shared-postgres status --url-env <ENV> --schema <SCHEMA>`.
29
+
30
+ In `runtime_store` mode, `--store` is not the authoritative ledger. Runner does
31
+ not fall back to that SQLite path when shared Postgres is unavailable. Versions
32
+ before `1.4.12` could orphan smoke-call artifacts locally; recreate that test
33
+ proposal after upgrading.
34
+
15
35
  ## Docker Missing
16
36
 
17
37
  What happened:
@@ -0,0 +1,169 @@
1
+ # Why Synapsor Over Prompt And Application Guardrails
2
+
3
+ You can build a safe database tool in application code. The important question
4
+ is not whether the code is custom or packaged. It is where authority lives and
5
+ which safety properties are enforced outside the model.
6
+
7
+ Synapsor Runner is useful when a narrow application function is no longer the
8
+ whole requirement. It adds a shared, reviewable contract and an operational
9
+ record around model-facing reads and writes: trusted context, evidence, query
10
+ audit, proposals, approval outside MCP, guarded writeback, idempotency
11
+ receipts, replay, and opt-in reviewed compensation.
12
+
13
+ It does not make prompts trustworthy, prevent prompt injection, replace
14
+ database permissions, or make an application compliant by itself.
15
+
16
+ ## Prompt Instructions Are Not A Security Boundary
17
+
18
+ A system prompt can improve normal model behavior. It cannot grant or remove
19
+ database authority. **Prompt-only enforcement is not a security boundary.** A
20
+ confused or prompt-injected model can ignore an instruction, so a rule such as
21
+ "only access this tenant" must be enforced by trusted code, database policy,
22
+ or both.
23
+
24
+ This is a general agent-security issue, not a hypothetical specific to one
25
+ vendor:
26
+
27
+ - [OWASP LLM01: Prompt Injection](https://genai.owasp.org/llmrisk/llm01-prompt-injection/)
28
+ describes direct and indirect prompt injection and recommends constrained
29
+ model behavior plus human approval for high-risk actions.
30
+ - [General Analysis published a controlled Supabase MCP
31
+ demonstration](https://generalanalysis.com/blog/supabase-mcp-blog) in which
32
+ untrusted support-ticket content influenced an agent with broad database
33
+ access and exposed integration-token data. The demonstration disabled
34
+ read-only mode and used a privileged `service_role`; it was a controlled
35
+ security test, not evidence that every Supabase MCP deployment is
36
+ vulnerable.
37
+ - [Supabase's defense-in-depth
38
+ response](https://supabase.com/blog/defense-in-depth-mcp) likewise recommends
39
+ a protective layer, least privilege, and avoiding direct production access.
40
+
41
+ The lesson is not that a prompt can never be useful. It is that prompt text
42
+ must not be the control that protects tenant scope, hidden fields, approval,
43
+ or commit authority.
44
+
45
+ ## First Ask: Who Produces The SQL?
46
+
47
+ ### The model produces SQL
48
+
49
+ If the model emits arbitrary SQL and application code decides whether to run
50
+ it, that validator must correctly understand the database grammar,
51
+ subqueries, functions, views, permissions, row scope, side effects, and future
52
+ schema changes. A blocklist or regular expression is not an authorization
53
+ system. This remains the `execute_sql` problem with an extra parser in front
54
+ of it.
55
+
56
+ Prefer fixed, parameterized statements selected by trusted application code,
57
+ restricted database roles, views, and row-level security. Do not make the
58
+ model the query planner for privileged production access.
59
+
60
+ ### Your application produces SQL
61
+
62
+ If application code selects a fixed parameterized statement and exposes a
63
+ narrow function such as `billing.inspect_invoice`, you have already made the
64
+ most important architectural move: a semantic business tool replaces raw SQL.
65
+ For a small application, that may be enough.
66
+
67
+ The next question is whether you also need a consistent review and operations
68
+ layer. Without one, each function must separately implement and preserve:
69
+
70
+ - trusted tenant and principal binding;
71
+ - read and write field allowlists;
72
+ - evidence and query-audit records;
73
+ - exact proposed diffs before a write;
74
+ - approval identity and quorum outside the model;
75
+ - conflict, transition, numeric, row-count, and aggregate guards;
76
+ - idempotent execution and receipts;
77
+ - activity search and replay;
78
+ - bounded, separately approved compensation where reversibility is possible.
79
+
80
+ Synapsor centralizes those mechanics in a contract and runtime instead of
81
+ requiring every tool handler to invent them independently.
82
+
83
+ ## What Runner Adds
84
+
85
+ | Concern | Prompt or one-off handler | Synapsor Runner |
86
+ | --- | --- | --- |
87
+ | Model-facing authority | Depends on instructions and handler design | Only reviewed semantic capabilities are served |
88
+ | Tenant/principal scope | Often passed as model arguments or scattered checks | Bound from trusted environment or authenticated session context |
89
+ | Data exposure | Handler-specific projection | Contract-reviewed visible fields and kept-out fields |
90
+ | Risky writes | Handler may write during the tool call | MCP creates a proposal; approval and apply stay outside MCP |
91
+ | Concurrency | Must be designed per handler | Version/conflict guards and frozen-set drift checks fail closed |
92
+ | Retry behavior | Must be designed per handler | Idempotency keys and source or Runner-ledger receipts |
93
+ | Investigation | Application logs | Evidence handles, query audit, proposal events, receipts, activity, and replay |
94
+ | Undo | Usually another bespoke endpoint | Opt-in reviewed compensation for supported direct writes |
95
+ | Review surface | Prompt, code, and policies may diverge | Portable canonical contract plus SQL-like DSL |
96
+
97
+ This is structural enforcement only for traffic that actually passes through
98
+ Runner and its reviewed capabilities. An unrestricted credential, a second raw
99
+ SQL MCP server, or a bypass path in the application remains outside the
100
+ boundary.
101
+
102
+ The local SQLite or shared Postgres ledger is durable through Runner's
103
+ supported interfaces, but a trusted host or database administrator can alter
104
+ local state. Use Synapsor Cloud or your own tamper-evident retention and access
105
+ controls when organizational audit requirements demand a stronger shared
106
+ record.
107
+
108
+ ## Build Or Adopt
109
+
110
+ Your own code is probably enough when all of these are true:
111
+
112
+ - the agent is read-only or has one or two fixed, low-risk functions;
113
+ - the application already derives tenant scope from authenticated server
114
+ state;
115
+ - database roles, views, and row-level security provide the required floor;
116
+ - you do not need approval history, evidence, receipts, replay, or reviewed
117
+ compensation;
118
+ - one team owns every tool and can test every change consistently.
119
+
120
+ Consider Runner when one or more of these are true:
121
+
122
+ - agents can propose writes to customer, billing, support, health, finance,
123
+ or other consequential data;
124
+ - tenant or principal isolation must not depend on model-supplied arguments;
125
+ - a human or operator must approve some changes outside the agent loop;
126
+ - retries, stale writes, bounded sets, or partial failures need explicit
127
+ fail-closed behavior;
128
+ - investigators need to answer what the agent read, requested, approved, and
129
+ changed;
130
+ - capabilities are growing across teams, agents, databases, or MCP clients;
131
+ - the safety boundary needs to be reviewed in source control as one portable
132
+ contract.
133
+
134
+ This is not a claim that the mechanics are impossible to build. It is a choice
135
+ between maintaining them independently in every integration and adopting a
136
+ tested contract/runtime that makes the same controls visible and repeatable.
137
+
138
+ ## Regulated And High-Consequence Data
139
+
140
+ Health, finance, and other regulated workloads make the distinction more
141
+ important, but Runner does not confer HIPAA, PCI DSS, SOC 2, or any other
142
+ compliance status. A real deployment still needs the appropriate legal and
143
+ organizational controls, least-privilege roles, encryption, retention,
144
+ monitoring, incident response, vendor agreements, and human access policy.
145
+
146
+ Runner can contribute technical evidence for that program: tenant-bound
147
+ capabilities, explicit field projections, proposal/approval separation,
148
+ signed operator identity where configured, guarded writeback receipts, and
149
+ replay. Validate those controls with your security and compliance owners
150
+ before using production regulated data.
151
+
152
+ ## Evaluate It Against Your Existing Layer
153
+
154
+ 1. Run the static MCP risk review against your current tools:
155
+
156
+ ```bash
157
+ npx -y @synapsor/runner audit ./tools-list.json
158
+ ```
159
+
160
+ 2. List every rule currently enforced only by a prompt.
161
+ 3. Identify whether the model or trusted code chooses SQL, tenant scope,
162
+ columns, predicates, approval, and commit timing.
163
+ 4. Pick one consequential staging workflow and compare its current evidence,
164
+ retry, conflict, approval, receipt, and investigation behavior with the
165
+ [own-database Runner flow](getting-started-own-database.md).
166
+
167
+ Keep your existing database permissions either way. Runner shapes the
168
+ agent-facing interface; it does not replace the database's own authorization
169
+ boundary.
@@ -64,6 +64,7 @@ INSERT INTO public.invoices (id, tenant_id, status, late_fee_cents, waiver_reaso
64
64
  ('INV-ACME', 'acme', 'overdue', 2500, NULL, '2026-07-12T12:00:00Z'),
65
65
  ('INV-GLOBEX', 'globex', 'overdue', 3100, NULL, '2026-07-12T12:00:00Z'),
66
66
  ('INV-RATECO', 'rateco', 'overdue', 900, NULL, '2026-07-12T12:00:00Z'),
67
+ ('INV-SMOKE-CALL', 'acme', 'overdue', 2100, NULL, '2026-07-12T12:00:00Z'),
67
68
  ('INV-BATCH-A', 'acme', 'overdue', 1100, NULL, '2026-07-12T12:00:00Z'),
68
69
  ('INV-BATCH-B', 'acme', 'overdue', 1300, NULL, '2026-07-12T12:00:00Z'),
69
70
  ('INV-QUORUM-RACE', 'acme', 'overdue', 1400, NULL, '2026-07-12T12:00:00Z'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synapsor/runner",
3
- "version": "1.4.0",
3
+ "version": "1.4.12",
4
4
  "description": "Stop giving AI agents execute_sql; expose reviewed Postgres/MySQL MCP actions with proposals, approval, writeback, and replay.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -35,6 +35,7 @@
35
35
  "AGENTS.md",
36
36
  "CHANGELOG.md",
37
37
  "CONTRIBUTING.md",
38
+ "SECURITY.md",
38
39
  "LICENSE",
39
40
  "NOTICE",
40
41
  "THREAT_MODEL.md",