@synapsor/runner 1.1.0 → 1.4.0
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 +105 -1
- package/README.md +29 -7
- package/THREAT_MODEL.md +13 -4
- package/dist/cli.d.ts +5 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/local-ui.d.ts.map +1 -1
- package/dist/runner.mjs +10406 -4932
- package/docs/README.md +19 -3
- package/docs/capability-authoring.md +20 -8
- package/docs/conformance.md +12 -0
- package/docs/current-scope.md +12 -4
- package/docs/doctor.md +21 -7
- package/docs/dsl-reference.md +91 -5
- package/docs/getting-started-own-database.md +26 -3
- package/docs/guarded-crud-writeback.md +218 -0
- package/docs/limitations.md +30 -9
- package/docs/oss-vs-cloud.md +1 -1
- package/docs/production.md +52 -20
- package/docs/release-notes.md +82 -2
- package/docs/release-policy.md +1 -1
- package/docs/result-envelope-v2.md +20 -6
- package/docs/result-envelope-v4.md +47 -0
- package/docs/reversible-change-sets.md +151 -0
- package/docs/rfcs/004-guarded-crud-receipt-authority.md +165 -0
- package/docs/rfcs/005-bounded-set-writeback.md +164 -0
- package/docs/rfcs/006-reviewed-reversible-change-sets.md +152 -0
- package/docs/runner-config-reference.md +83 -9
- package/docs/running-a-runner-fleet.md +2 -1
- package/docs/security-boundary.md +36 -9
- package/docs/writeback-executors.md +12 -11
- package/examples/runner-fleet/seed/mysql.sql +47 -0
- package/examples/runner-fleet/seed/postgres.sql +48 -0
- package/examples/support-plan-credit/README.md +31 -0
- package/examples/support-plan-credit/contract.synapsor.sql +26 -0
- package/examples/support-plan-credit/expected-output/tools-preview.txt +1 -0
- package/examples/support-plan-credit/seed/001_seed.sql +14 -1
- package/examples/support-plan-credit/synapsor.contract.json +101 -1
- package/examples/support-plan-credit/synapsor.runner.json +7 -1
- package/fixtures/protocol/MANIFEST.json +131 -1
- package/fixtures/protocol/change-set.bounded-update.v3.json +57 -0
- package/fixtures/protocol/change-set.delete.v2.json +31 -0
- package/fixtures/protocol/change-set.insert.v2.json +36 -0
- package/fixtures/protocol/change-set.update.v2.json +32 -0
- package/fixtures/protocol/compensation-change-set.update.v1.json +40 -0
- package/fixtures/protocol/execution-receipt.bounded-update-applied.v3.json +39 -0
- package/fixtures/protocol/execution-receipt.compensation-update-applied.v4.json +37 -0
- package/fixtures/protocol/execution-receipt.delete-applied.v2.json +25 -0
- package/fixtures/protocol/execution-receipt.insert-applied.v2.json +24 -0
- package/fixtures/protocol/execution-receipt.reconciliation-required.v2.json +25 -0
- package/fixtures/protocol/execution-receipt.update-applied.v2.json +25 -0
- package/fixtures/protocol/writeback-job.bounded-update.v3.json +43 -0
- package/fixtures/protocol/writeback-job.compensation-update.v4.json +34 -0
- package/fixtures/protocol/writeback-job.delete.v2.json +22 -0
- package/fixtures/protocol/writeback-job.insert.v2.json +28 -0
- package/fixtures/protocol/writeback-job.update.v2.json +24 -0
- package/package.json +1 -1
- package/schemas/change-set.v2.schema.json +245 -0
- package/schemas/change-set.v3.schema.json +112 -0
- package/schemas/compensation-change-set.v1.schema.json +32 -0
- package/schemas/execution-receipt.v2.schema.json +106 -0
- package/schemas/execution-receipt.v3.schema.json +40 -0
- package/schemas/execution-receipt.v4.schema.json +22 -0
- package/schemas/inverse-descriptor.v1.schema.json +46 -0
- package/schemas/synapsor.runner.schema.json +132 -2
- package/schemas/writeback-job.v2.schema.json +238 -0
- package/schemas/writeback-job.v3.schema.json +53 -0
- package/schemas/writeback-job.v4.schema.json +17 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# RFC 004: Guarded CRUD Receipt Authority
|
|
2
|
+
|
|
3
|
+
Status: implementation gate
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Runner 1.2 adds guarded single-row INSERT and DELETE while making source receipt
|
|
8
|
+
DDL optional. It must preserve the defining boundary: a reviewed, tenant-bound,
|
|
9
|
+
one-row business effect with evidence, external approval, a receipt, and replay.
|
|
10
|
+
|
|
11
|
+
This RFC is required before adapter expansion because receipt placement changes
|
|
12
|
+
which outcomes can be proven after a crash.
|
|
13
|
+
|
|
14
|
+
## Configuration Model
|
|
15
|
+
|
|
16
|
+
Two decisions are orthogonal:
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
receipt_authority: source_db | runner_ledger
|
|
20
|
+
source_receipt_provisioning: precreated | auto_migrate
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`source_receipt_provisioning` is valid only for `source_db`.
|
|
24
|
+
|
|
25
|
+
For compatibility, omitted fields mean `source_db` plus `precreated`. A new
|
|
26
|
+
wizard may recommend `auto_migrate`, but it must obtain explicit consent before
|
|
27
|
+
granting Runner DDL authority. `runner_ledger` is opt-in and must explain its
|
|
28
|
+
post-commit ambiguity.
|
|
29
|
+
|
|
30
|
+
Receipt schema/table identifiers are operator config, validated as fixed safe
|
|
31
|
+
identifiers. They are never contract fields or model arguments.
|
|
32
|
+
|
|
33
|
+
## Terms
|
|
34
|
+
|
|
35
|
+
- **Exactly-once classification:** Runner can prove whether this proposal was
|
|
36
|
+
applied, conflicted, or failed.
|
|
37
|
+
- **At-most-once business effect:** the source cannot perform the reviewed
|
|
38
|
+
effect twice for the same deterministic identity.
|
|
39
|
+
- **Idempotent effect:** repeating an operation converges on the same source
|
|
40
|
+
state, but may not prove who caused it.
|
|
41
|
+
- **Ambiguous outcome:** the source may have committed, but Runner lacks an
|
|
42
|
+
atomic receipt proving that fact. This requires reconciliation.
|
|
43
|
+
|
|
44
|
+
These terms are not interchangeable.
|
|
45
|
+
|
|
46
|
+
## Claim Matrix
|
|
47
|
+
|
|
48
|
+
The claims below apply equally to Postgres and MySQL. Engine-specific tests must
|
|
49
|
+
prove them separately.
|
|
50
|
+
|
|
51
|
+
| Operation | Authority | Source prerequisite | Normal result | Retry after completed receipt | Crash after source commit, before Runner completion |
|
|
52
|
+
| --- | --- | --- | --- | --- | --- |
|
|
53
|
+
| UPDATE | `source_db` | exact PK+tenant+version guard; source receipt table | atomic one-row UPDATE and receipt | `already_applied` | atomic transaction proves applied or rolled back |
|
|
54
|
+
| INSERT | `source_db` | trusted tenant; reviewed insert columns; proposal-specific PK/UNIQUE dedup key; source receipt table | atomic one-row INSERT and receipt | `already_applied` | atomic transaction proves applied or rolled back |
|
|
55
|
+
| DELETE | `source_db` | exact PK+tenant+version guard; no expanding cascade/write trigger; source receipt table | atomic one-row DELETE and receipt | `already_applied` | atomic transaction proves applied or rolled back |
|
|
56
|
+
| UPDATE | `runner_ledger` | authoritative shared ledger for fleets; exact PK+tenant+version guard; monotonic version advancement in source transaction | intent, one-row UPDATE, terminal ledger receipt | completed matching receipt returns `already_applied` | advanced guard plus incomplete intent is `reconciliation_required` |
|
|
57
|
+
| INSERT | `runner_ledger` | authoritative shared ledger for fleets; proposal-specific source PK/UNIQUE key | intent, one-row INSERT, terminal ledger receipt | completed matching receipt returns `already_applied` | matching unique row plus incomplete intent is reconciled only when provenance is provable; otherwise `reconciliation_required` |
|
|
58
|
+
| DELETE | `runner_ledger` | authoritative shared ledger for fleets; exact PK+tenant+version guard; no expanding side effects | intent, one-row DELETE, terminal ledger receipt | completed matching receipt returns `already_applied` | missing row plus incomplete intent is `reconciliation_required`, not proof of this delete |
|
|
59
|
+
|
|
60
|
+
## Failure and Crash Matrix
|
|
61
|
+
|
|
62
|
+
| Point/outcome | `source_db` | `runner_ledger` |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| ledger/source unavailable before intent | no source effect; retryable unavailable | no source effect; retryable unavailable |
|
|
65
|
+
| crash before source BEGIN | no source effect | durable intent remains; safe retry only if no ambiguous attempt was entered |
|
|
66
|
+
| crash after BEGIN, before mutation | transaction rolls back | source transaction rolls back; intent can be marked failed/retried by bounded operator/worker logic |
|
|
67
|
+
| stale/version conflict | conflict receipt commits atomically | conflict terminal event is recorded; no source mutation |
|
|
68
|
+
| crash after mutation, before source COMMIT | transaction rolls back | source transaction rolls back; no business effect |
|
|
69
|
+
| crash after source COMMIT, before Runner completion | impossible as a split outcome because source receipt shares the transaction | terminal state is `reconciliation_required`; never auto-retry |
|
|
70
|
+
| concurrent identical apply | source receipt serializes one outcome | ledger intent plus source guard/unique key permits at most one effect; ambiguous loser reconciles |
|
|
71
|
+
| source unavailable after intent | no source effect | intent remains failed/retryable only while no source transaction could have committed |
|
|
72
|
+
|
|
73
|
+
## Operation Proof Obligations
|
|
74
|
+
|
|
75
|
+
### UPDATE
|
|
76
|
+
|
|
77
|
+
Runner-ledger UPDATE requires a real version guard and monotonic advancement in
|
|
78
|
+
the same source transaction. Accepted strategies are a Runner-generated integer
|
|
79
|
+
increment or a database-generated version whose changed value is verified
|
|
80
|
+
before commit. A name such as `updated_at` is not proof. Insufficient timestamp
|
|
81
|
+
precision fails preflight.
|
|
82
|
+
|
|
83
|
+
An advanced source version plus an incomplete intent is ambiguous, not
|
|
84
|
+
`already_applied`.
|
|
85
|
+
|
|
86
|
+
### INSERT
|
|
87
|
+
|
|
88
|
+
INSERT requires a proposal-specific deterministic identity backed by an
|
|
89
|
+
inspected PRIMARY KEY or UNIQUE constraint. The model cannot set tenant,
|
|
90
|
+
identity/generated columns, or arbitrary columns. The contract declares the
|
|
91
|
+
portable dedup mapping; Runner config declares receipt placement.
|
|
92
|
+
|
|
93
|
+
Ledger memory by itself does not prevent a duplicate INSERT after a process
|
|
94
|
+
crash. A source uniqueness constraint is mandatory.
|
|
95
|
+
|
|
96
|
+
### DELETE
|
|
97
|
+
|
|
98
|
+
DELETE targets one reviewed row by primary key, trusted tenant, and expected
|
|
99
|
+
version. The proposal freezes allowlisted before evidence. A hard delete is
|
|
100
|
+
refused when cascading foreign keys or write triggers can expand the reviewed
|
|
101
|
+
effect. Soft delete remains the preferred path.
|
|
102
|
+
|
|
103
|
+
A missing row after an incomplete Runner-ledger attempt proves only that the
|
|
104
|
+
row is absent, not that this attempt deleted it.
|
|
105
|
+
|
|
106
|
+
## Durable Runner-Ledger Intent
|
|
107
|
+
|
|
108
|
+
Before touching the source, Runner durably stores:
|
|
109
|
+
|
|
110
|
+
- proposal ID, version, and hash;
|
|
111
|
+
- capability and operation;
|
|
112
|
+
- trusted tenant and target/dedup identity;
|
|
113
|
+
- expected guard;
|
|
114
|
+
- bounded attempt number;
|
|
115
|
+
- state and timestamps.
|
|
116
|
+
|
|
117
|
+
Allowed states are:
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
intent_recorded
|
|
121
|
+
applying
|
|
122
|
+
applied
|
|
123
|
+
already_applied
|
|
124
|
+
conflict
|
|
125
|
+
failed
|
|
126
|
+
reconciliation_required
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Transitions append immutable events. An incomplete `applying` attempt is never
|
|
130
|
+
automatically restarted if the source may have committed.
|
|
131
|
+
|
|
132
|
+
## Reconciliation
|
|
133
|
+
|
|
134
|
+
The operator command re-reads only the reviewed target under trusted tenant
|
|
135
|
+
scope and displays expected versus observed allowlisted metadata. It never
|
|
136
|
+
guesses. A verified operator can mark `applied`, `conflict`, or `failed` with a
|
|
137
|
+
reason; Runner appends a reconciliation event and receipt. Production signed
|
|
138
|
+
identity requirements apply unchanged.
|
|
139
|
+
|
|
140
|
+
## Topology Rules
|
|
141
|
+
|
|
142
|
+
- local SQLite Runner-ledger authority is allowed only for one local operator
|
|
143
|
+
process;
|
|
144
|
+
- Streamable HTTP fleets and multiple workers require shared Postgres
|
|
145
|
+
`runtime_store` as the authoritative ledger;
|
|
146
|
+
- `mirror` is not authoritative enough for Runner-ledger write intents;
|
|
147
|
+
- unsafe topology fails at config/doctor/start preflight.
|
|
148
|
+
|
|
149
|
+
## Security Invariants
|
|
150
|
+
|
|
151
|
+
- raw SQL, receipt controls, identifiers, approval, apply, credentials, tenant,
|
|
152
|
+
and principal remain outside model-facing tools;
|
|
153
|
+
- all identifiers are contract/config validated and all values are parameters;
|
|
154
|
+
- unknown, stale, ambiguous, or multi-row effects fail closed;
|
|
155
|
+
- kept-out fields are not persisted in intents, receipts, metrics, or replay;
|
|
156
|
+
- local resource reads continue to reauthorize tenant and principal;
|
|
157
|
+
- no claim of distributed atomic exactly-once is made for `runner_ledger`.
|
|
158
|
+
|
|
159
|
+
## Bounded Set Gate
|
|
160
|
+
|
|
161
|
+
This RFC does not authorize multi-row writes. Set operations require a separate
|
|
162
|
+
RFC with fixed typed predicates, frozen exact targets, mandatory count/value
|
|
163
|
+
bounds, deterministic row locking, one transaction, exact receipts, and all
|
|
164
|
+
R1-R7 threat mitigations. `MAX ROWS` syntax alone is not shippable.
|
|
165
|
+
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# RFC 005: Bounded Set Writeback
|
|
2
|
+
|
|
3
|
+
Status: implementation gate
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Runner 1.3 may execute a reviewed set write only when the set is fixed by the
|
|
8
|
+
contract, frozen before approval, bounded by both row count and business value,
|
|
9
|
+
and applied atomically. A row cap by itself is not a safety boundary.
|
|
10
|
+
|
|
11
|
+
This RFC authorizes three direct operations:
|
|
12
|
+
|
|
13
|
+
- fixed-predicate set UPDATE;
|
|
14
|
+
- fixed-predicate set DELETE;
|
|
15
|
+
- exact-review batch INSERT.
|
|
16
|
+
|
|
17
|
+
Free-form predicates, model-selected columns or operators, unbounded sets,
|
|
18
|
+
UPSERT, DDL, cross-table work, and external effects remain app-owned executor
|
|
19
|
+
work.
|
|
20
|
+
|
|
21
|
+
## Canonical Model
|
|
22
|
+
|
|
23
|
+
Set operations are explicit. They never reuse a single-row lookup by merely
|
|
24
|
+
raising `max_rows`.
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"operation": {
|
|
29
|
+
"kind": "update",
|
|
30
|
+
"cardinality": "set",
|
|
31
|
+
"selection": {
|
|
32
|
+
"all": [
|
|
33
|
+
{ "column": "status", "operator": "eq", "value": "overdue" }
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"max_rows": 10,
|
|
37
|
+
"aggregate_bounds": [
|
|
38
|
+
{ "column": "balance_cents", "measure": "before", "maximum": 50000 }
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The first release accepts only contract literals in `selection`. The typed AST
|
|
45
|
+
reserves no raw SQL escape hatch. The model can trigger the reviewed capability
|
|
46
|
+
but cannot choose a predicate, column, operator, ordering, range, wildcard, or
|
|
47
|
+
set size.
|
|
48
|
+
|
|
49
|
+
Batch INSERT uses an explicitly typed bounded item-array argument. Every item
|
|
50
|
+
is fully materialized in the proposal, receives the trusted tenant, and has a
|
|
51
|
+
source-enforced unique dedup identity. A generic JSON blob is not accepted.
|
|
52
|
+
|
|
53
|
+
## Hard Limits
|
|
54
|
+
|
|
55
|
+
- `MAX ROWS` is mandatory and must be between 1 and 100.
|
|
56
|
+
- at least one aggregate value bound is mandatory;
|
|
57
|
+
- a proposal query fetches at most `MAX ROWS + 1` rows;
|
|
58
|
+
- at most 64 reviewed columns may be captured per row;
|
|
59
|
+
- before/after payloads and digests are bounded before persistence;
|
|
60
|
+
- overflow rejects the proposal; it is never truncated to the cap.
|
|
61
|
+
|
|
62
|
+
The implementation ceiling of 100 is a safety limit, not a throughput claim.
|
|
63
|
+
|
|
64
|
+
## Frozen Set
|
|
65
|
+
|
|
66
|
+
At proposal time Runner stores, in deterministic primary-key order:
|
|
67
|
+
|
|
68
|
+
- exact primary key;
|
|
69
|
+
- trusted tenant;
|
|
70
|
+
- expected version for UPDATE/DELETE;
|
|
71
|
+
- allowlisted before and after values;
|
|
72
|
+
- per-row before and after digests;
|
|
73
|
+
- count, aggregate measurements, and one set digest.
|
|
74
|
+
|
|
75
|
+
Apply never re-runs the broad selection predicate. It locks the frozen primary
|
|
76
|
+
keys in the same deterministic order, rechecks tenant and every expected
|
|
77
|
+
version, and mutates the complete set in one transaction. A missing or stale
|
|
78
|
+
member rolls back the whole transaction.
|
|
79
|
+
|
|
80
|
+
## Approval
|
|
81
|
+
|
|
82
|
+
All set operations require verified human/operator approval in 1.3. Policy
|
|
83
|
+
auto-approval is rejected rather than silently downgraded. Set hard DELETE and
|
|
84
|
+
batch INSERT are never auto-approved. This keeps row/value-aware policy design
|
|
85
|
+
out of the first release.
|
|
86
|
+
|
|
87
|
+
## Exact Effects
|
|
88
|
+
|
|
89
|
+
The receipt and replay carry every affected primary key and bounded before/
|
|
90
|
+
after digest. Kept-out fields are never fetched merely to improve audit detail.
|
|
91
|
+
The source receipt and mutation share one transaction in `source_db` mode.
|
|
92
|
+
`runner_ledger` mode retains the documented post-source-commit ambiguity and
|
|
93
|
+
uses `reconciliation_required`; it never retries an ambiguous batch.
|
|
94
|
+
|
|
95
|
+
## Operation Rules
|
|
96
|
+
|
|
97
|
+
### Set UPDATE
|
|
98
|
+
|
|
99
|
+
- fixed literal predicate;
|
|
100
|
+
- fixed or scalar-argument patch with existing per-field bounds;
|
|
101
|
+
- exact version guard and monotonic version advance for every row;
|
|
102
|
+
- mandatory aggregate bound;
|
|
103
|
+
- one transaction and exact affected-row count.
|
|
104
|
+
|
|
105
|
+
### Set DELETE
|
|
106
|
+
|
|
107
|
+
- fixed literal predicate;
|
|
108
|
+
- exact version guard for every row;
|
|
109
|
+
- verified human approval;
|
|
110
|
+
- no write triggers or cascading foreign keys;
|
|
111
|
+
- bounded tombstones and exact identities.
|
|
112
|
+
|
|
113
|
+
Soft delete remains the preferred set operation because it is a guarded
|
|
114
|
+
UPDATE and can later be reversed safely.
|
|
115
|
+
|
|
116
|
+
### Batch INSERT
|
|
117
|
+
|
|
118
|
+
- one typed item array with `max_items` no greater than `MAX ROWS`;
|
|
119
|
+
- every candidate row shown in the proposal;
|
|
120
|
+
- trusted tenant injected by Runner;
|
|
121
|
+
- source UNIQUE/PRIMARY KEY dedup identity for every item;
|
|
122
|
+
- per-item bounds plus a mandatory aggregate bound;
|
|
123
|
+
- all rows and source receipts commit atomically or none do.
|
|
124
|
+
|
|
125
|
+
## Threat Cases And Fixtures
|
|
126
|
+
|
|
127
|
+
| Threat | Mitigation | Conformance fixture |
|
|
128
|
+
| --- | --- | --- |
|
|
129
|
+
| R1 selection abuse | literal typed predicate; no model predicate inputs | `bounded-set-r1-fixed-selection` |
|
|
130
|
+
| R2 misleading cap | row cap plus aggregate value bound; overflow rejects | `bounded-set-r2-count-value-cap` |
|
|
131
|
+
| R3 TOCTOU drift | frozen PK/version set; apply never broad-requeries | `bounded-set-r3-frozen-drift` |
|
|
132
|
+
| R4 partial application | one transaction; exact count; rollback on any member | `bounded-set-r4-atomic-rollback` |
|
|
133
|
+
| R5 audit collapse | every key and bounded before/after digest in receipt/replay | `bounded-set-r5-exact-receipt` |
|
|
134
|
+
| R6 DELETE cascades | hard DELETE preflight rejects cascades/write triggers | `bounded-set-r6-delete-side-effects` |
|
|
135
|
+
| R7 auto-approval amplification | human approval mandatory; policy mode rejected | `bounded-set-r7-human-approval` |
|
|
136
|
+
|
|
137
|
+
Each fixture must be exercised by canonical validation and runtime/adapter
|
|
138
|
+
tests. A fixture file that is never loaded by a test is not evidence.
|
|
139
|
+
|
|
140
|
+
## Crash And Receipt Matrix
|
|
141
|
+
|
|
142
|
+
| Outcome | `source_db` | `runner_ledger` |
|
|
143
|
+
| --- | --- | --- |
|
|
144
|
+
| before source BEGIN | no effect | durable intent, no effect |
|
|
145
|
+
| one member stale | complete rollback and conflict receipt | complete rollback and terminal conflict |
|
|
146
|
+
| process death before source commit | complete rollback | complete rollback; intent may be safely resolved as not committed |
|
|
147
|
+
| process death after source commit | source mutation and receipt prove outcome atomically | `reconciliation_required`; never automatic retry |
|
|
148
|
+
| retry after completed receipt | `already_applied` | `already_applied` from authoritative ledger |
|
|
149
|
+
|
|
150
|
+
No mode claims a distributed transaction across Runner's ledger and an external
|
|
151
|
+
source database.
|
|
152
|
+
|
|
153
|
+
## Release Gate
|
|
154
|
+
|
|
155
|
+
The feature remains unavailable until Postgres and MySQL live tests prove:
|
|
156
|
+
|
|
157
|
+
- cap+1 and aggregate overflow reject before proposal persistence;
|
|
158
|
+
- one stale member and injected failure leave every source row unchanged;
|
|
159
|
+
- lock order is deterministic;
|
|
160
|
+
- exact IDs/digests appear in receipts and replay;
|
|
161
|
+
- hard DELETE side effects and auto-approval are rejected;
|
|
162
|
+
- batch INSERT dedup and crash behavior are safe in both receipt modes;
|
|
163
|
+
- seeded 1, 10, and 100 row measurements are recorded as local evidence only.
|
|
164
|
+
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# RFC 006: Reviewed Reversible Change Sets
|
|
2
|
+
|
|
3
|
+
Status: implemented and verified for the prepared Runner 1.4 release line
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
An applied direct database write currently has evidence, approval, a guarded
|
|
8
|
+
writeback receipt, and replay, but no bounded compensating action. Correcting a
|
|
9
|
+
mistake requires an operator to author a new forward proposal manually.
|
|
10
|
+
|
|
11
|
+
Runner should be able to prepare a reviewed compensation without becoming a
|
|
12
|
+
database time-travel system or introducing an unreviewed rollback path.
|
|
13
|
+
|
|
14
|
+
## Safety boundary
|
|
15
|
+
|
|
16
|
+
Reversal is opt-in contract authority:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"reversibility": {
|
|
21
|
+
"mode": "reviewed_inverse"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The declaration is valid only for Runner-owned `direct_sql` writeback. It does
|
|
27
|
+
not apply to app handlers, Cloud workers, email, payments, or any other
|
|
28
|
+
external effect.
|
|
29
|
+
|
|
30
|
+
`synapsor-runner revert <proposal-id>` creates a new proposal. It never:
|
|
31
|
+
|
|
32
|
+
- mutates the source database;
|
|
33
|
+
- approves the proposal;
|
|
34
|
+
- bypasses tenant or operator identity checks;
|
|
35
|
+
- appears in the model-facing MCP tool list.
|
|
36
|
+
|
|
37
|
+
The new proposal must be independently approved and applied through the normal
|
|
38
|
+
trusted operator path.
|
|
39
|
+
|
|
40
|
+
## Protocol split
|
|
41
|
+
|
|
42
|
+
Published v1, v2, and v3 forward writeback jobs keep their existing meaning.
|
|
43
|
+
Reversible forward jobs carry an additive, explicit inverse-capture request.
|
|
44
|
+
The resulting execution receipt carries a bounded inverse descriptor only
|
|
45
|
+
after a successful, unambiguous source transaction.
|
|
46
|
+
|
|
47
|
+
Compensation is executed through a distinct normalized protocol v4 job. A v4
|
|
48
|
+
job contains:
|
|
49
|
+
|
|
50
|
+
- the exact source, tenant, and primary-key authority;
|
|
51
|
+
- a bounded, deterministically ordered member list;
|
|
52
|
+
- the original single-row or set cardinality;
|
|
53
|
+
- the state expected after the forward write;
|
|
54
|
+
- only the reviewed values to restore or remove;
|
|
55
|
+
- the original row and aggregate caps;
|
|
56
|
+
- the original approval role and quorum;
|
|
57
|
+
- lineage linking root, parent, and reverted proposals and the forward receipt;
|
|
58
|
+
- a maximum lineage depth.
|
|
59
|
+
|
|
60
|
+
This prevents compensation semantics from being inferred from a legacy job.
|
|
61
|
+
|
|
62
|
+
## Operation semantics
|
|
63
|
+
|
|
64
|
+
### UPDATE and soft delete
|
|
65
|
+
|
|
66
|
+
The inverse restores only previously reviewed writable values. The row must
|
|
67
|
+
still match the exact reviewed state left by the forward write. A monotonic
|
|
68
|
+
version column advances again during compensation; it is never decremented.
|
|
69
|
+
Any intervening write fails closed.
|
|
70
|
+
|
|
71
|
+
A soft delete is an UPDATE of a reviewed status or deletion marker and follows
|
|
72
|
+
these same rules.
|
|
73
|
+
|
|
74
|
+
### INSERT
|
|
75
|
+
|
|
76
|
+
The inverse deletes only the exact Runner-created identity. The current row
|
|
77
|
+
must still match the allowlisted values inserted by Runner. Trigger and foreign
|
|
78
|
+
key safety is rechecked immediately before deletion. A new dependent row or a
|
|
79
|
+
changed value blocks compensation.
|
|
80
|
+
|
|
81
|
+
Because this delete removes a row whose complete Runner-supplied values are
|
|
82
|
+
known, its own inverse may reinsert that exact row. It cannot infer or restore
|
|
83
|
+
unreviewed database-generated or trigger-generated state.
|
|
84
|
+
|
|
85
|
+
### Hard DELETE
|
|
86
|
+
|
|
87
|
+
A general hard DELETE records bounded reviewed before-state evidence but is
|
|
88
|
+
not automatically restorable. Generated identities, hidden required columns,
|
|
89
|
+
cascaded children, and trigger side effects cannot be reconstructed safely.
|
|
90
|
+
The receipt marks the inverse unavailable with specific reason codes and the
|
|
91
|
+
CLI recommends soft delete or an app-owned compensation capability.
|
|
92
|
+
|
|
93
|
+
## Bounded sets
|
|
94
|
+
|
|
95
|
+
Set compensation uses exactly the original frozen identities. It cannot widen
|
|
96
|
+
the predicate or increase the original row/value caps. Every member is locked
|
|
97
|
+
in deterministic primary-key order and checked before any mutation. One stale
|
|
98
|
+
member rolls back the entire transaction.
|
|
99
|
+
|
|
100
|
+
The inverse descriptor is bounded to 100 members and 256 scalar fields per
|
|
101
|
+
member, matching the public protocol hard ceilings.
|
|
102
|
+
|
|
103
|
+
## Receipt authority and crash behavior
|
|
104
|
+
|
|
105
|
+
With source-database receipt authority, mutation and source receipt remain in
|
|
106
|
+
one transaction. The inverse descriptor is deterministic from the immutable
|
|
107
|
+
job plus the state verified in that transaction, so an idempotent retry can
|
|
108
|
+
reproduce it without rereading kept-out columns.
|
|
109
|
+
|
|
110
|
+
With Runner-ledger authority, the existing cross-database crash window remains.
|
|
111
|
+
An ambiguous forward or compensation outcome enters
|
|
112
|
+
`reconciliation_required`. Runner does not generate or apply a revert until an
|
|
113
|
+
operator reconciles the original outcome and an applied receipt with an
|
|
114
|
+
available inverse exists.
|
|
115
|
+
|
|
116
|
+
## Lineage
|
|
117
|
+
|
|
118
|
+
Every compensation proposal records:
|
|
119
|
+
|
|
120
|
+
- `root_proposal_id`;
|
|
121
|
+
- `parent_proposal_id`;
|
|
122
|
+
- `reverts_proposal_id`;
|
|
123
|
+
- `forward_receipt_hash`;
|
|
124
|
+
- `depth`.
|
|
125
|
+
|
|
126
|
+
Depth is capped at 16. The same applied receipt may have only one active or
|
|
127
|
+
completed compensation child. To undo a compensation, the operator reverts the
|
|
128
|
+
child proposal, creating a linear audited chain instead of a cycle.
|
|
129
|
+
|
|
130
|
+
## Data minimization
|
|
131
|
+
|
|
132
|
+
Inverse capture may retain only:
|
|
133
|
+
|
|
134
|
+
- fixed primary-key and trusted tenant identity;
|
|
135
|
+
- conflict/version metadata;
|
|
136
|
+
- fields in the reviewed write allowlist;
|
|
137
|
+
- Runner-supplied deterministic deduplication fields needed to identify an
|
|
138
|
+
inserted row.
|
|
139
|
+
|
|
140
|
+
Visible read fields that were not writable and all kept-out fields are excluded.
|
|
141
|
+
Reversibility is rejected when the safe inverse cannot be represented with that
|
|
142
|
+
bounded data.
|
|
143
|
+
|
|
144
|
+
## Explicit non-goals
|
|
145
|
+
|
|
146
|
+
- point-in-time recovery or WAL replacement;
|
|
147
|
+
- automatic rollback;
|
|
148
|
+
- model-facing revert, approve, apply, or receipt tools;
|
|
149
|
+
- restoration of arbitrary hard deletes;
|
|
150
|
+
- compensation of app-owned executors or external effects;
|
|
151
|
+
- cross-table, cross-database, or unbounded compensation;
|
|
152
|
+
- weakening the existing optimistic concurrency and receipt-authority model.
|
|
@@ -112,7 +112,13 @@ not print database URLs or create the schema.
|
|
|
112
112
|
"read_url_env": "BILLING_POSTGRES_READ_URL",
|
|
113
113
|
"write_url_env": "BILLING_POSTGRES_WRITE_URL",
|
|
114
114
|
"read_only": false,
|
|
115
|
-
"statement_timeout_ms": 3000
|
|
115
|
+
"statement_timeout_ms": 3000,
|
|
116
|
+
"receipts": {
|
|
117
|
+
"authority": "source_db",
|
|
118
|
+
"provisioning": "precreated",
|
|
119
|
+
"schema": "synapsor",
|
|
120
|
+
"table": "writeback_receipts"
|
|
121
|
+
}
|
|
116
122
|
}
|
|
117
123
|
}
|
|
118
124
|
}
|
|
@@ -121,8 +127,13 @@ not print database URLs or create the schema.
|
|
|
121
127
|
`engine` is `postgres` or `mysql`. `read_url_env` is required. Use a
|
|
122
128
|
least-privilege read credential. `write_url_env` is optional unless direct SQL
|
|
123
129
|
writeback must apply; it should name a separate restricted writer. `read_only:
|
|
124
|
-
true` forbids direct SQL writeback. `statement_timeout_ms` is
|
|
125
|
-
timeout
|
|
130
|
+
true` forbids direct SQL writeback. `statement_timeout_ms` is an
|
|
131
|
+
operator-controlled positive timeout used for reads and direct SQL writeback.
|
|
132
|
+
PostgreSQL applies it as transaction-local `statement_timeout` and
|
|
133
|
+
`lock_timeout`. MySQL applies it to read/preflight execution and rounds it up
|
|
134
|
+
to whole seconds for `innodb_lock_wait_timeout`; MySQL does not provide the
|
|
135
|
+
same general DML statement-timeout guarantee as PostgreSQL. `ssl` carries
|
|
136
|
+
adapter-specific reviewed SSL options when used.
|
|
126
137
|
|
|
127
138
|
A contract's `SOURCE billing_postgres` must exactly match a `sources` key.
|
|
128
139
|
|
|
@@ -131,8 +142,31 @@ Long-running servers reuse native driver pools. Optional `pool` keys are
|
|
|
131
142
|
`idle_timeout_ms` (30000), `queue_timeout_ms` (5000), and `queue_limit`
|
|
132
143
|
(default `max(10, max_connections * 4)`). Queue overflow returns
|
|
133
144
|
`SOURCE_POOL_QUEUE_FULL`; acquisition timeout returns `SOURCE_POOL_TIMEOUT`.
|
|
145
|
+
With result envelope v2, these conditions and recognized transient driver
|
|
146
|
+
connection failures are model-facing `TEMPORARILY_UNAVAILABLE` errors with
|
|
147
|
+
`retryable: true` and a bounded `retry_after_ms`. Operational logs retain only
|
|
148
|
+
a normalized safe runtime code, not the driver message, host, or credentials.
|
|
134
149
|
One-shot CLI commands close their pools.
|
|
135
150
|
|
|
151
|
+
### Source receipts
|
|
152
|
+
|
|
153
|
+
`sources.<name>.receipts.authority` is `source_db` or `runner_ledger`.
|
|
154
|
+
|
|
155
|
+
- `source_db` requires `provisioning: "precreated"` or `"auto_migrate"`.
|
|
156
|
+
Optional fixed `schema` and `table` identifiers select the receipt table.
|
|
157
|
+
The source mutation and receipt share one transaction.
|
|
158
|
+
- `runner_ledger` forbids `provisioning`, `schema`, and `table`. It creates no
|
|
159
|
+
source receipt table. Local SQLite is permitted for one process;
|
|
160
|
+
networked/multi-Runner use requires authoritative shared Postgres
|
|
161
|
+
`runtime_store` and rejects mirror mode.
|
|
162
|
+
|
|
163
|
+
Existing configs without `receipts` retain the compatibility source-receipt
|
|
164
|
+
behavior. New onboarding asks explicitly. Runner-ledger UPDATE requires exact
|
|
165
|
+
version advancement; INSERT requires source-enforced deduplication; DELETE
|
|
166
|
+
requires an exact version guard. Ambiguous post-commit outcomes stop at
|
|
167
|
+
`reconciliation_required`. See
|
|
168
|
+
[Guarded Single-Row CRUD Writeback](guarded-crud-writeback.md).
|
|
169
|
+
|
|
136
170
|
## Trusted context
|
|
137
171
|
|
|
138
172
|
```json
|
|
@@ -227,6 +261,39 @@ fields are documented in the JSON Schema and
|
|
|
227
261
|
lookup, visible fields, evidence, patch allowlist, bounds/transitions, conflict
|
|
228
262
|
guard, approval, and writeback mode.
|
|
229
263
|
|
|
264
|
+
## Bounded set operation fields
|
|
265
|
+
|
|
266
|
+
A set operation is portable reviewed authority, so it can arrive from a
|
|
267
|
+
contract or the equivalent embedded capability:
|
|
268
|
+
|
|
269
|
+
```json
|
|
270
|
+
{
|
|
271
|
+
"operation": {
|
|
272
|
+
"kind": "update",
|
|
273
|
+
"cardinality": "set",
|
|
274
|
+
"selection": {
|
|
275
|
+
"all": [{ "column": "status", "operator": "eq", "value": "overdue" }]
|
|
276
|
+
},
|
|
277
|
+
"max_rows": 10,
|
|
278
|
+
"aggregate_bounds": [
|
|
279
|
+
{ "column": "balance_cents", "measure": "before", "maximum": 50000 }
|
|
280
|
+
],
|
|
281
|
+
"version_advance": { "column": "version", "strategy": "integer_increment" }
|
|
282
|
+
},
|
|
283
|
+
"approval": { "mode": "human", "required_role": "billing_reviewer" },
|
|
284
|
+
"writeback": { "mode": "direct_sql" }
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Set UPDATE/DELETE requires one through eight fixed equality terms in
|
|
289
|
+
`selection.all`; the model cannot add or override them. Every set requires
|
|
290
|
+
`max_rows` from 1 through 100 and one through eight aggregate bounds. Set
|
|
291
|
+
UPDATE requires integer version advancement. Batch INSERT instead declares
|
|
292
|
+
`operation.batch.items_from_arg` pointing to a bounded `object_array` argument
|
|
293
|
+
and item-field dedup components. All forms require human/operator approval and
|
|
294
|
+
direct SQL writeback in 1.3. See [Bounded Set
|
|
295
|
+
Writeback](bounded-set-writeback.md).
|
|
296
|
+
|
|
230
297
|
## Executors
|
|
231
298
|
|
|
232
299
|
```json
|
|
@@ -247,7 +314,10 @@ guard, approval, and writeback mode.
|
|
|
247
314
|
}
|
|
248
315
|
```
|
|
249
316
|
|
|
250
|
-
Types are `sql_update`, `http_handler`, and `command_handler`.
|
|
317
|
+
Types are `sql_update`, `http_handler`, and `command_handler`. The legacy
|
|
318
|
+
`sql_update` executor identifier now dispatches the reviewed native operation
|
|
319
|
+
(`insert`, `update`, or `delete`) and remains named for config compatibility.
|
|
320
|
+
HTTP handlers use
|
|
251
321
|
`url_env`, optional `POST|PUT|PATCH`, bearer auth, signing secret, and timeout.
|
|
252
322
|
Command handlers use `command_env` and timeout. Secrets stay in the environment.
|
|
253
323
|
See [Writeback Executors](writeback-executors.md).
|
|
@@ -382,9 +452,10 @@ capability permissions, and trusted session metadata.
|
|
|
382
452
|
|
|
383
453
|
## Direct SQL readiness
|
|
384
454
|
|
|
385
|
-
|
|
386
|
-
needs target-table SELECT
|
|
387
|
-
SELECT
|
|
455
|
+
For `source_db` + `precreated`, an administrator applies `writeback migration`
|
|
456
|
+
once. The steady-state writer needs target-table `SELECT` and only the reviewed
|
|
457
|
+
operation DML plus receipt-table `SELECT`/`INSERT`/`UPDATE`; it does not need
|
|
458
|
+
schema `CREATE`.
|
|
388
459
|
|
|
389
460
|
```bash
|
|
390
461
|
synapsor-runner writeback migration --engine postgres --schema synapsor
|
|
@@ -393,5 +464,8 @@ synapsor-runner writeback grants --engine postgres \
|
|
|
393
464
|
synapsor-runner doctor --config ./synapsor.runner.json --check-writeback
|
|
394
465
|
```
|
|
395
466
|
|
|
396
|
-
|
|
397
|
-
|
|
467
|
+
`source_db` + `auto_migrate` runs that fixed migration idempotently and needs
|
|
468
|
+
bounded `CREATE`. `runner_ledger` skips source receipt DDL/DML and validates its
|
|
469
|
+
authoritative ledger topology. Doctor uses rollback-only receipt and target
|
|
470
|
+
checks and does not mutate business rows. See
|
|
471
|
+
[Guarded Single-Row CRUD Writeback](guarded-crud-writeback.md).
|
|
@@ -89,7 +89,8 @@ worker termination during an open write transaction rolls back and recovers
|
|
|
89
89
|
worker termination after commit recovers as already-applied
|
|
90
90
|
source-down, read-only-ledger, and timeout readiness failures recover without restart
|
|
91
91
|
shared dead letters requeue or discard with receipts and events preserved
|
|
92
|
-
Postgres and MySQL pool queues fail fast at configured bounds
|
|
92
|
+
Postgres and MySQL pool queues fail fast at configured bounds with retry hints
|
|
93
|
+
shared runtime-store batch apply preserves one authoritative bridge and durable receipts
|
|
93
94
|
backup digest and clean restore match
|
|
94
95
|
retention archives before delete and preserves active proposals
|
|
95
96
|
```
|