@synapsor/runner 1.1.1 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +123 -1
- package/README.md +52 -31
- package/SECURITY.md +11 -0
- 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 +8103 -2601
- package/docs/README.md +23 -3
- package/docs/benchmarks/bounded-set-local.md +27 -0
- package/docs/bounded-set-writeback.md +298 -0
- package/docs/capability-authoring.md +26 -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 +105 -2
- package/docs/release-policy.md +1 -1
- package/docs/result-envelope-v2.md +20 -6
- package/docs/result-envelope-v3.md +43 -0
- 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/why-synapsor-vs-app-guardrails.md +169 -0
- 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 +2 -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,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.
|
|
@@ -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
|
}
|