@synapsor/runner 1.4.123 → 1.5.3

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 (98) hide show
  1. package/CHANGELOG.md +144 -19
  2. package/README.md +132 -135
  3. package/dist/authoring.d.ts +23 -0
  4. package/dist/authoring.d.ts.map +1 -0
  5. package/dist/authoring.mjs +1318 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +4 -5
  8. package/dist/local-ui.d.ts +14 -0
  9. package/dist/local-ui.d.ts.map +1 -1
  10. package/dist/runner.mjs +18427 -7084
  11. package/dist/runtime.d.ts +50 -0
  12. package/dist/runtime.d.ts.map +1 -0
  13. package/dist/runtime.mjs +15094 -0
  14. package/dist/shadow.d.ts +36 -0
  15. package/dist/shadow.d.ts.map +1 -0
  16. package/dist/shadow.mjs +5262 -0
  17. package/docs/README.md +44 -5
  18. package/docs/alternatives.md +122 -0
  19. package/docs/capability-authoring.md +44 -1
  20. package/docs/client-recipes.md +218 -0
  21. package/docs/contract-testing.md +9 -7
  22. package/docs/cursor-plugin.md +78 -0
  23. package/docs/database-enforced-scope.md +273 -0
  24. package/docs/dependency-license-inventory.md +6 -2
  25. package/docs/doctor.md +17 -0
  26. package/docs/effect-regression.md +179 -0
  27. package/docs/fresh-developer-usability.md +110 -0
  28. package/docs/getting-started-own-database.md +103 -2
  29. package/docs/host-compatibility.md +59 -0
  30. package/docs/local-mode.md +12 -7
  31. package/docs/mcp-apps.md +188 -0
  32. package/docs/mcp-audit.md +220 -2
  33. package/docs/mcp-client-setup.md +20 -4
  34. package/docs/mcp-clients.md +41 -4
  35. package/docs/oss-vs-cloud.md +3 -0
  36. package/docs/production.md +8 -0
  37. package/docs/release-notes.md +138 -14
  38. package/docs/release-policy.md +9 -5
  39. package/docs/runner-config-reference.md +25 -0
  40. package/docs/schema-api-candidates.md +68 -0
  41. package/docs/security-boundary.md +30 -5
  42. package/docs/shadow-studies.md +241 -0
  43. package/docs/troubleshooting-first-run.md +46 -0
  44. package/examples/mcp-postgres-billing-app-handler/app-handler.mjs +9 -1
  45. package/examples/mcp-postgres-billing-app-handler/schema.sql +11 -0
  46. package/examples/mcp-postgres-billing-app-handler/synapsor-handler.mjs +3 -0
  47. package/examples/raw-sql-vs-synapsor/Makefile +2 -2
  48. package/examples/raw-sql-vs-synapsor/README.md +6 -4
  49. package/examples/support-billing-agent/Makefile +5 -1
  50. package/examples/support-billing-agent/README.md +57 -5
  51. package/examples/support-billing-agent/app/README.md +6 -0
  52. package/examples/support-billing-agent/app/contract.ts +28 -0
  53. package/examples/support-billing-agent/app/effect-adapter.mjs +23 -0
  54. package/examples/support-billing-agent/app/record-shadow-outcomes.mjs +44 -0
  55. package/examples/support-billing-agent/db/schema.sql +88 -1
  56. package/examples/support-billing-agent/db/seed.sql +15 -15
  57. package/examples/support-billing-agent/scripts/run-demo.sh +4 -0
  58. package/examples/support-billing-agent/scripts/run-evaluation.sh +63 -0
  59. package/examples/support-billing-agent/shadow-study/cases.jsonl +6 -0
  60. package/examples/support-billing-agent/shadow-study/outcomes.jsonl +2 -0
  61. package/examples/support-billing-agent/synapsor.runner.json +17 -5
  62. package/examples/support-plan-credit/README.md +48 -2
  63. package/examples/support-plan-credit/mcp-client-examples/README.md +23 -0
  64. package/examples/support-plan-credit/mcp-client-examples/claude-code.sh +34 -0
  65. package/examples/support-plan-credit/mcp-client-examples/codex.config.toml +24 -0
  66. package/examples/support-plan-credit/mcp-client-examples/generic-stdio.mjs +35 -0
  67. package/examples/support-plan-credit/mcp-client-examples/generic-streamable-http.mjs +31 -0
  68. package/examples/support-plan-credit/mcp-client-examples/google-adk.py +40 -0
  69. package/examples/support-plan-credit/mcp-client-examples/langchain.mjs +29 -0
  70. package/examples/support-plan-credit/mcp-client-examples/llamaindex.py +36 -0
  71. package/examples/support-plan-credit/mcp-client-examples/openai-agents-stdio.ts +1 -1
  72. package/examples/support-plan-credit/mcp-client-examples/openai-agents-streamable-http.ts +1 -1
  73. package/examples/support-plan-credit/mcp-client-examples/vscode.mcp.json +20 -0
  74. package/examples/support-plan-credit/synapsor/actions/support.propose_plan_credit.contract-tests.generated.json +221 -0
  75. package/examples/support-plan-credit/synapsor/actions/support.propose_plan_credit.ts +34 -0
  76. package/fixtures/effects/changed/eff_support_late_fee_v1.result.json +58 -0
  77. package/fixtures/effects/dataset.json +8 -0
  78. package/fixtures/effects/results/eff_support_late_fee_v1.result.json +59 -0
  79. package/fixtures/effects/support-late-fee.effect.json +71 -0
  80. package/fixtures/generators/drizzle/malicious.ts +11 -0
  81. package/fixtures/generators/drizzle/schema.ts +12 -0
  82. package/fixtures/generators/openapi/external-ref.yaml +15 -0
  83. package/fixtures/generators/openapi/openapi.yaml +81 -0
  84. package/fixtures/generators/prisma/schema.prisma +34 -0
  85. package/fixtures/mcp-audit/README.md +14 -0
  86. package/fixtures/mcp-audit/cursor-bypass-config.json +38 -0
  87. package/fixtures/mcp-audit/dangerous-tools-list.json +33 -0
  88. package/fixtures/mcp-audit/reviewed-proposal-tools-list.json +60 -0
  89. package/package.json +29 -3
  90. package/schemas/effect-dataset.schema.json +19 -0
  91. package/schemas/effect-fixture.schema.json +191 -0
  92. package/schemas/effect-result.schema.json +52 -0
  93. package/schemas/mcp-audit-candidates.schema.json +41 -0
  94. package/schemas/mcp-audit-report.schema.json +203 -0
  95. package/schemas/schema-candidate-review.schema.json +167 -0
  96. package/schemas/schema-candidates.schema.json +49 -0
  97. package/schemas/synapsor.contract-tests.schema.json +1 -1
  98. package/schemas/synapsor.runner.schema.json +51 -0
@@ -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,42 @@ 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
+ The app-owned integration files make those two evaluation paths reusable:
67
+
68
+ - `app/contract.ts` is the code-first TypeScript form of the reviewed late-fee
69
+ action and compiles to the same canonical spec as JSON/DSL;
70
+ - `app/record-shadow-outcomes.mjs` records authoritative app outcomes through
71
+ `@synapsor/runner/shadow` without approval or source mutation;
72
+ - `app/effect-adapter.mjs` emits one deterministic, propose-only result through
73
+ the provider-neutral command adapter;
74
+ - `.github/workflows/effect-regression.yml` runs the effect check and uploads
75
+ JUnit output for relevant pull requests.
76
+
77
+ This repository result is deterministic application evidence, not an external
78
+ model benchmark. Use `--result-origin external-model` only when your adapter
79
+ actually invokes a model.
80
+
81
+ RLS is defense in depth, not a claim that a fully compromised Runner process is
82
+ contained. A process that can choose arbitrary trusted context or replace the
83
+ database credential remains outside this guarantee. Production deployments
84
+ must authenticate context upstream, keep the database roles least-privileged,
85
+ and protect Runner's environment.
48
86
 
49
87
  ## Compare The Unsafe Shortcut
50
88
 
@@ -61,8 +99,13 @@ fixture; it shows the path an MCP client should not expose to the model:
61
99
  execute_sql("UPDATE invoices SET late_fee_cents = 0 ...")
62
100
  ```
63
101
 
64
- Use `make demo` for the real proposal, approval, writeback, idempotent retry,
65
- stale-row conflict, and replay proof.
102
+ Use `make demo` for the live RLS, proposal, approval, writeback, idempotent
103
+ retry, stale-row conflict, replay, strict-shadow, human-outcome, and effect
104
+ proofs. Run only the deterministic comparison pass with:
105
+
106
+ ```bash
107
+ make evaluate
108
+ ```
66
109
 
67
110
  ## What This Wraps
68
111
 
@@ -77,7 +120,16 @@ Relevant files:
77
120
  - `db/seed.sql`
78
121
  - `synapsor.runner.json`
79
122
  - `scripts/run-demo.sh`
123
+ - `scripts/run-evaluation.sh`
80
124
  - `app/README.md`
125
+ - `app/contract.ts`
126
+ - `app/record-shadow-outcomes.mjs`
127
+ - `app/effect-adapter.mjs`
128
+ - `shadow-study/cases.jsonl` and `shadow-study/outcomes.jsonl`: deterministic
129
+ true-shadow reference data for agent-versus-authoritative-outcome reports.
130
+
131
+ See [Shadow Studies](../../docs/shadow-studies.md) for the import and report
132
+ commands. The reference cases do not mutate this demo database.
81
133
 
82
134
  ## Stop And Clean Up
83
135
 
@@ -11,3 +11,9 @@ writeback with tenant, primary-key, allowed-column, idempotency, and
11
11
  For richer app actions, such as inserting a credit ledger row and emitting an
12
12
  event, use the app-owned handler examples in `../app-owned-writeback/` and
13
13
  `../mcp-postgres-billing-app-handler/`.
14
+
15
+ `contract.ts` demonstrates the optional typed authoring frontend without
16
+ changing the canonical contract. `record-shadow-outcomes.mjs` is the normal-app
17
+ bridge for authoritative human outcomes. `effect-adapter.mjs` is a deterministic
18
+ propose-only CI adapter; it receives a bounded fixture path and emits one
19
+ canonical result. Neither helper approves, applies, or mutates the source.
@@ -0,0 +1,28 @@
1
+ import { defineCapability } from "@synapsor/runner/authoring";
2
+
3
+ export const waiveLateFee = defineCapability({
4
+ name: "billing.propose_late_fee_waiver",
5
+ kind: "proposal",
6
+ context: "local_operator",
7
+ source: "app_postgres",
8
+ subject: { resource: "billing.invoice", primary_key: "id", tenant_key: "tenant_id" },
9
+ args: {
10
+ invoice_id: { type: "string", required: true, max_length: 128 },
11
+ reason: { type: "string", required: true, max_length: 500 },
12
+ },
13
+ lookup: { id_from_arg: "invoice_id" },
14
+ visible_fields: ["id", "tenant_id", "late_fee_cents", "waiver_reason", "updated_at"],
15
+ kept_out_fields: ["card_token", "internal_risk_note"],
16
+ evidence: { required: true, query_audit: true },
17
+ max_rows: 1,
18
+ proposal: {
19
+ action: "waive_late_fee",
20
+ operation: { kind: "update" },
21
+ allowed_fields: ["late_fee_cents", "waiver_reason"],
22
+ patch: { late_fee_cents: { fixed: 0 }, waiver_reason: { from_arg: "reason" } },
23
+ numeric_bounds: { late_fee_cents: { minimum: 0, maximum: 10000 } },
24
+ conflict_guard: { column: "updated_at" },
25
+ approval: { mode: "human", required_role: "billing_lead" },
26
+ writeback: { mode: "direct_sql" },
27
+ },
28
+ });
@@ -0,0 +1,23 @@
1
+ import fs from "node:fs";
2
+
3
+ if (process.env.SYNAPSOR_EFFECT_MODE !== "propose_only") {
4
+ throw new Error("reference effect adapter requires SYNAPSOR_EFFECT_MODE=propose_only");
5
+ }
6
+ if (process.env.DATABASE_URL || process.env.SYNAPSOR_DATABASE_WRITE_URL) {
7
+ throw new Error("reference effect adapter must not receive database credentials");
8
+ }
9
+ const fixturePath = process.env.SYNAPSOR_EFFECT_FIXTURE_PATH;
10
+ if (!fixturePath) throw new Error("SYNAPSOR_EFFECT_FIXTURE_PATH is required");
11
+ const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
12
+ const expected = fixture.expected;
13
+
14
+ process.stdout.write(`${JSON.stringify({
15
+ schema_version: "synapsor.effect-result.v1",
16
+ fixture_id: fixture.fixture_id,
17
+ capability_calls: expected.capability_calls.map((name) => ({ name })),
18
+ trusted_context: expected.trusted_context,
19
+ proposal: expected.proposal,
20
+ observed_fields: Object.keys(expected.proposal.diff ?? {}).sort(),
21
+ evidence: { mode: "fixture", new_source_reads: false },
22
+ source_database_changed: false,
23
+ })}\n`);
@@ -0,0 +1,44 @@
1
+ import { createShadowOutcomeRecorder } from "@synapsor/runner/shadow";
2
+
3
+ const [storePath, studyId] = process.argv.slice(2);
4
+ if (!storePath || !studyId) {
5
+ throw new Error("usage: node record-shadow-outcomes.mjs <store-path> <study-id>");
6
+ }
7
+
8
+ const recorder = createShadowOutcomeRecorder({
9
+ storePath,
10
+ studyId,
11
+ actor: "support_reference_app",
12
+ source: "support-app-audit",
13
+ });
14
+ try {
15
+ recorder.record({
16
+ requestId: "req-waiver-exact",
17
+ tenantId: "acme",
18
+ businessObject: "invoice",
19
+ objectId: "INV-3001",
20
+ disposition: "applied",
21
+ actualEffect: {
22
+ before: { late_fee_cents: 5500, waiver_reason: null },
23
+ after: { late_fee_cents: 0, waiver_reason: "courtesy waiver" },
24
+ patch: { late_fee_cents: 0, waiver_reason: "courtesy waiver" },
25
+ },
26
+ occurredAt: "2026-07-19T11:00:00.000Z",
27
+ reference: "ticket:SUP-184",
28
+ reason: "customer qualified",
29
+ });
30
+ recorder.record({
31
+ requestId: "req-waiver-rejected",
32
+ tenantId: "acme",
33
+ businessObject: "invoice",
34
+ objectId: "INV-REJECTED",
35
+ disposition: "rejected_no_action",
36
+ occurredAt: "2026-07-19T11:01:00.000Z",
37
+ reference: "ticket:SUP-REJECTED",
38
+ reason: "customer already received a courtesy waiver",
39
+ });
40
+ } finally {
41
+ recorder.close();
42
+ }
43
+
44
+ process.stdout.write("Recorded 2 authoritative outcomes through @synapsor/runner/shadow.\n");
@@ -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,63 @@
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 build:runner-package >/dev/null
27
+ node "$ROOT_DIR/examples/support-billing-agent/app/record-shadow-outcomes.mjs" \
28
+ "$STORE_PATH" sst_support_reference >/dev/null
29
+
30
+ corepack pnpm runner shadow report \
31
+ --study sst_support_reference \
32
+ --store "$STORE_PATH" \
33
+ --output "$REPORT_PATH" >/dev/null
34
+
35
+ node --input-type=module - "$REPORT_PATH" <<'NODE'
36
+ import fs from "node:fs";
37
+ const report = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
38
+ const expected = {
39
+ total_tasks_observed: 6,
40
+ tasks_with_authoritative_outcomes: 2,
41
+ comparable_tasks: 1,
42
+ exact_agreements: 1,
43
+ human_rejections_no_action: 1,
44
+ policy_denials: 1,
45
+ stale_conflicts: 1,
46
+ unmatched_cases: 1,
47
+ invalid_or_unsafe_scope_attempts: 1,
48
+ };
49
+ for (const [key, value] of Object.entries(expected)) {
50
+ if (report[key] !== value) throw new Error(`shadow report ${key}=${report[key]}, expected ${value}`);
51
+ }
52
+ NODE
53
+
54
+ corepack pnpm runner effect run \
55
+ --dataset "$ROOT_DIR/fixtures/effects/dataset.json" \
56
+ --adapter node \
57
+ --adapter-arg "$ROOT_DIR/examples/support-billing-agent/app/effect-adapter.mjs" \
58
+ --result-origin deterministic-application \
59
+ --format json \
60
+ --out "$EFFECT_PATH" >/dev/null
61
+
62
+ printf '%s\n' "Shadow comparison passed: 6 cases, 2 human outcomes, 1 exact agreement, 1 human rejection."
63
+ 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"}
@@ -9,7 +9,15 @@
9
9
  "engine": "postgres",
10
10
  "read_url_env": "REFERENCE_POSTGRES_READ_URL",
11
11
  "write_url_env": "REFERENCE_POSTGRES_WRITE_URL",
12
- "statement_timeout_ms": 3000
12
+ "statement_timeout_ms": 3000,
13
+ "database_scope": {
14
+ "mode": "postgres_rls",
15
+ "tenant_setting": "synapsor.tenant_id",
16
+ "principal_setting": "synapsor.principal"
17
+ },
18
+ "credential_scope": {
19
+ "mode": "shared"
20
+ }
13
21
  }
14
22
  },
15
23
  "trusted_context": {
@@ -38,7 +46,8 @@
38
46
  "schema": "public",
39
47
  "table": "support_tickets",
40
48
  "primary_key": "id",
41
- "tenant_key": "tenant_id"
49
+ "tenant_key": "tenant_id",
50
+ "principal_scope_key": "assigned_to"
42
51
  },
43
52
  "args": {
44
53
  "ticket_id": {
@@ -71,7 +80,8 @@
71
80
  "schema": "public",
72
81
  "table": "customers",
73
82
  "primary_key": "id",
74
- "tenant_key": "tenant_id"
83
+ "tenant_key": "tenant_id",
84
+ "principal_scope_key": "assigned_to"
75
85
  },
76
86
  "args": {
77
87
  "customer_id": {
@@ -140,7 +150,8 @@
140
150
  "schema": "public",
141
151
  "table": "invoices",
142
152
  "primary_key": "id",
143
- "tenant_key": "tenant_id"
153
+ "tenant_key": "tenant_id",
154
+ "principal_scope_key": "assigned_to"
144
155
  },
145
156
  "args": {
146
157
  "invoice_id": {
@@ -174,7 +185,8 @@
174
185
  "schema": "public",
175
186
  "table": "invoices",
176
187
  "primary_key": "id",
177
- "tenant_key": "tenant_id"
188
+ "tenant_key": "tenant_id",
189
+ "principal_scope_key": "assigned_to"
178
190
  },
179
191
  "args": {
180
192
  "invoice_id": {
@@ -15,7 +15,7 @@ policy-tier demonstration.
15
15
  ## Prerequisites
16
16
 
17
17
  - Docker
18
- - Node 22.5+
18
+ - Node 22.13+
19
19
  - Port `55438` available
20
20
 
21
21
  Install the stable CLI once for the commands below:
@@ -50,6 +50,42 @@ network connection or connecting to a database. From a source checkout,
50
50
  `corepack pnpm verify:adoption` runs this boundary check plus the no-database
51
51
  quick demo and MCP config validation.
52
52
 
53
+ ## Clone And Validate The Same Safe Action
54
+
55
+ The checked-in code-first action is
56
+ [`synapsor/actions/support.propose_plan_credit.ts`](synapsor/actions/support.propose_plan_credit.ts).
57
+ It is a reviewable frontend for the same canonical JSON contract, not another
58
+ runtime format. Editing it cannot change active tools.
59
+
60
+ After cloning the repository and installing dependencies, another developer can
61
+ run the deterministic team gate without a database or Cloud account:
62
+
63
+ ```bash
64
+ corepack pnpm install --frozen-lockfile
65
+ corepack pnpm verify:safe-action-team
66
+ ```
67
+
68
+ The verifier copies this example to a disposable directory, compiles the action
69
+ only as a disabled draft, compares the regenerated boundary tests with the
70
+ checked-in file, proves the config and active contract are unchanged, and emits
71
+ text, JSON, and JUnit reports under `tmp/safe-action-team-ci/`. Its static gate
72
+ covers the exact proposal effect, trusted scope, evidence requirement, approval
73
+ policy, operator separation, kept-out fields, argument bounds, and conflict
74
+ guard. It performs no approval, activation, source read, or source mutation.
75
+
76
+ The same gate runs in
77
+ [`safe-action-ci.yml`](https://github.com/Synapsor/Synapsor-Runner/blob/main/.github/workflows/safe-action-ci.yml).
78
+ CI adds a disposable PostgreSQL service and runs the generated allowed-effect,
79
+ cross-tenant-denial, and source-unchanged assertions. It uses an amount above
80
+ the auto-approval threshold, performs no approval or apply, and requires source
81
+ rows and source receipt state to remain equivalent before and after the live
82
+ suite.
83
+ For a real staging preview, each developer supplies their own
84
+ `PLAN_CREDIT_POSTGRES_READ_URL`, `PLAN_CREDIT_POSTGRES_WRITE_URL`,
85
+ `SYNAPSOR_TENANT_ID`, and `SYNAPSOR_PRINCIPAL` outside Git, then follows the
86
+ Quick Start below. Cursor receives the same proposal-only tools; activation,
87
+ approval, and apply remain outside the agent.
88
+
53
89
  ## Quick Start
54
90
 
55
91
  ```bash
@@ -199,9 +235,19 @@ shape without guessing which state should change.
199
235
  Copy the matching template from [`mcp-client-examples/`](mcp-client-examples/):
200
236
 
201
237
  - Claude Desktop: `claude-desktop.json`
238
+ - Claude Code: `claude-code.sh`
239
+ - Codex: `codex.config.toml`
202
240
  - Cursor project/global: `cursor-project.mcp.json`, `cursor-global.mcp.json`
241
+ - VS Code: `vscode.mcp.json`
203
242
  - OpenAI Agents SDK: stdio and Streamable HTTP TypeScript examples
204
- - generic MCP: stdio and Streamable HTTP JSON examples
243
+ - LangChain/LangGraph: `langchain.mjs`
244
+ - Google ADK: `google-adk.py`
245
+ - LlamaIndex: `llamaindex.py`
246
+ - generic MCP: stdio and Streamable HTTP JSON and JavaScript examples
247
+
248
+ See [Client And Framework Recipes](../../docs/client-recipes.md) for current
249
+ official references, exact proposal input, evidence labels, and the mechanical
250
+ verification gate.
205
251
 
206
252
  For Streamable HTTP, run:
207
253
 
@@ -0,0 +1,23 @@
1
+ # Support Plan Credit MCP Recipes
2
+
3
+ These files connect different MCP hosts and frameworks to the same reviewed
4
+ Runner tools:
5
+
6
+ - `support.inspect_customer`
7
+ - `support.propose_plan_credit`
8
+
9
+ Every proposal example uses:
10
+
11
+ ```json
12
+ {
13
+ "customer_id": "CUS-3001",
14
+ "credit_cents": 2500,
15
+ "reason": "SLA outage ticket SUP-481"
16
+ }
17
+ ```
18
+
19
+ The expected call result has `source_database_changed: false`. Human review,
20
+ approval, and apply stay outside every model-facing recipe.
21
+
22
+ See [Client And Framework Recipes](../../../docs/client-recipes.md) for setup,
23
+ evidence labels, official references, and verification commands.