@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.
- package/CHANGELOG.md +50 -0
- package/README.md +39 -33
- package/SECURITY.md +11 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/runner.mjs +172 -72
- package/docs/README.md +4 -0
- package/docs/benchmarks/bounded-set-local.md +27 -0
- package/docs/bounded-set-writeback.md +298 -0
- package/docs/capability-authoring.md +6 -0
- package/docs/getting-started-own-database.md +8 -1
- package/docs/production.md +10 -0
- package/docs/release-notes.md +41 -0
- package/docs/result-envelope-v3.md +43 -0
- package/docs/security-boundary.md +7 -0
- package/docs/troubleshooting-first-run.md +20 -0
- package/docs/why-synapsor-vs-app-guardrails.md +169 -0
- package/examples/runner-fleet/seed/postgres.sql +1 -0
- package/package.json +2 -1
|
@@ -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
|
|
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.
|
package/docs/production.md
CHANGED
|
@@ -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
|
package/docs/release-notes.md
CHANGED
|
@@ -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.
|
|
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",
|