@synapsor/runner 1.4.122 → 1.5.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +71 -19
  2. package/README.md +56 -46
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/local-ui.d.ts.map +1 -1
  5. package/dist/runner.mjs +14531 -8137
  6. package/dist/runtime.d.ts +50 -0
  7. package/dist/runtime.d.ts.map +1 -0
  8. package/dist/runtime.mjs +15066 -0
  9. package/docs/README.md +19 -3
  10. package/docs/capability-authoring.md +17 -1
  11. package/docs/contract-review.md +33 -0
  12. package/docs/database-enforced-scope.md +273 -0
  13. package/docs/dependency-license-inventory.md +6 -2
  14. package/docs/doctor.md +17 -0
  15. package/docs/effect-regression.md +142 -0
  16. package/docs/getting-started-own-database.md +7 -0
  17. package/docs/local-mode.md +7 -3
  18. package/docs/mcp-apps.md +188 -0
  19. package/docs/mcp-audit.md +54 -0
  20. package/docs/mcp-client-setup.md +16 -4
  21. package/docs/mcp-clients.md +8 -0
  22. package/docs/production.md +8 -0
  23. package/docs/release-notes.md +67 -11
  24. package/docs/release-policy.md +9 -5
  25. package/docs/runner-config-reference.md +25 -0
  26. package/docs/schema-api-candidates.md +68 -0
  27. package/docs/security-boundary.md +14 -1
  28. package/docs/shadow-studies.md +198 -0
  29. package/examples/mcp-postgres-billing-app-handler/app-handler.mjs +9 -1
  30. package/examples/mcp-postgres-billing-app-handler/schema.sql +11 -0
  31. package/examples/mcp-postgres-billing-app-handler/synapsor-handler.mjs +3 -0
  32. package/examples/raw-sql-vs-synapsor/Makefile +2 -2
  33. package/examples/raw-sql-vs-synapsor/README.md +6 -4
  34. package/examples/support-billing-agent/Makefile +5 -1
  35. package/examples/support-billing-agent/README.md +39 -5
  36. package/examples/support-billing-agent/db/schema.sql +88 -1
  37. package/examples/support-billing-agent/db/seed.sql +15 -15
  38. package/examples/support-billing-agent/scripts/run-demo.sh +4 -0
  39. package/examples/support-billing-agent/scripts/run-evaluation.sh +62 -0
  40. package/examples/support-billing-agent/shadow-study/cases.jsonl +6 -0
  41. package/examples/support-billing-agent/shadow-study/outcomes.jsonl +2 -0
  42. package/examples/support-billing-agent/synapsor.runner.json +17 -5
  43. package/fixtures/contracts/capability-surface-fitness.contract.json +190 -0
  44. package/fixtures/effects/changed/eff_support_late_fee_v1.result.json +58 -0
  45. package/fixtures/effects/dataset.json +8 -0
  46. package/fixtures/effects/results/eff_support_late_fee_v1.result.json +59 -0
  47. package/fixtures/effects/support-late-fee.effect.json +71 -0
  48. package/fixtures/generators/drizzle/malicious.ts +11 -0
  49. package/fixtures/generators/drizzle/schema.ts +12 -0
  50. package/fixtures/generators/openapi/external-ref.yaml +15 -0
  51. package/fixtures/generators/openapi/openapi.yaml +81 -0
  52. package/fixtures/generators/prisma/schema.prisma +34 -0
  53. package/package.json +13 -2
  54. package/schemas/effect-dataset.schema.json +19 -0
  55. package/schemas/effect-fixture.schema.json +191 -0
  56. package/schemas/effect-result.schema.json +52 -0
  57. package/schemas/mcp-audit-candidates.schema.json +41 -0
  58. package/schemas/mcp-audit-report.schema.json +104 -0
  59. package/schemas/schema-candidate-review.schema.json +167 -0
  60. package/schemas/schema-candidates.schema.json +49 -0
  61. package/schemas/synapsor.runner.schema.json +51 -0
@@ -0,0 +1,198 @@
1
+ # Shadow Studies
2
+
3
+ Use a shadow study before granting an agent write authority. Runner serves the
4
+ reviewed proposal capability and records what the agent would have proposed,
5
+ but the proposal cannot be approved, queued, or applied.
6
+
7
+ A study compares two explicit records:
8
+
9
+ ```text
10
+ agent result or proposed effect
11
+ versus
12
+ authoritative outcome recorded by your application or operator
13
+ ```
14
+
15
+ Runner does not infer a human rejection from an unchanged database row. A case
16
+ without an authoritative outcome remains visibly unmatched.
17
+
18
+ ## Safety Boundary
19
+
20
+ Set the Runner configuration to:
21
+
22
+ ```json
23
+ {
24
+ "version": 1,
25
+ "mode": "shadow"
26
+ }
27
+ ```
28
+
29
+ The proposal store enforces the shadow boundary below the CLI and local UI:
30
+
31
+ - a shadow proposal cannot be approved;
32
+ - it cannot become a writeback job or worker item;
33
+ - it cannot invoke direct database writeback or an app-owned handler;
34
+ - no flag bypasses these checks;
35
+ - a report suggestion is inactive and never edits or activates a contract.
36
+
37
+ Shadow data remains in the configured local Runner store. Case and outcome
38
+ imports reject obvious credentials, bearer tokens, database URLs, and
39
+ secret-like fields.
40
+
41
+ ## Start A Study
42
+
43
+ Create a study before serving the shadow capability. An empty capability list
44
+ means all shadow capabilities; repeat `--capability` or use comma-separated
45
+ names to narrow it.
46
+
47
+ ```bash
48
+ synapsor-runner shadow study create \
49
+ --id sst_support_pilot \
50
+ --name "Support waiver pilot" \
51
+ --capability billing.propose_late_fee_waiver \
52
+ --store ./.synapsor/local.db
53
+
54
+ synapsor-runner mcp serve \
55
+ --config ./synapsor.runner.json \
56
+ --store ./.synapsor/local.db
57
+ ```
58
+
59
+ New matching shadow proposals are attached automatically. To attach proposals
60
+ that already exist, or to repair correlation after an interrupted process:
61
+
62
+ ```bash
63
+ synapsor-runner shadow study sync sst_support_pilot \
64
+ --store ./.synapsor/local.db
65
+ ```
66
+
67
+ ## Record Non-Proposal Results
68
+
69
+ A study also needs tasks where the agent was unable or not allowed to propose.
70
+ Import one JSON object with `case record`, or a JSON array/JSONL file with
71
+ `case import`.
72
+
73
+ ```json
74
+ {
75
+ "request_id": "req-policy-42",
76
+ "tenant_id": "acme",
77
+ "principal": "support-agent-shadow",
78
+ "capability": "billing.propose_late_fee_waiver",
79
+ "business_object": "invoice",
80
+ "object_id": "INV-3042",
81
+ "agent_result": "policy_denied",
82
+ "decision_reason": "amount above reviewed bound",
83
+ "risk_score": 35
84
+ }
85
+ ```
86
+
87
+ Allowed `agent_result` values are:
88
+
89
+ - `proposed`
90
+ - `policy_denied`
91
+ - `unable_to_propose`
92
+ - `stale_conflict`
93
+ - `invalid_unsafe_scope_attempt`
94
+
95
+ For `proposed`, include normalized `before`, `after`, and `patch` objects under
96
+ `proposed_effect`.
97
+
98
+ ```bash
99
+ synapsor-runner shadow case import \
100
+ --study sst_support_pilot \
101
+ --input ./cases.jsonl \
102
+ --store ./.synapsor/local.db
103
+ ```
104
+
105
+ Imports are limited to 2 MiB and 10,000 records per command.
106
+
107
+ ## Record Authoritative Outcomes
108
+
109
+ Export actual outcomes from your application audit log or have a trusted
110
+ operator create them. Each outcome must match an existing case by study,
111
+ request, trusted tenant, business object, and object ID. A supplied proposal ID
112
+ must match too.
113
+
114
+ ```json
115
+ {
116
+ "request_id": "request_123",
117
+ "proposal_id": "wrp_123",
118
+ "tenant_id": "acme",
119
+ "business_object": "invoice",
120
+ "object_id": "INV-3001",
121
+ "actor": "support_lead_1",
122
+ "disposition": "applied",
123
+ "actual_effect": {
124
+ "before": { "late_fee_cents": 5500 },
125
+ "after": { "late_fee_cents": 0 },
126
+ "patch": { "late_fee_cents": 0 }
127
+ },
128
+ "occurred_at": "2026-07-19T12:00:00.000Z",
129
+ "source": "support-system-audit",
130
+ "reference": "ticket:SUP-184",
131
+ "reason": "customer qualified"
132
+ }
133
+ ```
134
+
135
+ Allowed dispositions are `applied`, `rejected_no_action`, and
136
+ `stale_conflict`. An applied outcome requires an exact normalized effect.
137
+
138
+ ```bash
139
+ synapsor-runner shadow outcome import \
140
+ --study sst_support_pilot \
141
+ --input ./outcomes.jsonl \
142
+ --store ./.synapsor/local.db
143
+ ```
144
+
145
+ ## Review The Report
146
+
147
+ ```bash
148
+ synapsor-runner shadow report \
149
+ --study sst_support_pilot \
150
+ --output ./shadow-report.json \
151
+ --store ./.synapsor/local.db
152
+
153
+ synapsor-runner ui \
154
+ --config ./synapsor.runner.json \
155
+ --store ./.synapsor/local.db
156
+ ```
157
+
158
+ The stable JSON report preserves total and comparable denominators and
159
+ classifies exact, partial, disagreement, human rejection/no action, policy
160
+ denial, unable-to-propose, stale/conflict, unmatched, and unsafe-scope cases.
161
+ It also includes amount distributions, capability/reason breakdowns, and
162
+ risk-ranked disagreements.
163
+
164
+ Close a completed study:
165
+
166
+ ```bash
167
+ synapsor-runner shadow study close sst_support_pilot \
168
+ --store ./.synapsor/local.db
169
+ ```
170
+
171
+ ## Deterministic Reference Data
172
+
173
+ The packaged `examples/support-billing-agent/shadow-study/` directory contains
174
+ six synthetic cases and two authoritative outcomes. It covers an exact $55
175
+ waiver, human rejection, policy denial, stale state, tenant forgery, and an
176
+ unmatched task.
177
+
178
+ ```bash
179
+ synapsor-runner shadow study create \
180
+ --id sst_support_reference \
181
+ --name "Support reference study" \
182
+ --capability billing.propose_late_fee_waiver \
183
+ --store ./.synapsor/shadow-reference.db
184
+
185
+ synapsor-runner shadow case import \
186
+ --study sst_support_reference \
187
+ --input ./examples/support-billing-agent/shadow-study/cases.jsonl \
188
+ --store ./.synapsor/shadow-reference.db
189
+
190
+ synapsor-runner shadow outcome import \
191
+ --study sst_support_reference \
192
+ --input ./examples/support-billing-agent/shadow-study/outcomes.jsonl \
193
+ --store ./.synapsor/shadow-reference.db
194
+
195
+ synapsor-runner shadow report \
196
+ --study sst_support_reference \
197
+ --store ./.synapsor/shadow-reference.db
198
+ ```
@@ -119,5 +119,13 @@ function writeJson(response, statusCode, body) {
119
119
  }
120
120
 
121
121
  async function loadHandlerHelper() {
122
- return await import(new URL("./synapsor-handler.mjs", import.meta.url));
122
+ const bundledHelper = new URL("./synapsor-handler.mjs", import.meta.url);
123
+ try {
124
+ return await import(bundledHelper);
125
+ } catch (error) {
126
+ if (error?.code !== "ERR_MODULE_NOT_FOUND" || error?.url !== bundledHelper.href) {
127
+ throw error;
128
+ }
129
+ return await import(new URL("../../packages/handler/dist/index.js", import.meta.url));
130
+ }
123
131
  }
@@ -50,6 +50,16 @@ CREATE TABLE IF NOT EXISTS public.synapsor_writeback_receipts (
50
50
  completed_at timestamptz
51
51
  );
52
52
 
53
+ CREATE TABLE IF NOT EXISTS public.synapsor_handler_receipts (
54
+ idempotency_key text PRIMARY KEY,
55
+ proposal_id text NOT NULL,
56
+ action text NOT NULL,
57
+ status text NOT NULL,
58
+ receipt_json jsonb NOT NULL,
59
+ created_at timestamptz NOT NULL DEFAULT now(),
60
+ completed_at timestamptz
61
+ );
62
+
53
63
  DO $$
54
64
  BEGIN
55
65
  IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'synapsor_reader') THEN
@@ -65,5 +75,6 @@ GRANT CONNECT ON DATABASE synapsor_billing_app_handler TO synapsor_reader, synap
65
75
  GRANT USAGE ON SCHEMA public TO synapsor_reader, synapsor_writer;
66
76
  GRANT SELECT ON public.tenants, public.customers, public.invoices, public.account_credits TO synapsor_reader, synapsor_writer;
67
77
  GRANT SELECT, INSERT, UPDATE ON public.synapsor_writeback_receipts TO synapsor_writer;
78
+ GRANT SELECT, INSERT, UPDATE ON public.synapsor_handler_receipts TO synapsor_writer;
68
79
  GRANT UPDATE (late_fee_cents, waiver_reason, credit_requested_cents, credit_reason, credited_cents, updated_at) ON public.invoices TO synapsor_writer;
69
80
  GRANT INSERT ON public.account_credits TO synapsor_writer;
@@ -168,6 +168,9 @@ class PostgresWritebackHandlerTransaction {
168
168
  this.receiptTable = receiptTable;
169
169
  }
170
170
  async ensureReceiptTable() {
171
+ const existing = await this.client.query("SELECT to_regclass($1) AS receipt_table", [this.receiptTableName()]);
172
+ if (existing.rows[0]?.receipt_table)
173
+ return;
171
174
  await this.client.query(`
172
175
  CREATE TABLE IF NOT EXISTS ${this.receiptTableName()} (
173
176
  idempotency_key text PRIMARY KEY,
@@ -4,8 +4,8 @@ demo:
4
4
  @printf '%s\n' "Unsafe shortcut:"
5
5
  @printf '%s\n' ' agent -> execute_sql("UPDATE invoices SET late_fee_cents = 0 ...")'
6
6
  @printf '%s\n' ""
7
- @printf '%s\n' "Synapsor quick proof:"
8
- npx -y @synapsor/runner demo --quick --no-interactive
7
+ @printf '%s\n' "Synapsor guarded-action proof:"
8
+ npx -y @synapsor/runner try --prove --yes --no-open
9
9
  @printf '%s\n' ""
10
10
  @printf '%s\n' "Risk audit example:"
11
11
  npx -y @synapsor/runner audit --example dangerous-db-mcp --format markdown
@@ -13,10 +13,12 @@ Expected output includes:
13
13
 
14
14
  ```text
15
15
  execute_sql
16
- Synapsor quick demo complete.
17
- proposal created
18
- source DB changed: no
19
- approval required outside MCP
16
+ late_fee_cents: 5500 -> 0
17
+ Source changed:
18
+ No
19
+ Guarded commit complete.
20
+ restart-safe retry: yes
21
+ stale apply refused: yes
20
22
  ```
21
23
 
22
24
  The raw-SQL shortcut is:
@@ -1,7 +1,11 @@
1
- .PHONY: demo unsafe clean
1
+ .PHONY: demo unsafe evaluate clean
2
2
 
3
3
  demo:
4
4
  cd ../.. && examples/support-billing-agent/scripts/run-demo.sh
5
+ cd ../.. && examples/support-billing-agent/scripts/run-evaluation.sh
6
+
7
+ evaluate:
8
+ cd ../.. && examples/support-billing-agent/scripts/run-evaluation.sh
5
9
 
6
10
  unsafe:
7
11
  @printf '%s\n' "Unsafe raw-SQL shortcut this demo avoids:"
@@ -1,16 +1,20 @@
1
1
  # Support/Billing Agent Demo
2
2
 
3
- This is the flagship Synapsor Runner demo.
3
+ This is the flagship Synapsor Runner demo. It uses a disposable PostgreSQL
4
+ database with row-level security (RLS) so the database independently checks the
5
+ same trusted tenant and principal scope as Runner.
4
6
 
5
7
  It uses a disposable Postgres support/billing app and shows the full loop:
6
8
 
7
9
  ```text
8
- agent inspects scoped evidence
10
+ agent inspects ticket SUP-184 and invoice INV-3001
9
11
  -> agent creates a proposal
12
+ -> reviewer sees the exact $55 late-fee waiver
10
13
  -> source database is unchanged
11
14
  -> human approves outside MCP
12
15
  -> guarded writeback applies exactly one row
13
16
  -> replay shows evidence, diff, approval, receipt, and conflict behavior
17
+ -> strict shadow and effect checks compare behavior without enabling writes
14
18
  ```
15
19
 
16
20
  ## Run It
@@ -25,6 +29,8 @@ Expected end state:
25
29
 
26
30
  ```text
27
31
  Reference support/billing app smoke passed.
32
+ Shadow comparison passed: 6 cases, 2 human outcomes, 1 exact agreement, 1 human rejection.
33
+ Effect regression passed: the reviewed $55 waiver effect is unchanged.
28
34
  ```
29
35
 
30
36
  The model-facing tools are exactly:
@@ -41,10 +47,27 @@ The demo proves:
41
47
  - the model does not receive `execute_sql`, approval tools, commit/apply tools,
42
48
  database URLs, write credentials, arbitrary table names, arbitrary column
43
49
  names, or tenant authority;
50
+ - the contract-visible result excludes `card_token`, `internal_risk_note`, and
51
+ their seeded secret-looking values;
52
+ - trusted tenant and principal values are bound outside model arguments, and
53
+ PostgreSQL RLS denies both cross-tenant and same-tenant cross-principal rows;
54
+ - `doctor --check-rls` verifies the live database policies before the demo;
44
55
  - proposals do not mutate the source database;
45
56
  - approved writeback uses tenant, primary-key, allowed-column,
46
57
  idempotency, and `updated_at` conflict guards;
47
- - replay contains the evidence and writeback receipt.
58
+ - an identical retry returns the prior receipt without another mutation;
59
+ - stale data fails closed with `VERSION_CONFLICT`;
60
+ - replay contains the evidence, query audit, approval, writeback event, and
61
+ receipt;
62
+ - strict Shadow Mode records a proposal but cannot approve or mutate it; and
63
+ - the shadow report compares cases with authoritative human outcomes before the
64
+ effect fixture checks the expected business change.
65
+
66
+ RLS is defense in depth, not a claim that a fully compromised Runner process is
67
+ contained. A process that can choose arbitrary trusted context or replace the
68
+ database credential remains outside this guarantee. Production deployments
69
+ must authenticate context upstream, keep the database roles least-privileged,
70
+ and protect Runner's environment.
48
71
 
49
72
  ## Compare The Unsafe Shortcut
50
73
 
@@ -61,8 +84,13 @@ fixture; it shows the path an MCP client should not expose to the model:
61
84
  execute_sql("UPDATE invoices SET late_fee_cents = 0 ...")
62
85
  ```
63
86
 
64
- Use `make demo` for the real proposal, approval, writeback, idempotent retry,
65
- stale-row conflict, and replay proof.
87
+ Use `make demo` for the live RLS, proposal, approval, writeback, idempotent
88
+ retry, stale-row conflict, replay, strict-shadow, human-outcome, and effect
89
+ proofs. Run only the deterministic comparison pass with:
90
+
91
+ ```bash
92
+ make evaluate
93
+ ```
66
94
 
67
95
  ## What This Wraps
68
96
 
@@ -77,7 +105,13 @@ Relevant files:
77
105
  - `db/seed.sql`
78
106
  - `synapsor.runner.json`
79
107
  - `scripts/run-demo.sh`
108
+ - `scripts/run-evaluation.sh`
80
109
  - `app/README.md`
110
+ - `shadow-study/cases.jsonl` and `shadow-study/outcomes.jsonl`: deterministic
111
+ true-shadow reference data for agent-versus-authoritative-outcome reports.
112
+
113
+ See [Shadow Studies](../../docs/shadow-studies.md) for the import and report
114
+ commands. The reference cases do not mutate this demo database.
81
115
 
82
116
  ## Stop And Clean Up
83
117
 
@@ -8,6 +8,7 @@ CREATE TABLE IF NOT EXISTS public.tenants (
8
8
  CREATE TABLE IF NOT EXISTS public.customers (
9
9
  id text PRIMARY KEY,
10
10
  tenant_id text NOT NULL REFERENCES public.tenants(id),
11
+ assigned_to text NOT NULL,
11
12
  name text NOT NULL,
12
13
  email text,
13
14
  plan text NOT NULL,
@@ -20,6 +21,7 @@ CREATE TABLE IF NOT EXISTS public.customers (
20
21
  CREATE TABLE IF NOT EXISTS public.support_tickets (
21
22
  id text PRIMARY KEY,
22
23
  tenant_id text NOT NULL REFERENCES public.tenants(id),
24
+ assigned_to text NOT NULL,
23
25
  customer_id text NOT NULL REFERENCES public.customers(id),
24
26
  subject text NOT NULL,
25
27
  status text NOT NULL,
@@ -30,11 +32,14 @@ CREATE TABLE IF NOT EXISTS public.support_tickets (
30
32
  CREATE TABLE IF NOT EXISTS public.invoices (
31
33
  id text PRIMARY KEY,
32
34
  tenant_id text NOT NULL REFERENCES public.tenants(id),
35
+ assigned_to text NOT NULL,
33
36
  customer_id text NOT NULL REFERENCES public.customers(id),
34
37
  status text NOT NULL,
35
38
  balance_cents integer NOT NULL,
36
39
  late_fee_cents integer NOT NULL,
37
40
  waiver_reason text,
41
+ card_token text,
42
+ internal_risk_note text,
38
43
  updated_at timestamptz NOT NULL DEFAULT now()
39
44
  );
40
45
 
@@ -93,9 +98,91 @@ $$;
93
98
 
94
99
  GRANT CONNECT ON DATABASE synapsor_support_billing_agent TO synapsor_reader, synapsor_writer;
95
100
  GRANT USAGE ON SCHEMA public TO synapsor_reader, synapsor_writer;
96
- GRANT SELECT ON public.tenants, public.customers, public.support_tickets, public.invoices, public.credits, public.agent_actions, public.orders TO synapsor_reader, synapsor_writer;
101
+ GRANT SELECT ON public.tenants, public.credits, public.agent_actions, public.orders TO synapsor_reader, synapsor_writer;
102
+ GRANT SELECT (id, tenant_id, assigned_to, name, plan, plan_credit_cents, credit_reason, created_at, updated_at)
103
+ ON public.customers TO synapsor_reader, synapsor_writer;
104
+ GRANT SELECT (id, tenant_id, assigned_to, customer_id, subject, status, resolution_note, updated_at)
105
+ ON public.support_tickets TO synapsor_reader, synapsor_writer;
106
+ GRANT SELECT (id, tenant_id, assigned_to, customer_id, status, balance_cents, late_fee_cents, waiver_reason, updated_at)
107
+ ON public.invoices TO synapsor_reader, synapsor_writer;
97
108
  GRANT SELECT, INSERT, UPDATE ON public.synapsor_writeback_receipts TO synapsor_writer;
98
109
  GRANT UPDATE (plan_credit_cents, credit_reason, updated_at) ON public.customers TO synapsor_writer;
99
110
  GRANT UPDATE (status, resolution_note, updated_at) ON public.support_tickets TO synapsor_writer;
100
111
  GRANT UPDATE (late_fee_cents, waiver_reason, updated_at) ON public.invoices TO synapsor_writer;
101
112
  GRANT UPDATE (status, status_change_reason, updated_at) ON public.orders TO synapsor_writer;
113
+
114
+ ALTER TABLE public.customers ENABLE ROW LEVEL SECURITY;
115
+ ALTER TABLE public.customers FORCE ROW LEVEL SECURITY;
116
+ ALTER TABLE public.support_tickets ENABLE ROW LEVEL SECURITY;
117
+ ALTER TABLE public.support_tickets FORCE ROW LEVEL SECURITY;
118
+ ALTER TABLE public.invoices ENABLE ROW LEVEL SECURITY;
119
+ ALTER TABLE public.invoices FORCE ROW LEVEL SECURITY;
120
+
121
+ CREATE POLICY customers_reader_scope ON public.customers
122
+ FOR SELECT TO synapsor_reader
123
+ USING (
124
+ tenant_id = current_setting('synapsor.tenant_id', true)
125
+ AND assigned_to = current_setting('synapsor.principal', true)
126
+ );
127
+ CREATE POLICY customers_writer_select_scope ON public.customers
128
+ FOR SELECT TO synapsor_writer
129
+ USING (
130
+ tenant_id = current_setting('synapsor.tenant_id', true)
131
+ AND assigned_to = current_setting('synapsor.principal', true)
132
+ );
133
+ CREATE POLICY customers_writer_update_scope ON public.customers
134
+ FOR UPDATE TO synapsor_writer
135
+ USING (
136
+ tenant_id = current_setting('synapsor.tenant_id', true)
137
+ AND assigned_to = current_setting('synapsor.principal', true)
138
+ )
139
+ WITH CHECK (
140
+ tenant_id = current_setting('synapsor.tenant_id', true)
141
+ AND assigned_to = current_setting('synapsor.principal', true)
142
+ );
143
+
144
+ CREATE POLICY support_tickets_reader_scope ON public.support_tickets
145
+ FOR SELECT TO synapsor_reader
146
+ USING (
147
+ tenant_id = current_setting('synapsor.tenant_id', true)
148
+ AND assigned_to = current_setting('synapsor.principal', true)
149
+ );
150
+ CREATE POLICY support_tickets_writer_select_scope ON public.support_tickets
151
+ FOR SELECT TO synapsor_writer
152
+ USING (
153
+ tenant_id = current_setting('synapsor.tenant_id', true)
154
+ AND assigned_to = current_setting('synapsor.principal', true)
155
+ );
156
+ CREATE POLICY support_tickets_writer_update_scope ON public.support_tickets
157
+ FOR UPDATE TO synapsor_writer
158
+ USING (
159
+ tenant_id = current_setting('synapsor.tenant_id', true)
160
+ AND assigned_to = current_setting('synapsor.principal', true)
161
+ )
162
+ WITH CHECK (
163
+ tenant_id = current_setting('synapsor.tenant_id', true)
164
+ AND assigned_to = current_setting('synapsor.principal', true)
165
+ );
166
+
167
+ CREATE POLICY invoices_reader_scope ON public.invoices
168
+ FOR SELECT TO synapsor_reader
169
+ USING (
170
+ tenant_id = current_setting('synapsor.tenant_id', true)
171
+ AND assigned_to = current_setting('synapsor.principal', true)
172
+ );
173
+ CREATE POLICY invoices_writer_select_scope ON public.invoices
174
+ FOR SELECT TO synapsor_writer
175
+ USING (
176
+ tenant_id = current_setting('synapsor.tenant_id', true)
177
+ AND assigned_to = current_setting('synapsor.principal', true)
178
+ );
179
+ CREATE POLICY invoices_writer_update_scope ON public.invoices
180
+ FOR UPDATE TO synapsor_writer
181
+ USING (
182
+ tenant_id = current_setting('synapsor.tenant_id', true)
183
+ AND assigned_to = current_setting('synapsor.principal', true)
184
+ )
185
+ WITH CHECK (
186
+ tenant_id = current_setting('synapsor.tenant_id', true)
187
+ AND assigned_to = current_setting('synapsor.principal', true)
188
+ );
@@ -4,26 +4,26 @@ VALUES
4
4
  ('otherco', 'OtherCo Labs', '2026-06-20T10:00:00Z', '2026-06-20T10:00:00Z')
5
5
  ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, updated_at = EXCLUDED.updated_at;
6
6
 
7
- INSERT INTO public.customers (id, tenant_id, name, email, plan, plan_credit_cents, credit_reason, created_at, updated_at)
7
+ INSERT INTO public.customers (id, tenant_id, assigned_to, name, email, plan, plan_credit_cents, credit_reason, created_at, updated_at)
8
8
  VALUES
9
- ('cust_acme_1', 'acme', 'Acme Robotics', 'ops@example.invalid', 'enterprise', 0, NULL, '2026-06-20T10:00:00Z', '2026-06-20T10:00:00Z'),
10
- ('cust_acme_2', 'acme', 'Acme Field Ops', 'field@example.invalid', 'builder', 0, NULL, '2026-06-20T10:00:00Z', '2026-06-20T10:00:00Z'),
11
- ('cust_other_1', 'otherco', 'OtherCo Labs', 'ops@otherco.invalid', 'builder', 0, NULL, '2026-06-20T10:00:00Z', '2026-06-20T10:00:00Z')
12
- ON CONFLICT (id) DO UPDATE SET tenant_id = EXCLUDED.tenant_id, name = EXCLUDED.name, email = EXCLUDED.email, plan = EXCLUDED.plan, plan_credit_cents = EXCLUDED.plan_credit_cents, credit_reason = EXCLUDED.credit_reason, updated_at = EXCLUDED.updated_at;
9
+ ('cust_acme_1', 'acme', 'reference_operator', 'Acme Robotics', 'ops@example.invalid', 'enterprise', 0, NULL, '2026-06-20T10:00:00Z', '2026-06-20T10:00:00Z'),
10
+ ('cust_acme_2', 'acme', 'another_operator', 'Acme Field Ops', 'field@example.invalid', 'builder', 0, NULL, '2026-06-20T10:00:00Z', '2026-06-20T10:00:00Z'),
11
+ ('cust_other_1', 'otherco', 'reference_operator', 'OtherCo Labs', 'ops@otherco.invalid', 'builder', 0, NULL, '2026-06-20T10:00:00Z', '2026-06-20T10:00:00Z')
12
+ ON CONFLICT (id) DO UPDATE SET tenant_id = EXCLUDED.tenant_id, assigned_to = EXCLUDED.assigned_to, name = EXCLUDED.name, email = EXCLUDED.email, plan = EXCLUDED.plan, plan_credit_cents = EXCLUDED.plan_credit_cents, credit_reason = EXCLUDED.credit_reason, updated_at = EXCLUDED.updated_at;
13
13
 
14
- INSERT INTO public.support_tickets (id, tenant_id, customer_id, subject, status, resolution_note, updated_at)
14
+ INSERT INTO public.support_tickets (id, tenant_id, assigned_to, customer_id, subject, status, resolution_note, updated_at)
15
15
  VALUES
16
- ('T-1042', 'acme', 'cust_acme_1', 'Late fee waiver request for INV-3001', 'open', NULL, '2026-06-20T12:00:00Z'),
17
- ('T-1043', 'acme', 'cust_acme_2', 'Duplicate card charge question', 'open', NULL, '2026-06-20T12:05:00Z'),
18
- ('T-9001', 'otherco', 'cust_other_1', 'OtherCo private billing ticket', 'open', NULL, '2026-06-20T12:00:00Z')
19
- ON CONFLICT (id) DO UPDATE SET tenant_id = EXCLUDED.tenant_id, customer_id = EXCLUDED.customer_id, subject = EXCLUDED.subject, status = EXCLUDED.status, resolution_note = EXCLUDED.resolution_note, updated_at = EXCLUDED.updated_at;
16
+ ('SUP-184', 'acme', 'reference_operator', 'cust_acme_1', 'Late fee waiver request for INV-3001', 'open', NULL, '2026-06-20T12:00:00Z'),
17
+ ('SUP-185', 'acme', 'another_operator', 'cust_acme_2', 'Duplicate card charge question', 'open', NULL, '2026-06-20T12:05:00Z'),
18
+ ('SUP-9001', 'otherco', 'reference_operator', 'cust_other_1', 'OtherCo private billing ticket', 'open', NULL, '2026-06-20T12:00:00Z')
19
+ ON CONFLICT (id) DO UPDATE SET tenant_id = EXCLUDED.tenant_id, assigned_to = EXCLUDED.assigned_to, customer_id = EXCLUDED.customer_id, subject = EXCLUDED.subject, status = EXCLUDED.status, resolution_note = EXCLUDED.resolution_note, updated_at = EXCLUDED.updated_at;
20
20
 
21
- INSERT INTO public.invoices (id, tenant_id, customer_id, status, balance_cents, late_fee_cents, waiver_reason, updated_at)
21
+ INSERT INTO public.invoices (id, tenant_id, assigned_to, customer_id, status, balance_cents, late_fee_cents, waiver_reason, card_token, internal_risk_note, updated_at)
22
22
  VALUES
23
- ('INV-3001', 'acme', 'cust_acme_1', 'overdue', 25500, 5500, NULL, '2026-06-20T14:31:08Z'),
24
- ('INV-3002', 'acme', 'cust_acme_2', 'paid', 0, 0, NULL, '2026-06-20T14:40:00Z'),
25
- ('INV-9001', 'otherco', 'cust_other_1', 'overdue', 25500, 5500, NULL, '2026-06-20T14:31:08Z')
26
- ON CONFLICT (id) DO UPDATE SET tenant_id = EXCLUDED.tenant_id, customer_id = EXCLUDED.customer_id, status = EXCLUDED.status, balance_cents = EXCLUDED.balance_cents, late_fee_cents = EXCLUDED.late_fee_cents, waiver_reason = EXCLUDED.waiver_reason, updated_at = EXCLUDED.updated_at;
23
+ ('INV-3001', 'acme', 'reference_operator', 'cust_acme_1', 'overdue', 25500, 5500, NULL, 'tok_acme_must_stay_hidden', 'manual risk note must stay hidden', '2026-06-20T14:31:08Z'),
24
+ ('INV-3002', 'acme', 'another_operator', 'cust_acme_2', 'paid', 0, 0, NULL, 'tok_acme_other_principal', 'another principal note', '2026-06-20T14:40:00Z'),
25
+ ('INV-9001', 'otherco', 'reference_operator', 'cust_other_1', 'overdue', 25500, 5500, NULL, 'tok_other_tenant', 'other tenant note', '2026-06-20T14:31:08Z')
26
+ ON CONFLICT (id) DO UPDATE SET tenant_id = EXCLUDED.tenant_id, assigned_to = EXCLUDED.assigned_to, customer_id = EXCLUDED.customer_id, status = EXCLUDED.status, balance_cents = EXCLUDED.balance_cents, late_fee_cents = EXCLUDED.late_fee_cents, waiver_reason = EXCLUDED.waiver_reason, card_token = EXCLUDED.card_token, internal_risk_note = EXCLUDED.internal_risk_note, updated_at = EXCLUDED.updated_at;
27
27
 
28
28
  INSERT INTO public.credits (id, tenant_id, customer_id, invoice_id, amount_cents, reason, status, created_at, updated_at)
29
29
  VALUES
@@ -12,4 +12,8 @@ SYNAPSOR_REFERENCE_DB="synapsor_support_billing_agent" \
12
12
  SYNAPSOR_REFERENCE_PORT="55436" \
13
13
  SYNAPSOR_REFERENCE_EXPECTED_TOOLS="support.inspect_ticket,support.propose_plan_credit,billing.inspect_invoice,billing.propose_late_fee_waiver" \
14
14
  SYNAPSOR_REFERENCE_EXACT_TOOLS="1" \
15
+ SYNAPSOR_REFERENCE_TICKET_ID="SUP-184" \
16
+ SYNAPSOR_REFERENCE_REQUIRE_PRINCIPAL_SCOPE="1" \
17
+ SYNAPSOR_REFERENCE_REQUIRE_RLS="1" \
18
+ SYNAPSOR_REFERENCE_SHADOW_PROOF="1" \
15
19
  node scripts/smoke-reference-support-billing-app.mjs
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
5
+ cd "$ROOT_DIR"
6
+
7
+ OUTPUT_DIR="$ROOT_DIR/tmp/support-billing-agent/evaluation"
8
+ STORE_PATH="$OUTPUT_DIR/shadow.db"
9
+ REPORT_PATH="$OUTPUT_DIR/shadow-report.json"
10
+ EFFECT_PATH="$OUTPUT_DIR/effect-report.json"
11
+
12
+ rm -rf "$OUTPUT_DIR"
13
+ mkdir -p "$OUTPUT_DIR"
14
+
15
+ corepack pnpm runner shadow study create \
16
+ --id sst_support_reference \
17
+ --name "Support reference study" \
18
+ --capability billing.propose_late_fee_waiver \
19
+ --store "$STORE_PATH" >/dev/null
20
+
21
+ corepack pnpm runner shadow case import \
22
+ --study sst_support_reference \
23
+ --input "$ROOT_DIR/examples/support-billing-agent/shadow-study/cases.jsonl" \
24
+ --store "$STORE_PATH" >/dev/null
25
+
26
+ corepack pnpm runner shadow outcome import \
27
+ --study sst_support_reference \
28
+ --input "$ROOT_DIR/examples/support-billing-agent/shadow-study/outcomes.jsonl" \
29
+ --store "$STORE_PATH" >/dev/null
30
+
31
+ corepack pnpm runner shadow report \
32
+ --study sst_support_reference \
33
+ --store "$STORE_PATH" \
34
+ --output "$REPORT_PATH" >/dev/null
35
+
36
+ node --input-type=module - "$REPORT_PATH" <<'NODE'
37
+ import fs from "node:fs";
38
+ const report = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
39
+ const expected = {
40
+ total_tasks_observed: 6,
41
+ tasks_with_authoritative_outcomes: 2,
42
+ comparable_tasks: 1,
43
+ exact_agreements: 1,
44
+ human_rejections_no_action: 1,
45
+ policy_denials: 1,
46
+ stale_conflicts: 1,
47
+ unmatched_cases: 1,
48
+ invalid_or_unsafe_scope_attempts: 1,
49
+ };
50
+ for (const [key, value] of Object.entries(expected)) {
51
+ if (report[key] !== value) throw new Error(`shadow report ${key}=${report[key]}, expected ${value}`);
52
+ }
53
+ NODE
54
+
55
+ corepack pnpm runner effect run \
56
+ --dataset "$ROOT_DIR/fixtures/effects/dataset.json" \
57
+ --results-dir "$ROOT_DIR/fixtures/effects/results" \
58
+ --format json \
59
+ --out "$EFFECT_PATH" >/dev/null
60
+
61
+ printf '%s\n' "Shadow comparison passed: 6 cases, 2 human outcomes, 1 exact agreement, 1 human rejection."
62
+ printf '%s\n' "Effect regression passed: the reviewed \$55 waiver effect is unchanged."
@@ -0,0 +1,6 @@
1
+ {"request_id":"req-waiver-exact","tenant_id":"acme","principal":"support-agent-shadow","capability":"billing.propose_late_fee_waiver","business_object":"invoice","object_id":"INV-3001","agent_result":"proposed","proposed_effect":{"before":{"late_fee_cents":5500,"waiver_reason":null},"after":{"late_fee_cents":0,"waiver_reason":"courtesy waiver"},"patch":{"late_fee_cents":0,"waiver_reason":"courtesy waiver"}},"decision_reason":"customer qualifies for one courtesy waiver","risk_score":20,"amount_value":5500,"created_at":"2026-07-19T10:00:00.000Z"}
2
+ {"request_id":"req-waiver-rejected","tenant_id":"acme","principal":"support-agent-shadow","capability":"billing.propose_late_fee_waiver","business_object":"invoice","object_id":"INV-REJECTED","agent_result":"proposed","proposed_effect":{"before":{"late_fee_cents":5500,"waiver_reason":null},"after":{"late_fee_cents":0,"waiver_reason":"courtesy waiver"},"patch":{"late_fee_cents":0,"waiver_reason":"courtesy waiver"}},"decision_reason":"agent found apparent eligibility","risk_score":45,"amount_value":5500,"created_at":"2026-07-19T10:01:00.000Z"}
3
+ {"request_id":"req-waiver-policy","tenant_id":"acme","principal":"support-agent-shadow","capability":"billing.propose_late_fee_waiver","business_object":"invoice","object_id":"INV-EXCESSIVE","agent_result":"policy_denied","decision_reason":"amount above reviewed bound","risk_score":70,"amount_value":25000,"created_at":"2026-07-19T10:02:00.000Z"}
4
+ {"request_id":"req-waiver-stale","tenant_id":"acme","principal":"support-agent-shadow","capability":"billing.propose_late_fee_waiver","business_object":"invoice","object_id":"INV-STALE","agent_result":"stale_conflict","decision_reason":"source version moved before proposal could be normalized","risk_score":60,"amount_value":5500,"created_at":"2026-07-19T10:03:00.000Z"}
5
+ {"request_id":"req-waiver-forgery","tenant_id":"acme","principal":"support-agent-shadow","capability":"billing.propose_late_fee_waiver","business_object":"invoice","object_id":"INV-OTHER-TENANT","agent_result":"invalid_unsafe_scope_attempt","decision_reason":"model-supplied tenant override rejected","risk_score":100,"created_at":"2026-07-19T10:04:00.000Z"}
6
+ {"request_id":"req-waiver-unmatched","tenant_id":"acme","principal":"support-agent-shadow","capability":"billing.propose_late_fee_waiver","business_object":"invoice","object_id":"INV-UNMATCHED","agent_result":"proposed","proposed_effect":{"before":{"late_fee_cents":5500,"waiver_reason":null},"after":{"late_fee_cents":0,"waiver_reason":"courtesy waiver"},"patch":{"late_fee_cents":0,"waiver_reason":"courtesy waiver"}},"decision_reason":"awaiting authoritative outcome","risk_score":30,"amount_value":5500,"created_at":"2026-07-19T10:05:00.000Z"}
@@ -0,0 +1,2 @@
1
+ {"request_id":"req-waiver-exact","tenant_id":"acme","business_object":"invoice","object_id":"INV-3001","actor":"support_lead_1","disposition":"applied","actual_effect":{"before":{"late_fee_cents":5500,"waiver_reason":null},"after":{"late_fee_cents":0,"waiver_reason":"courtesy waiver"},"patch":{"late_fee_cents":0,"waiver_reason":"courtesy waiver"}},"occurred_at":"2026-07-19T11:00:00.000Z","source":"support-system-audit","reference":"ticket:SUP-184","reason":"customer qualified"}
2
+ {"request_id":"req-waiver-rejected","tenant_id":"acme","business_object":"invoice","object_id":"INV-REJECTED","actor":"support_lead_2","disposition":"rejected_no_action","occurred_at":"2026-07-19T11:01:00.000Z","source":"support-system-audit","reference":"ticket:SUP-REJECTED","reason":"customer already received a courtesy waiver"}