@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.
- package/CHANGELOG.md +144 -19
- package/README.md +132 -135
- package/dist/authoring.d.ts +23 -0
- package/dist/authoring.d.ts.map +1 -0
- package/dist/authoring.mjs +1318 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -5
- package/dist/local-ui.d.ts +14 -0
- package/dist/local-ui.d.ts.map +1 -1
- package/dist/runner.mjs +18427 -7084
- package/dist/runtime.d.ts +50 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.mjs +15094 -0
- package/dist/shadow.d.ts +36 -0
- package/dist/shadow.d.ts.map +1 -0
- package/dist/shadow.mjs +5262 -0
- package/docs/README.md +44 -5
- package/docs/alternatives.md +122 -0
- package/docs/capability-authoring.md +44 -1
- package/docs/client-recipes.md +218 -0
- package/docs/contract-testing.md +9 -7
- package/docs/cursor-plugin.md +78 -0
- package/docs/database-enforced-scope.md +273 -0
- package/docs/dependency-license-inventory.md +6 -2
- package/docs/doctor.md +17 -0
- package/docs/effect-regression.md +179 -0
- package/docs/fresh-developer-usability.md +110 -0
- package/docs/getting-started-own-database.md +103 -2
- package/docs/host-compatibility.md +59 -0
- package/docs/local-mode.md +12 -7
- package/docs/mcp-apps.md +188 -0
- package/docs/mcp-audit.md +220 -2
- package/docs/mcp-client-setup.md +20 -4
- package/docs/mcp-clients.md +41 -4
- package/docs/oss-vs-cloud.md +3 -0
- package/docs/production.md +8 -0
- package/docs/release-notes.md +138 -14
- package/docs/release-policy.md +9 -5
- package/docs/runner-config-reference.md +25 -0
- package/docs/schema-api-candidates.md +68 -0
- package/docs/security-boundary.md +30 -5
- package/docs/shadow-studies.md +241 -0
- package/docs/troubleshooting-first-run.md +46 -0
- package/examples/mcp-postgres-billing-app-handler/app-handler.mjs +9 -1
- package/examples/mcp-postgres-billing-app-handler/schema.sql +11 -0
- package/examples/mcp-postgres-billing-app-handler/synapsor-handler.mjs +3 -0
- package/examples/raw-sql-vs-synapsor/Makefile +2 -2
- package/examples/raw-sql-vs-synapsor/README.md +6 -4
- package/examples/support-billing-agent/Makefile +5 -1
- package/examples/support-billing-agent/README.md +57 -5
- package/examples/support-billing-agent/app/README.md +6 -0
- package/examples/support-billing-agent/app/contract.ts +28 -0
- package/examples/support-billing-agent/app/effect-adapter.mjs +23 -0
- package/examples/support-billing-agent/app/record-shadow-outcomes.mjs +44 -0
- package/examples/support-billing-agent/db/schema.sql +88 -1
- package/examples/support-billing-agent/db/seed.sql +15 -15
- package/examples/support-billing-agent/scripts/run-demo.sh +4 -0
- package/examples/support-billing-agent/scripts/run-evaluation.sh +63 -0
- package/examples/support-billing-agent/shadow-study/cases.jsonl +6 -0
- package/examples/support-billing-agent/shadow-study/outcomes.jsonl +2 -0
- package/examples/support-billing-agent/synapsor.runner.json +17 -5
- package/examples/support-plan-credit/README.md +48 -2
- package/examples/support-plan-credit/mcp-client-examples/README.md +23 -0
- package/examples/support-plan-credit/mcp-client-examples/claude-code.sh +34 -0
- package/examples/support-plan-credit/mcp-client-examples/codex.config.toml +24 -0
- package/examples/support-plan-credit/mcp-client-examples/generic-stdio.mjs +35 -0
- package/examples/support-plan-credit/mcp-client-examples/generic-streamable-http.mjs +31 -0
- package/examples/support-plan-credit/mcp-client-examples/google-adk.py +40 -0
- package/examples/support-plan-credit/mcp-client-examples/langchain.mjs +29 -0
- package/examples/support-plan-credit/mcp-client-examples/llamaindex.py +36 -0
- package/examples/support-plan-credit/mcp-client-examples/openai-agents-stdio.ts +1 -1
- package/examples/support-plan-credit/mcp-client-examples/openai-agents-streamable-http.ts +1 -1
- package/examples/support-plan-credit/mcp-client-examples/vscode.mcp.json +20 -0
- package/examples/support-plan-credit/synapsor/actions/support.propose_plan_credit.contract-tests.generated.json +221 -0
- package/examples/support-plan-credit/synapsor/actions/support.propose_plan_credit.ts +34 -0
- package/fixtures/effects/changed/eff_support_late_fee_v1.result.json +58 -0
- package/fixtures/effects/dataset.json +8 -0
- package/fixtures/effects/results/eff_support_late_fee_v1.result.json +59 -0
- package/fixtures/effects/support-late-fee.effect.json +71 -0
- package/fixtures/generators/drizzle/malicious.ts +11 -0
- package/fixtures/generators/drizzle/schema.ts +12 -0
- package/fixtures/generators/openapi/external-ref.yaml +15 -0
- package/fixtures/generators/openapi/openapi.yaml +81 -0
- package/fixtures/generators/prisma/schema.prisma +34 -0
- package/fixtures/mcp-audit/README.md +14 -0
- package/fixtures/mcp-audit/cursor-bypass-config.json +38 -0
- package/fixtures/mcp-audit/dangerous-tools-list.json +33 -0
- package/fixtures/mcp-audit/reviewed-proposal-tools-list.json +60 -0
- package/package.json +29 -3
- package/schemas/effect-dataset.schema.json +19 -0
- package/schemas/effect-fixture.schema.json +191 -0
- package/schemas/effect-result.schema.json +52 -0
- package/schemas/mcp-audit-candidates.schema.json +41 -0
- package/schemas/mcp-audit-report.schema.json +203 -0
- package/schemas/schema-candidate-review.schema.json +167 -0
- package/schemas/schema-candidates.schema.json +49 -0
- package/schemas/synapsor.contract-tests.schema.json +1 -1
- 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
|
|
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
|
-
-
|
|
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
|
|
65
|
-
stale-row conflict, and
|
|
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.
|
|
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
|
-
('
|
|
17
|
-
('
|
|
18
|
-
('
|
|
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.
|
|
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
|
-
-
|
|
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.
|