@synapsor/runner 1.1.1 → 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 +92 -1
- package/README.md +26 -7
- package/THREAT_MODEL.md +7 -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 +8060 -2615
- 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 +29 -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
|
@@ -67,17 +67,35 @@ outside the model-facing MCP server and verifies:
|
|
|
67
67
|
- allowed mutable columns;
|
|
68
68
|
- conflict/version guard;
|
|
69
69
|
- idempotency key;
|
|
70
|
+
- operation-specific version or source-unique deduplication guard;
|
|
70
71
|
- job expiry;
|
|
71
|
-
- exactly one
|
|
72
|
+
- exactly one reviewed row or every member of one bounded frozen set.
|
|
72
73
|
|
|
73
74
|
If any authority check cannot be verified, the write fails closed.
|
|
74
75
|
|
|
75
76
|
For direct SQL writeback, the writer connection is the env var named by the
|
|
76
|
-
source `write_url_env` in `synapsor.runner.json`.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
`
|
|
80
|
-
|
|
77
|
+
source `write_url_env` in `synapsor.runner.json`. With `source_db` receipt
|
|
78
|
+
authority, the source mutation and receipt commit atomically; the receipt table
|
|
79
|
+
can be administrator-precreated or explicitly auto-migrated. With
|
|
80
|
+
`runner_ledger` authority, Runner creates no source receipt table, but a crash
|
|
81
|
+
after source commit and before ledger completion can require verified operator
|
|
82
|
+
reconciliation. It is not distributed exactly-once. See
|
|
83
|
+
[Guarded Single-Row CRUD Writeback](guarded-crud-writeback.md).
|
|
84
|
+
|
|
85
|
+
Bounded-set direct writeback additionally requires a contract-fixed predicate
|
|
86
|
+
or complete reviewed item list, `MAX ROWS` plus aggregate bounds,
|
|
87
|
+
human/operator approval, exact frozen identities and versions, deterministic
|
|
88
|
+
locks, one atomic source transaction, and per-member receipt digests. The
|
|
89
|
+
model cannot supply the predicate or cap. See [Bounded Set
|
|
90
|
+
Writeback](bounded-set-writeback.md).
|
|
91
|
+
|
|
92
|
+
When a direct SQL capability opts into reviewed reversibility, an applied
|
|
93
|
+
receipt may carry a bounded inverse containing only trusted identity, version,
|
|
94
|
+
and reviewed writable values. `revert` is operator-only and creates a new
|
|
95
|
+
proposal; it never mutates or approves. Compensation rechecks the exact forward
|
|
96
|
+
result under the trusted tenant and uses the normal approval, receipt,
|
|
97
|
+
reconciliation, and replay boundaries. See [Reviewed Reversible Change
|
|
98
|
+
Sets](reversible-change-sets.md).
|
|
81
99
|
|
|
82
100
|
When a capability uses an `http_handler` or `command_handler` executor, the
|
|
83
101
|
same approval boundary applies. The runner sends a structured proposal/job
|
|
@@ -96,6 +114,8 @@ approve/reject actions. It does not expose raw SQL, database URLs, write
|
|
|
96
114
|
credentials, approval tools, commit tools, or controls that widen reviewed
|
|
97
115
|
tables/columns.
|
|
98
116
|
|
|
99
|
-
Synapsor Runner supports reviewed single-row
|
|
100
|
-
|
|
101
|
-
|
|
117
|
+
Synapsor Runner supports reviewed single-row CRUD and the narrowly bounded set
|
|
118
|
+
path documented above. It does not support arbitrary SQL, DDL, UPSERT,
|
|
119
|
+
model-generated predicates, unbounded sets, or cross-table direct writeback.
|
|
120
|
+
Hard DELETE fails closed when cascades, write triggers, or required metadata
|
|
121
|
+
visibility prevent a bounded-effect proof.
|
|
@@ -42,19 +42,19 @@ For `synapsor-runner apply --job ... --config ...`, set the env var named by
|
|
|
42
42
|
`write_url_env`. `SYNAPSOR_DATABASE_URL` is only a legacy fallback for direct
|
|
43
43
|
worker flows that do not pass a local config.
|
|
44
44
|
|
|
45
|
-
Direct SQL writeback
|
|
46
|
-
|
|
45
|
+
Direct SQL writeback uses the receipt authority selected under the source
|
|
46
|
+
config. `source_db` can use an administrator-precreated table or explicitly run
|
|
47
|
+
an idempotent migration:
|
|
47
48
|
|
|
48
49
|
```sql
|
|
49
50
|
CREATE TABLE IF NOT EXISTS synapsor_writeback_receipts (...);
|
|
50
51
|
```
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
application owns receipt storage and business writes.
|
|
53
|
+
`precreated` needs only receipt-table DML; `auto_migrate` also needs bounded
|
|
54
|
+
`CREATE`. `runner_ledger` creates no source receipt table and uses a durable
|
|
55
|
+
Runner intent plus operator reconciliation for ambiguous post-commit crashes.
|
|
56
|
+
It does not claim distributed exactly-once. See
|
|
57
|
+
[Guarded Single-Row CRUD Writeback](guarded-crud-writeback.md).
|
|
58
58
|
|
|
59
59
|
Use the helper commands before enabling direct SQL writeback:
|
|
60
60
|
|
|
@@ -65,9 +65,10 @@ npx -y -p @synapsor/runner synapsor-runner writeback grants --engine postgres --
|
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
`writeback doctor --check-db` connects with the configured writer credential and
|
|
68
|
-
checks the
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
checks the selected receipt authority plus operation-specific target guards in
|
|
69
|
+
a rolled-back transaction. In `precreated` mode, apply the printed migration as
|
|
70
|
+
an administrator first; the steady-state writer needs table
|
|
71
|
+
`SELECT`/`INSERT`/`UPDATE`, not schema `CREATE`.
|
|
71
72
|
|
|
72
73
|
## `http_handler`
|
|
73
74
|
|
|
@@ -7,6 +7,39 @@ CREATE TABLE invoices (
|
|
|
7
7
|
updated_at datetime(6) NOT NULL
|
|
8
8
|
);
|
|
9
9
|
|
|
10
|
+
CREATE TABLE guard_crud_items (
|
|
11
|
+
id bigint NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
12
|
+
tenant_id varchar(128) NOT NULL,
|
|
13
|
+
request_id varchar(128) NOT NULL,
|
|
14
|
+
value_cents integer NOT NULL,
|
|
15
|
+
version bigint NOT NULL DEFAULT 1,
|
|
16
|
+
UNIQUE KEY guard_crud_items_tenant_request (tenant_id, request_id)
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
CREATE TABLE synapsor_receipts_precreated (
|
|
20
|
+
idempotency_key varchar(191) PRIMARY KEY,
|
|
21
|
+
job_id varchar(191) UNIQUE NOT NULL,
|
|
22
|
+
proposal_id varchar(191) NOT NULL,
|
|
23
|
+
status varchar(64) NOT NULL,
|
|
24
|
+
result_hash varchar(255),
|
|
25
|
+
created_at datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
|
26
|
+
completed_at datetime(6)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
INSERT INTO guard_crud_items (id, tenant_id, request_id, value_cents, version) VALUES
|
|
30
|
+
(1001, 'acme', 'update-precreated', 100, 1),
|
|
31
|
+
(1002, 'acme', 'delete-precreated', 200, 1),
|
|
32
|
+
(1011, 'acme', 'update-auto', 110, 1),
|
|
33
|
+
(1012, 'acme', 'delete-auto', 210, 1),
|
|
34
|
+
(1021, 'acme', 'update-ledger', 120, 1),
|
|
35
|
+
(1022, 'acme', 'delete-ledger', 220, 1),
|
|
36
|
+
(1031, 'globex', 'wrong-tenant', 230, 1),
|
|
37
|
+
(1041, 'acme', 'stale-version', 240, 2),
|
|
38
|
+
(1051, 'acme', 'crash-after-commit', 250, 1),
|
|
39
|
+
(1052, 'acme', 'crash-before-commit', 260, 1),
|
|
40
|
+
(1061, 'acme', 'concurrent-apply', 270, 1);
|
|
41
|
+
ALTER TABLE guard_crud_items AUTO_INCREMENT = 2001;
|
|
42
|
+
|
|
10
43
|
INSERT INTO invoices (id, tenant_id, status, late_fee_cents, waiver_reason, updated_at) VALUES
|
|
11
44
|
('MYSQL-ACME', 'acme', 'overdue', 700, NULL, '2026-07-12 12:00:00.000000');
|
|
12
45
|
|
|
@@ -15,6 +48,20 @@ SELECT invoices.*, SLEEP(0.35) AS synthetic_delay
|
|
|
15
48
|
FROM invoices;
|
|
16
49
|
|
|
17
50
|
CREATE USER IF NOT EXISTS 'synapsor_reader'@'%' IDENTIFIED BY 'synapsor_reader_password';
|
|
51
|
+
CREATE USER IF NOT EXISTS 'synapsor_crud_precreated'@'%' IDENTIFIED BY 'synapsor_crud_precreated_password';
|
|
52
|
+
CREATE USER IF NOT EXISTS 'synapsor_crud_auto'@'%' IDENTIFIED BY 'synapsor_crud_auto_password';
|
|
53
|
+
CREATE USER IF NOT EXISTS 'synapsor_crud_ledger'@'%' IDENTIFIED BY 'synapsor_crud_ledger_password';
|
|
18
54
|
GRANT SELECT ON synapsor_fleet.invoices TO 'synapsor_reader'@'%';
|
|
19
55
|
GRANT SELECT ON synapsor_fleet.slow_invoices TO 'synapsor_reader'@'%';
|
|
56
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON synapsor_fleet.guard_crud_items TO 'synapsor_crud_precreated'@'%';
|
|
57
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON synapsor_fleet.guard_crud_items TO 'synapsor_crud_auto'@'%';
|
|
58
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON synapsor_fleet.guard_crud_items TO 'synapsor_crud_ledger'@'%';
|
|
59
|
+
GRANT TRIGGER ON synapsor_fleet.guard_crud_items TO 'synapsor_crud_precreated'@'%';
|
|
60
|
+
GRANT TRIGGER ON synapsor_fleet.guard_crud_items TO 'synapsor_crud_auto'@'%';
|
|
61
|
+
GRANT TRIGGER ON synapsor_fleet.guard_crud_items TO 'synapsor_crud_ledger'@'%';
|
|
62
|
+
GRANT PROCESS ON *.* TO 'synapsor_crud_precreated'@'%';
|
|
63
|
+
GRANT PROCESS ON *.* TO 'synapsor_crud_auto'@'%';
|
|
64
|
+
GRANT PROCESS ON *.* TO 'synapsor_crud_ledger'@'%';
|
|
65
|
+
GRANT SELECT, INSERT, UPDATE ON synapsor_fleet.synapsor_receipts_precreated TO 'synapsor_crud_precreated'@'%';
|
|
66
|
+
GRANT SELECT, INSERT, UPDATE, CREATE ON synapsor_fleet.* TO 'synapsor_crud_auto'@'%';
|
|
20
67
|
FLUSH PRIVILEGES;
|
|
@@ -25,10 +25,47 @@ CREATE TABLE public.synthetic_handler_receipts (
|
|
|
25
25
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
26
26
|
);
|
|
27
27
|
|
|
28
|
+
CREATE TABLE public.guard_crud_items (
|
|
29
|
+
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
30
|
+
tenant_id text NOT NULL,
|
|
31
|
+
request_id text NOT NULL,
|
|
32
|
+
value_cents integer NOT NULL,
|
|
33
|
+
version bigint NOT NULL DEFAULT 1,
|
|
34
|
+
UNIQUE (tenant_id, request_id)
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
CREATE SCHEMA synapsor_precreated;
|
|
38
|
+
CREATE TABLE synapsor_precreated.receipts (
|
|
39
|
+
idempotency_key text PRIMARY KEY,
|
|
40
|
+
job_id text UNIQUE NOT NULL,
|
|
41
|
+
proposal_id text NOT NULL,
|
|
42
|
+
status text NOT NULL,
|
|
43
|
+
result_hash text,
|
|
44
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
45
|
+
completed_at timestamptz
|
|
46
|
+
);
|
|
47
|
+
CREATE SCHEMA synapsor_auto;
|
|
48
|
+
|
|
49
|
+
INSERT INTO public.guard_crud_items (id, tenant_id, request_id, value_cents, version) VALUES
|
|
50
|
+
(1001, 'acme', 'update-precreated', 100, 1),
|
|
51
|
+
(1002, 'acme', 'delete-precreated', 200, 1),
|
|
52
|
+
(1011, 'acme', 'update-auto', 110, 1),
|
|
53
|
+
(1012, 'acme', 'delete-auto', 210, 1),
|
|
54
|
+
(1021, 'acme', 'update-ledger', 120, 1),
|
|
55
|
+
(1022, 'acme', 'delete-ledger', 220, 1),
|
|
56
|
+
(1031, 'globex', 'wrong-tenant', 230, 1),
|
|
57
|
+
(1041, 'acme', 'stale-version', 240, 2),
|
|
58
|
+
(1051, 'acme', 'crash-after-commit', 250, 1),
|
|
59
|
+
(1052, 'acme', 'crash-before-commit', 260, 1),
|
|
60
|
+
(1061, 'acme', 'concurrent-apply', 270, 1);
|
|
61
|
+
SELECT setval(pg_get_serial_sequence('public.guard_crud_items', 'id'), 2000, true);
|
|
62
|
+
|
|
28
63
|
INSERT INTO public.invoices (id, tenant_id, status, late_fee_cents, waiver_reason, updated_at) VALUES
|
|
29
64
|
('INV-ACME', 'acme', 'overdue', 2500, NULL, '2026-07-12T12:00:00Z'),
|
|
30
65
|
('INV-GLOBEX', 'globex', 'overdue', 3100, NULL, '2026-07-12T12:00:00Z'),
|
|
31
66
|
('INV-RATECO', 'rateco', 'overdue', 900, NULL, '2026-07-12T12:00:00Z'),
|
|
67
|
+
('INV-BATCH-A', 'acme', 'overdue', 1100, NULL, '2026-07-12T12:00:00Z'),
|
|
68
|
+
('INV-BATCH-B', 'acme', 'overdue', 1300, NULL, '2026-07-12T12:00:00Z'),
|
|
32
69
|
('INV-QUORUM-RACE', 'acme', 'overdue', 1400, NULL, '2026-07-12T12:00:00Z'),
|
|
33
70
|
('INV-WORKER-RACE', 'acme', 'overdue', 1600, NULL, '2026-07-12T12:00:00Z'),
|
|
34
71
|
('INV-DEAD-REQUEUE', 'acme', 'overdue', 1700, NULL, '2026-07-12T12:00:00Z'),
|
|
@@ -53,6 +90,9 @@ DO $$
|
|
|
53
90
|
BEGIN
|
|
54
91
|
CREATE ROLE synapsor_reader LOGIN PASSWORD 'synapsor_reader_password';
|
|
55
92
|
CREATE ROLE synapsor_writer LOGIN PASSWORD 'synapsor_writer_password';
|
|
93
|
+
CREATE ROLE synapsor_crud_precreated LOGIN PASSWORD 'synapsor_crud_precreated_password';
|
|
94
|
+
CREATE ROLE synapsor_crud_auto LOGIN PASSWORD 'synapsor_crud_auto_password';
|
|
95
|
+
CREATE ROLE synapsor_crud_ledger LOGIN PASSWORD 'synapsor_crud_ledger_password';
|
|
56
96
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
57
97
|
END
|
|
58
98
|
$$;
|
|
@@ -64,6 +104,14 @@ GRANT SELECT ON public.slow_invoices TO synapsor_reader;
|
|
|
64
104
|
GRANT UPDATE (late_fee_cents, waiver_reason, updated_at) ON public.invoices TO synapsor_writer;
|
|
65
105
|
GRANT SELECT, INSERT, UPDATE ON public.synapsor_writeback_receipts TO synapsor_writer;
|
|
66
106
|
|
|
107
|
+
GRANT CONNECT ON DATABASE synapsor_fleet TO synapsor_crud_precreated, synapsor_crud_auto, synapsor_crud_ledger;
|
|
108
|
+
GRANT USAGE ON SCHEMA public TO synapsor_crud_precreated, synapsor_crud_auto, synapsor_crud_ledger;
|
|
109
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.guard_crud_items TO synapsor_crud_precreated, synapsor_crud_auto, synapsor_crud_ledger;
|
|
110
|
+
GRANT USAGE, SELECT ON SEQUENCE public.guard_crud_items_id_seq TO synapsor_crud_precreated, synapsor_crud_auto, synapsor_crud_ledger;
|
|
111
|
+
GRANT USAGE ON SCHEMA synapsor_precreated TO synapsor_crud_precreated;
|
|
112
|
+
GRANT SELECT, INSERT, UPDATE ON synapsor_precreated.receipts TO synapsor_crud_precreated;
|
|
113
|
+
GRANT USAGE, CREATE ON SCHEMA synapsor_auto TO synapsor_crud_auto;
|
|
114
|
+
|
|
67
115
|
-- Docker's Postgres entrypoint briefly starts a temporary postmaster while it
|
|
68
116
|
-- runs init scripts. The Compose health check compares this timestamp with the
|
|
69
117
|
-- current postmaster start time so dependents wait for the final server.
|
|
@@ -6,6 +6,12 @@ This example shows three enforcement tiers on one support action:
|
|
|
6
6
|
- `$100` (`10000` cents): saved as `pending_review` until a local operator approves.
|
|
7
7
|
- `$1000` (`100000` cents): rejected before proposal creation by the contract bound.
|
|
8
8
|
|
|
9
|
+
It also shows native guarded single-row INSERT through
|
|
10
|
+
`support.propose_plan_credit_record`. Runner supplies the trusted tenant and a
|
|
11
|
+
proposal-derived request ID; the database enforces `(tenant_id, request_id)` as
|
|
12
|
+
a unique deduplication key. The existing UPDATE scenario above remains the
|
|
13
|
+
policy-tier demonstration.
|
|
14
|
+
|
|
9
15
|
## Prerequisites
|
|
10
16
|
|
|
11
17
|
- Docker
|
|
@@ -132,6 +138,30 @@ synapsor-runner apply latest --config examples/support-plan-credit/synapsor.runn
|
|
|
132
138
|
The live smoke test mutates `updated_at` before apply and proves stale proposals
|
|
133
139
|
return `VERSION_CONFLICT` without changing the source row.
|
|
134
140
|
|
|
141
|
+
Create one reviewed credit record with native INSERT:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
synapsor-runner propose support.propose_plan_credit_record \
|
|
145
|
+
--config examples/support-plan-credit/synapsor.runner.json \
|
|
146
|
+
--store ./tmp/support-plan-credit/local.db \
|
|
147
|
+
--json '{"customer_id":"CUS-3001","credit_cents":2500,"reason":"SLA outage ticket SUP-481"}'
|
|
148
|
+
|
|
149
|
+
synapsor-runner proposals approve latest --yes \
|
|
150
|
+
--config examples/support-plan-credit/synapsor.runner.json \
|
|
151
|
+
--store ./tmp/support-plan-credit/local.db
|
|
152
|
+
synapsor-runner apply latest \
|
|
153
|
+
--config examples/support-plan-credit/synapsor.runner.json \
|
|
154
|
+
--store ./tmp/support-plan-credit/local.db
|
|
155
|
+
synapsor-runner replay show latest --store ./tmp/support-plan-credit/local.db
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The insert is exactly one row. The model cannot set `tenant_id`, `request_id`,
|
|
159
|
+
table names, or columns. Retry is classified from the atomic source receipt and
|
|
160
|
+
the source-unique deduplication key. Use an
|
|
161
|
+
[app-owned executor](../../docs/writeback-executors.md) for multi-table writes,
|
|
162
|
+
external effects, UPSERT, or business actions that cannot fit this one-row
|
|
163
|
+
boundary.
|
|
164
|
+
|
|
135
165
|
Run the complete Docker-backed scenario, including all three policy tiers and
|
|
136
166
|
the stale-row conflict:
|
|
137
167
|
|
|
@@ -153,6 +183,7 @@ The contract keeps these fields out:
|
|
|
153
183
|
```text
|
|
154
184
|
support.inspect_customer
|
|
155
185
|
support.propose_plan_credit
|
|
186
|
+
support.propose_plan_credit_record
|
|
156
187
|
auto-approval: enabled
|
|
157
188
|
```
|
|
158
189
|
|
|
@@ -51,10 +51,36 @@ CREATE CAPABILITY support.propose_plan_credit
|
|
|
51
51
|
WRITEBACK DIRECT SQL
|
|
52
52
|
END
|
|
53
53
|
|
|
54
|
+
CREATE CAPABILITY support.propose_plan_credit_record
|
|
55
|
+
DESCRIPTION 'Propose creating one bounded plan-credit record with source-enforced deduplication.'
|
|
56
|
+
RETURNS HINT 'Returns a proposal id for one new account_credits row; source database unchanged until human approval and guarded apply.'
|
|
57
|
+
USING CONTEXT support_agent_context
|
|
58
|
+
SOURCE local_postgres
|
|
59
|
+
ON public.account_credits
|
|
60
|
+
PRIMARY KEY id
|
|
61
|
+
TENANT KEY tenant_id
|
|
62
|
+
ARG customer_id STRING REQUIRED MAX LENGTH 128 DESCRIPTION 'Customer id such as CUS-3001.'
|
|
63
|
+
ARG credit_cents NUMBER REQUIRED MIN 1 MAX 50000 DESCRIPTION 'Requested plan credit in cents.'
|
|
64
|
+
ARG reason TEXT REQUIRED MAX LENGTH 500 DESCRIPTION 'Business reason tied to support evidence.'
|
|
65
|
+
ALLOW READ id, tenant_id, request_id, customer_id, amount_cents, reason, created_at
|
|
66
|
+
REQUIRE EVIDENCE
|
|
67
|
+
MAX ROWS 1
|
|
68
|
+
PROPOSE ACTION create_plan_credit INSERT
|
|
69
|
+
DEDUP KEY tenant_id = TRUSTED TENANT, request_id = PROPOSAL ID
|
|
70
|
+
ALLOW WRITE customer_id, amount_cents, reason
|
|
71
|
+
PATCH customer_id = ARG customer_id
|
|
72
|
+
PATCH amount_cents = ARG credit_cents
|
|
73
|
+
PATCH reason = ARG reason
|
|
74
|
+
BOUND amount_cents 1..50000
|
|
75
|
+
APPROVAL ROLE support_reviewer
|
|
76
|
+
WRITEBACK DIRECT SQL
|
|
77
|
+
END
|
|
78
|
+
|
|
54
79
|
CREATE AGENT WORKFLOW support.plan_credit_request
|
|
55
80
|
USING CONTEXT support_agent_context
|
|
56
81
|
ALLOW CAPABILITY support.inspect_customer
|
|
57
82
|
ALLOW CAPABILITY support.propose_plan_credit
|
|
83
|
+
ALLOW CAPABILITY support.propose_plan_credit_record
|
|
58
84
|
REQUIRE EVIDENCE
|
|
59
85
|
APPROVAL REQUIRED ROLE support_reviewer
|
|
60
86
|
CHECKPOINT PROPOSAL ONLY
|
|
@@ -24,6 +24,17 @@ CREATE TABLE IF NOT EXISTS public.synapsor_writeback_receipts (
|
|
|
24
24
|
completed_at timestamptz
|
|
25
25
|
);
|
|
26
26
|
|
|
27
|
+
CREATE TABLE IF NOT EXISTS public.account_credits (
|
|
28
|
+
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
29
|
+
tenant_id text NOT NULL,
|
|
30
|
+
request_id text NOT NULL,
|
|
31
|
+
customer_id text NOT NULL,
|
|
32
|
+
amount_cents integer NOT NULL CHECK (amount_cents BETWEEN 1 AND 50000),
|
|
33
|
+
reason text NOT NULL,
|
|
34
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
35
|
+
CONSTRAINT account_credits_tenant_request_unique UNIQUE (tenant_id, request_id)
|
|
36
|
+
);
|
|
37
|
+
|
|
27
38
|
INSERT INTO public.customers (
|
|
28
39
|
id,
|
|
29
40
|
tenant_id,
|
|
@@ -97,6 +108,8 @@ $$;
|
|
|
97
108
|
|
|
98
109
|
GRANT CONNECT ON DATABASE synapsor_runner_plan_credit TO synapsor_reader, synapsor_writer;
|
|
99
110
|
GRANT USAGE ON SCHEMA public TO synapsor_reader, synapsor_writer;
|
|
100
|
-
GRANT SELECT ON public.customers TO synapsor_reader, synapsor_writer;
|
|
111
|
+
GRANT SELECT ON public.customers, public.account_credits TO synapsor_reader, synapsor_writer;
|
|
101
112
|
GRANT UPDATE (plan_credit_cents, credit_reason, updated_at) ON public.customers TO synapsor_writer;
|
|
113
|
+
GRANT INSERT (tenant_id, request_id, customer_id, amount_cents, reason) ON public.account_credits TO synapsor_writer;
|
|
114
|
+
GRANT USAGE, SELECT ON SEQUENCE public.account_credits_id_seq TO synapsor_writer;
|
|
102
115
|
GRANT SELECT, INSERT, UPDATE ON public.synapsor_writeback_receipts TO synapsor_writer;
|
|
@@ -139,6 +139,105 @@
|
|
|
139
139
|
"credit_reason",
|
|
140
140
|
"updated_at"
|
|
141
141
|
]
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"args": {
|
|
145
|
+
"credit_cents": {
|
|
146
|
+
"description": "Requested plan credit in cents.",
|
|
147
|
+
"maximum": 50000,
|
|
148
|
+
"minimum": 1,
|
|
149
|
+
"required": true,
|
|
150
|
+
"type": "number"
|
|
151
|
+
},
|
|
152
|
+
"customer_id": {
|
|
153
|
+
"description": "Customer id such as CUS-3001.",
|
|
154
|
+
"max_length": 128,
|
|
155
|
+
"required": true,
|
|
156
|
+
"type": "string"
|
|
157
|
+
},
|
|
158
|
+
"reason": {
|
|
159
|
+
"description": "Business reason tied to support evidence.",
|
|
160
|
+
"max_length": 500,
|
|
161
|
+
"required": true,
|
|
162
|
+
"type": "string"
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
"context": "support_agent_context",
|
|
166
|
+
"description": "Propose creating one bounded plan-credit record with source-enforced deduplication.",
|
|
167
|
+
"evidence": {
|
|
168
|
+
"query_audit": true,
|
|
169
|
+
"required": true
|
|
170
|
+
},
|
|
171
|
+
"kind": "proposal",
|
|
172
|
+
"max_rows": 1,
|
|
173
|
+
"name": "support.propose_plan_credit_record",
|
|
174
|
+
"proposal": {
|
|
175
|
+
"action": "create_plan_credit",
|
|
176
|
+
"allowed_fields": [
|
|
177
|
+
"customer_id",
|
|
178
|
+
"amount_cents",
|
|
179
|
+
"reason"
|
|
180
|
+
],
|
|
181
|
+
"approval": {
|
|
182
|
+
"mode": "human",
|
|
183
|
+
"required_role": "support_reviewer"
|
|
184
|
+
},
|
|
185
|
+
"conflict_guard": {
|
|
186
|
+
"weak_guard_ack": true
|
|
187
|
+
},
|
|
188
|
+
"numeric_bounds": {
|
|
189
|
+
"amount_cents": {
|
|
190
|
+
"maximum": 50000,
|
|
191
|
+
"minimum": 1
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
"operation": {
|
|
195
|
+
"deduplication": {
|
|
196
|
+
"components": [
|
|
197
|
+
{
|
|
198
|
+
"column": "tenant_id",
|
|
199
|
+
"source": "trusted_tenant"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"column": "request_id",
|
|
203
|
+
"source": "proposal_id"
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
},
|
|
207
|
+
"kind": "insert"
|
|
208
|
+
},
|
|
209
|
+
"patch": {
|
|
210
|
+
"amount_cents": {
|
|
211
|
+
"from_arg": "credit_cents"
|
|
212
|
+
},
|
|
213
|
+
"customer_id": {
|
|
214
|
+
"from_arg": "customer_id"
|
|
215
|
+
},
|
|
216
|
+
"reason": {
|
|
217
|
+
"from_arg": "reason"
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
"writeback": {
|
|
221
|
+
"mode": "direct_sql"
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
"returns_hint": "Returns a proposal id for one new account_credits row; source database unchanged until human approval and guarded apply.",
|
|
225
|
+
"source": "local_postgres",
|
|
226
|
+
"subject": {
|
|
227
|
+
"primary_key": "id",
|
|
228
|
+
"schema": "public",
|
|
229
|
+
"table": "account_credits",
|
|
230
|
+
"tenant_key": "tenant_id"
|
|
231
|
+
},
|
|
232
|
+
"visible_fields": [
|
|
233
|
+
"id",
|
|
234
|
+
"tenant_id",
|
|
235
|
+
"request_id",
|
|
236
|
+
"customer_id",
|
|
237
|
+
"amount_cents",
|
|
238
|
+
"reason",
|
|
239
|
+
"created_at"
|
|
240
|
+
]
|
|
142
241
|
}
|
|
143
242
|
],
|
|
144
243
|
"contexts": [
|
|
@@ -196,7 +295,8 @@
|
|
|
196
295
|
{
|
|
197
296
|
"allowed_capabilities": [
|
|
198
297
|
"support.inspect_customer",
|
|
199
|
-
"support.propose_plan_credit"
|
|
298
|
+
"support.propose_plan_credit",
|
|
299
|
+
"support.propose_plan_credit_record"
|
|
200
300
|
],
|
|
201
301
|
"approval": {
|
|
202
302
|
"required": true,
|
|
@@ -12,7 +12,13 @@
|
|
|
12
12
|
"engine": "postgres",
|
|
13
13
|
"read_url_env": "PLAN_CREDIT_POSTGRES_READ_URL",
|
|
14
14
|
"write_url_env": "PLAN_CREDIT_POSTGRES_WRITE_URL",
|
|
15
|
-
"statement_timeout_ms": 3000
|
|
15
|
+
"statement_timeout_ms": 3000,
|
|
16
|
+
"receipts": {
|
|
17
|
+
"authority": "source_db",
|
|
18
|
+
"provisioning": "precreated",
|
|
19
|
+
"schema": "public",
|
|
20
|
+
"table": "synapsor_writeback_receipts"
|
|
21
|
+
}
|
|
16
22
|
}
|
|
17
23
|
}
|
|
18
24
|
}
|