@synapsor/runner 1.5.3 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -2
- package/README.md +131 -130
- package/dist/authoring.mjs +405 -9
- package/dist/cli.d.ts.map +1 -1
- package/dist/local-ui.d.ts +2 -0
- package/dist/local-ui.d.ts.map +1 -1
- package/dist/runner.mjs +19532 -14246
- package/dist/runtime.mjs +10856 -8495
- package/dist/shadow.mjs +101 -1
- package/docs/README.md +14 -6
- package/docs/aggregate-reads.md +22 -0
- package/docs/auto-boundary-and-scoped-explore.md +338 -0
- package/docs/capability-authoring.md +36 -1
- package/docs/cloud-mode.md +18 -0
- package/docs/conformance.md +16 -0
- package/docs/current-scope.md +55 -28
- package/docs/cursor-plugin.md +20 -3
- package/docs/database-enforced-scope.md +8 -0
- package/docs/dsl-reference.md +123 -4
- package/docs/getting-started-own-database.md +42 -35
- package/docs/guarded-crud-writeback.md +10 -1
- package/docs/http-mcp.md +222 -207
- package/docs/limitations.md +33 -9
- package/docs/local-mode.md +5 -2
- package/docs/mcp-client-setup.md +24 -11
- package/docs/mcp-clients.md +10 -4
- package/docs/openai-agents-sdk.md +16 -2
- package/docs/production.md +72 -7
- package/docs/release-notes.md +77 -3
- package/docs/runner-bundles.md +7 -2
- package/docs/runner-config-reference.md +96 -6
- package/docs/running-a-runner-fleet.md +42 -13
- package/docs/schema-api-candidates.md +28 -1
- package/docs/security-boundary.md +38 -3
- package/docs/store-lifecycle.md +93 -5
- package/docs/troubleshooting-first-run.md +98 -0
- package/examples/auto-boundary-churn/README.md +23 -0
- package/examples/auto-boundary-churn/app/page.tsx +8 -0
- package/examples/auto-boundary-churn/docker-compose.yml +16 -0
- package/examples/auto-boundary-churn/package.json +18 -0
- package/examples/auto-boundary-churn/prisma/schema.prisma +36 -0
- package/examples/auto-boundary-churn/seed/postgres.sql +126 -0
- package/examples/openai-agents-http/README.md +10 -4
- package/examples/openai-agents-http/agent.py +2 -2
- package/examples/runner-fleet/Dockerfile +7 -2
- package/examples/runner-fleet/README.md +11 -7
- package/examples/runner-fleet/docker-compose.yml +4 -4
- package/examples/runner-fleet/mint-dev-token.mjs +1 -1
- package/examples/runner-fleet/start-tls-runner.sh +21 -0
- package/examples/runner-fleet/synapsor.runner.json +27 -1
- package/examples/support-plan-credit/README.md +6 -1
- package/examples/support-plan-credit/mcp-client-examples/generic-streamable-http.json +4 -1
- package/examples/support-plan-credit/mcp-client-examples/openai-agents-streamable-http.ts +4 -0
- package/examples/support-plan-credit/synapsor.contract.json +0 -3
- package/fixtures/compatibility/published-1.5.4/manifest.json +76 -0
- package/fixtures/compatibility/published-1.5.4/sources/packages/dsl/examples/aggregate-read.synapsor.sql +21 -0
- package/fixtures/compatibility/published-1.5.4/sources/packages/dsl/examples/billing-late-fee.synapsor.sql +56 -0
- package/fixtures/compatibility/published-1.5.4/sources/packages/dsl/examples/bounded-set-multi-term.synapsor.sql +30 -0
- package/fixtures/compatibility/published-1.5.4/sources/packages/dsl/examples/principal-row-scope.synapsor.sql +23 -0
- package/fixtures/compatibility/published-1.5.4/sources/packages/spec/fixtures/conformance/aggregate-read/contract.json +119 -0
- package/fixtures/compatibility/published-1.5.4/sources/packages/spec/fixtures/conformance/approval-quorum/contract.json +44 -0
- package/fixtures/compatibility/published-1.5.4/sources/packages/spec/fixtures/conformance/bounded-set-threats/contract.json +115 -0
- package/fixtures/compatibility/published-1.5.4/sources/packages/spec/fixtures/conformance/principal-row-scope/contract.json +78 -0
- package/fixtures/compatibility/published-1.5.4/sources/packages/spec/fixtures/conformance/proposal-capability/contract.json +101 -0
- package/fixtures/compatibility/published-1.5.4/sources/packages/spec/fixtures/conformance/reversible-change-sets/contract.json +98 -0
- package/fixtures/compatibility/published-1.5.4/sources/packages/spec/fixtures/valid/basic-read.contract.json +60 -0
- package/llms.txt +37 -0
- package/package.json +7 -4
- package/schemas/synapsor.runner.schema.json +98 -1
package/dist/shadow.mjs
CHANGED
|
@@ -1908,6 +1908,11 @@ var ProposalStore = class {
|
|
|
1908
1908
|
const rows = this.db.prepare(query.sql).all(...query.params);
|
|
1909
1909
|
return rows.map((row) => rowToProposal(row)).filter((proposal) => proposal !== void 0);
|
|
1910
1910
|
}
|
|
1911
|
+
countProposals(filters = {}) {
|
|
1912
|
+
const query = buildProposalCountQuery(filters);
|
|
1913
|
+
const row = this.db.prepare(query.sql).get(...query.params);
|
|
1914
|
+
return isRecord(row) ? Number(row.count ?? 0) : 0;
|
|
1915
|
+
}
|
|
1911
1916
|
listEvidenceBundles(filters = {}) {
|
|
1912
1917
|
const query = buildEvidenceQuery(filters);
|
|
1913
1918
|
const rows = this.db.prepare(query.sql).all(...query.params);
|
|
@@ -1934,6 +1939,19 @@ var ProposalStore = class {
|
|
|
1934
1939
|
const proposalId = replayId.startsWith(prefix) ? replayId.slice(prefix.length) : replayId;
|
|
1935
1940
|
return this.replay(proposalId);
|
|
1936
1941
|
}
|
|
1942
|
+
getStoredReplay(replayId) {
|
|
1943
|
+
const row = this.db.prepare("SELECT * FROM replay_records WHERE replay_id = ?").get(replayId);
|
|
1944
|
+
return rowToStoredReplay(row);
|
|
1945
|
+
}
|
|
1946
|
+
getStoredReplayForProposal(proposalId) {
|
|
1947
|
+
const row = this.db.prepare(`
|
|
1948
|
+
SELECT * FROM replay_records
|
|
1949
|
+
WHERE proposal_id = ?
|
|
1950
|
+
ORDER BY created_at DESC, replay_id DESC
|
|
1951
|
+
LIMIT 1
|
|
1952
|
+
`).get(proposalId);
|
|
1953
|
+
return rowToStoredReplay(row);
|
|
1954
|
+
}
|
|
1937
1955
|
proposalIdForEvidence(evidenceBundleId) {
|
|
1938
1956
|
const evidence = this.getEvidenceBundle(evidenceBundleId);
|
|
1939
1957
|
if (evidence?.proposal_id) return evidence.proposal_id;
|
|
@@ -2236,6 +2254,19 @@ var ProposalStore = class {
|
|
|
2236
2254
|
});
|
|
2237
2255
|
return job;
|
|
2238
2256
|
}
|
|
2257
|
+
getWritebackJob(writebackJobId) {
|
|
2258
|
+
return rowToWritebackJob(this.db.prepare("SELECT * FROM writeback_jobs WHERE writeback_job_id = ?").get(writebackJobId));
|
|
2259
|
+
}
|
|
2260
|
+
listWritebackJobs(options = {}) {
|
|
2261
|
+
const clauses = [];
|
|
2262
|
+
const values = [];
|
|
2263
|
+
if (options.proposal_id) {
|
|
2264
|
+
clauses.push("proposal_id = ?");
|
|
2265
|
+
values.push(options.proposal_id);
|
|
2266
|
+
}
|
|
2267
|
+
const limit = Math.min(Math.max(options.limit ?? 100, 1), 1e3);
|
|
2268
|
+
return this.db.prepare(`SELECT * FROM writeback_jobs${clauses.length ? ` WHERE ${clauses.join(" AND ")}` : ""} ORDER BY created_at ASC, writeback_job_id ASC LIMIT ?`).all(...values, limit).map(rowToWritebackJob).filter((job) => Boolean(job));
|
|
2269
|
+
}
|
|
2239
2270
|
claimWritebackIntent(jobInput, runnerId) {
|
|
2240
2271
|
const job = parseWritebackJob(jobInput);
|
|
2241
2272
|
const proposal = this.requireProposal(job.proposal_id);
|
|
@@ -4059,6 +4090,19 @@ function inWhere(column, values) {
|
|
|
4059
4090
|
};
|
|
4060
4091
|
}
|
|
4061
4092
|
function buildProposalQuery(filters) {
|
|
4093
|
+
const { clauses, params } = proposalQueryParts(filters);
|
|
4094
|
+
const where = clauses.length ? ` WHERE ${clauses.join(" AND ")}` : "";
|
|
4095
|
+
return {
|
|
4096
|
+
sql: `SELECT * FROM proposals${where} ORDER BY created_at DESC, proposal_id DESC${filters.limit ? " LIMIT ?" : ""}`,
|
|
4097
|
+
params: filters.limit ? [...params, filters.limit] : params
|
|
4098
|
+
};
|
|
4099
|
+
}
|
|
4100
|
+
function buildProposalCountQuery(filters) {
|
|
4101
|
+
const { clauses, params } = proposalQueryParts(filters);
|
|
4102
|
+
const where = clauses.length ? ` WHERE ${clauses.join(" AND ")}` : "";
|
|
4103
|
+
return { sql: `SELECT COUNT(*) AS count FROM proposals${where}`, params };
|
|
4104
|
+
}
|
|
4105
|
+
function proposalQueryParts(filters) {
|
|
4062
4106
|
const clauses = [];
|
|
4063
4107
|
const params = [];
|
|
4064
4108
|
addEqual(clauses, params, "proposal_id", filters.proposal);
|
|
@@ -4070,7 +4114,7 @@ function buildProposalQuery(filters) {
|
|
|
4070
4114
|
addEqual(clauses, params, "action", filters.capability ?? filters.action);
|
|
4071
4115
|
addObjectFilter(clauses, params, "business_object", "source_table", "object_id", filters.objectType, filters.objectId);
|
|
4072
4116
|
addTimeRange(clauses, params, "created_at", filters.from, filters.to);
|
|
4073
|
-
return
|
|
4117
|
+
return { clauses, params };
|
|
4074
4118
|
}
|
|
4075
4119
|
function buildEvidenceQuery(filters) {
|
|
4076
4120
|
const clauses = [];
|
|
@@ -4652,6 +4696,62 @@ function rowToReceipt(row) {
|
|
|
4652
4696
|
source_table: row.source_table == null ? void 0 : String(row.source_table)
|
|
4653
4697
|
};
|
|
4654
4698
|
}
|
|
4699
|
+
function rowToWritebackJob(row) {
|
|
4700
|
+
if (!isRecord(row)) return void 0;
|
|
4701
|
+
let payload;
|
|
4702
|
+
try {
|
|
4703
|
+
payload = JSON.parse(String(row.job_json));
|
|
4704
|
+
} catch {
|
|
4705
|
+
throw new ProposalStoreError("WRITEBACK_JOB_CORRUPT", `writeback job ${String(row.writeback_job_id)} payload is not valid JSON`);
|
|
4706
|
+
}
|
|
4707
|
+
if (!isRecord(payload)) {
|
|
4708
|
+
throw new ProposalStoreError("WRITEBACK_JOB_CORRUPT", `writeback job ${String(row.writeback_job_id)} payload is not an object`);
|
|
4709
|
+
}
|
|
4710
|
+
const handler = payload.schema_version === "synapsor.handler-writeback.v1";
|
|
4711
|
+
let normalizedJob;
|
|
4712
|
+
if (!handler) {
|
|
4713
|
+
try {
|
|
4714
|
+
normalizedJob = parseWritebackJob(payload);
|
|
4715
|
+
} catch {
|
|
4716
|
+
throw new ProposalStoreError("WRITEBACK_JOB_CORRUPT", `writeback job ${String(row.writeback_job_id)} payload is not a supported writeback protocol`);
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
const payloadJobId = handler ? payload.writeback_job_id : normalizedJob?.job_id;
|
|
4720
|
+
const payloadProposalId = handler ? payload.proposal_id : normalizedJob?.proposal_id;
|
|
4721
|
+
const payloadProposalHash = handler ? payload.proposal_hash : normalizedJob?.approval_id;
|
|
4722
|
+
if (payloadJobId !== String(row.writeback_job_id) || payloadProposalId !== String(row.proposal_id) || payloadProposalHash !== String(row.proposal_hash)) {
|
|
4723
|
+
throw new ProposalStoreError("WRITEBACK_JOB_CORRUPT", `writeback job ${String(row.writeback_job_id)} index fields do not match its payload`);
|
|
4724
|
+
}
|
|
4725
|
+
return {
|
|
4726
|
+
writeback_job_id: String(row.writeback_job_id),
|
|
4727
|
+
proposal_id: String(row.proposal_id),
|
|
4728
|
+
proposal_hash: String(row.proposal_hash),
|
|
4729
|
+
status: String(row.status),
|
|
4730
|
+
kind: handler ? "app_handler" : "direct_sql",
|
|
4731
|
+
payload,
|
|
4732
|
+
...normalizedJob ? { normalized_job: normalizedJob } : {},
|
|
4733
|
+
created_at: String(row.created_at),
|
|
4734
|
+
updated_at: String(row.updated_at)
|
|
4735
|
+
};
|
|
4736
|
+
}
|
|
4737
|
+
function rowToStoredReplay(row) {
|
|
4738
|
+
if (!isRecord(row)) return void 0;
|
|
4739
|
+
let payload;
|
|
4740
|
+
try {
|
|
4741
|
+
payload = JSON.parse(String(row.payload_json));
|
|
4742
|
+
} catch {
|
|
4743
|
+
throw new ProposalStoreError("REPLAY_RECORD_CORRUPT", `replay ${String(row.replay_id)} payload is not valid JSON`);
|
|
4744
|
+
}
|
|
4745
|
+
if (!isRecord(payload) || !isRecord(payload.proposal)) {
|
|
4746
|
+
throw new ProposalStoreError("REPLAY_RECORD_CORRUPT", `replay ${String(row.replay_id)} payload is not a supported replay record`);
|
|
4747
|
+
}
|
|
4748
|
+
const replayId = String(row.replay_id);
|
|
4749
|
+
const proposalId = String(row.proposal_id);
|
|
4750
|
+
if (payload.replay_id !== replayId || payload.proposal.proposal_id !== proposalId || !Array.isArray(payload.approvals) || !Array.isArray(payload.events) || !Array.isArray(payload.receipts) || !Array.isArray(payload.query_audit) || !Array.isArray(payload.evidence) || typeof payload.generated_at !== "string") {
|
|
4751
|
+
throw new ProposalStoreError("REPLAY_RECORD_CORRUPT", `replay ${replayId} index fields do not match its payload`);
|
|
4752
|
+
}
|
|
4753
|
+
return payload;
|
|
4754
|
+
}
|
|
4655
4755
|
function rowToWritebackIntent(row) {
|
|
4656
4756
|
if (!isRecord(row)) return void 0;
|
|
4657
4757
|
const status = String(row.status);
|
package/docs/README.md
CHANGED
|
@@ -47,10 +47,14 @@ no-database demo, wire your database, then read deeper concepts.
|
|
|
47
47
|
|
|
48
48
|
## 04 Connect Your DB
|
|
49
49
|
|
|
50
|
+
- [Auto Boundary, Scoped Explore, And
|
|
51
|
+
Protect](auto-boundary-and-scoped-explore.md): deterministically inspect a
|
|
52
|
+
whole staging application, review one digest-bound boundary, ask bounded row
|
|
53
|
+
and PM-style aggregate questions in Cursor, and turn a useful query into a
|
|
54
|
+
disabled named production capability.
|
|
50
55
|
- [Connect Your Own Database](getting-started-own-database.md): inspect a
|
|
51
|
-
staging Postgres/MySQL database
|
|
52
|
-
|
|
53
|
-
activation report.
|
|
56
|
+
staging Postgres/MySQL database through the new whole-schema path or an
|
|
57
|
+
established one-object/headless route.
|
|
54
58
|
- [Use Your Own Database](use-your-own-database.md): short entry point that
|
|
55
59
|
links to the canonical own-database guide.
|
|
56
60
|
- [Fresh-Developer Usability Protocol](fresh-developer-usability.md): the
|
|
@@ -72,6 +76,10 @@ no-database demo, wire your database, then read deeper concepts.
|
|
|
72
76
|
|
|
73
77
|
## 05 Generate Capabilities
|
|
74
78
|
|
|
79
|
+
- [Auto Boundary, Scoped Explore, And
|
|
80
|
+
Protect](auto-boundary-and-scoped-explore.md): generate disabled public DSL
|
|
81
|
+
from deterministic schema/ORM/OpenAPI evidence, then Protect a reviewed
|
|
82
|
+
exploratory plan without giving the model SQL or activation authority.
|
|
75
83
|
- [Connect Your Own
|
|
76
84
|
Database](getting-started-own-database.md#draft-another-safe-action-with-a-coding-agent):
|
|
77
85
|
describe one action, let a coding agent complete only the restricted
|
|
@@ -101,8 +109,8 @@ no-database demo, wire your database, then read deeper concepts.
|
|
|
101
109
|
- [Agent Effect Regression](effect-regression.md): provider-neutral,
|
|
102
110
|
propose-only fixtures that catch changed capability calls, business diffs,
|
|
103
111
|
policy outcomes, tenant handling, and hidden-field behavior.
|
|
104
|
-
- [Bounded Aggregate Reads](aggregate-reads.md): fixed scalar
|
|
105
|
-
|
|
112
|
+
- [Bounded Aggregate Reads](aggregate-reads.md): fixed production scalar
|
|
113
|
+
aggregates plus the separate authoring-only reviewed aggregate Explore path.
|
|
106
114
|
|
|
107
115
|
## 06 Serve MCP
|
|
108
116
|
|
|
@@ -172,7 +180,7 @@ no-database demo, wire your database, then read deeper concepts.
|
|
|
172
180
|
|
|
173
181
|
## 10 Concepts
|
|
174
182
|
|
|
175
|
-
- [Current Scope](current-scope.md): compact
|
|
183
|
+
- [Current Scope](current-scope.md): compact current 1.x scope summary.
|
|
176
184
|
- [Current Limitations](limitations.md): intentional safety limits.
|
|
177
185
|
- [Production-Candidate Guide](production.md): single-node and bounded-fleet
|
|
178
186
|
OSS deployment scope, database roles, receipt grants, restart
|
package/docs/aggregate-reads.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Bounded Aggregate Reads
|
|
2
2
|
|
|
3
|
+
There are two distinct aggregate surfaces:
|
|
4
|
+
|
|
5
|
+
1. A fixed named `aggregate_read` capability, described on this page, is
|
|
6
|
+
production-capable and returns one contract-authored scalar.
|
|
7
|
+
2. Runner 1.6.0 Scoped Aggregate Explore is a temporary local development/
|
|
8
|
+
staging authoring tool. It accepts only a typed plan inside a human-activated
|
|
9
|
+
analytical boundary, supports reviewed dimensions and time buckets, and
|
|
10
|
+
must be converted through Protect into a named production capability.
|
|
11
|
+
|
|
12
|
+
Neither surface accepts SQL strings or arbitrary identifiers. Read
|
|
13
|
+
[Auto Boundary, Scoped Explore, And
|
|
14
|
+
Protect](auto-boundary-and-scoped-explore.md) for the second path.
|
|
15
|
+
|
|
3
16
|
An `aggregate_read` capability returns one reviewed scalar rather than source
|
|
4
17
|
rows. It is intended for questions such as a tenant-scoped overdue balance
|
|
5
18
|
total where exposing individual records would be unnecessary.
|
|
@@ -47,3 +60,12 @@ retryable unavailable result without exposing a driver error. Minimum-group
|
|
|
47
60
|
suppression reduces single-record inference; it does not solve every statistical
|
|
48
61
|
inference risk. Review the underlying view, database role, and aggregation
|
|
49
62
|
policy as well.
|
|
63
|
+
|
|
64
|
+
Scoped Aggregate Explore reuses and extends this suppression machinery. Its
|
|
65
|
+
reviewed boundary additionally fixes aggregate-safe measures,
|
|
66
|
+
`count_distinct` identifiers, dimensions, day/week/month buckets, typed
|
|
67
|
+
filters, optional one-hop proven many-to-one relationships, maximum groups,
|
|
68
|
+
response/query/rate limits, and durable extraction/differencing budgets. A
|
|
69
|
+
field may be approved for `count_distinct` while its raw values remain hidden.
|
|
70
|
+
Production receives only the protected named capability; broad Explore is
|
|
71
|
+
absent from production `tools/list`.
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# Auto Boundary, Scoped Explore, And Protect
|
|
2
|
+
|
|
3
|
+
Runner 1.6.0 adds a deterministic authoring path for a real application:
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
Connect staging
|
|
7
|
+
-> draft the boundary
|
|
8
|
+
-> review and activate its exact digest
|
|
9
|
+
-> explore through two bounded MCP tools
|
|
10
|
+
-> protect a useful query
|
|
11
|
+
-> activate the named capability
|
|
12
|
+
-> disable exploration
|
|
13
|
+
-> serve only named production tools
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This path does not give the model SQL, database credentials, tenant identity,
|
|
17
|
+
approval, activation, or commit authority. Auto Boundary and Protect use no
|
|
18
|
+
LLM. Existing hand-authored contracts and established onboarding commands
|
|
19
|
+
continue to work without this feature.
|
|
20
|
+
|
|
21
|
+
## Start With Staging
|
|
22
|
+
|
|
23
|
+
Use a dedicated SELECT-only, non-owner database role. Keep database-level
|
|
24
|
+
controls underneath Runner. PostgreSQL deployments should use forced row-level
|
|
25
|
+
security (RLS) where possible; MySQL deployments should use restricted views or
|
|
26
|
+
tenant-bound credentials.
|
|
27
|
+
|
|
28
|
+
Export the connection and trusted context in the process that Cursor will use:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
export DATABASE_URL='postgresql://runner_reader:REPLACE_ME@127.0.0.1:5432/app'
|
|
32
|
+
export SYNAPSOR_TENANT_ID='acme'
|
|
33
|
+
export SYNAPSOR_PRINCIPAL='pm-1'
|
|
34
|
+
|
|
35
|
+
npx -y @synapsor/runner start --from-env DATABASE_URL --schema public
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
A fresh interactive invocation with no existing config, selector, or automation
|
|
39
|
+
input enters Auto Boundary. It scans the whole selected schema and opens the
|
|
40
|
+
secured loopback Workbench. The initial npm download is not part of Runner's
|
|
41
|
+
measured onboarding time.
|
|
42
|
+
|
|
43
|
+
For scripts or CI, draft without prompts or a browser:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
synapsor-runner boundary draft \
|
|
47
|
+
--from-env DATABASE_URL \
|
|
48
|
+
--schema public \
|
|
49
|
+
--project-root . \
|
|
50
|
+
--json
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Established `--table`, `--answers`, `onboard db`, `--mode`, JSON, and
|
|
54
|
+
noninteractive routes keep their previous one-object behavior.
|
|
55
|
+
|
|
56
|
+
## What Auto Boundary Reads
|
|
57
|
+
|
|
58
|
+
Runner builds one deterministic evidence graph from:
|
|
59
|
+
|
|
60
|
+
- database catalogs, keys, constraints, grants, ownership, RLS, triggers, and
|
|
61
|
+
cascades;
|
|
62
|
+
- statically parsed Prisma schema files;
|
|
63
|
+
- statically parsed Drizzle schema files;
|
|
64
|
+
- OpenAPI documents;
|
|
65
|
+
- existing Synapsor DSL, canonical JSON, and TypeScript definitions.
|
|
66
|
+
|
|
67
|
+
It does not import or execute adopter code. Database, ORM, and API comments are
|
|
68
|
+
naming evidence only. They never grant field access, trusted scope, write
|
|
69
|
+
authority, approval, or activation.
|
|
70
|
+
|
|
71
|
+
Runner can determine structure such as primary keys, foreign keys, enums,
|
|
72
|
+
likely version columns, and possible deduplication keys. It cannot determine
|
|
73
|
+
business authority such as:
|
|
74
|
+
|
|
75
|
+
- which column is the real tenant boundary;
|
|
76
|
+
- whether a principal may see every tenant row or only assigned rows;
|
|
77
|
+
- whether a field is appropriate for an agent;
|
|
78
|
+
- which state transition, refund, credit, or delete is permitted;
|
|
79
|
+
- which evidence, bounds, reviewers, or auto-approval policy are required.
|
|
80
|
+
|
|
81
|
+
Those decisions remain explicit human review.
|
|
82
|
+
|
|
83
|
+
## Generated Files
|
|
84
|
+
|
|
85
|
+
Auto Boundary writes disabled review artifacts:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
synapsor/generated/
|
|
89
|
+
domain.synapsor.sql
|
|
90
|
+
read-capabilities.synapsor.sql
|
|
91
|
+
synapsor.candidate.contract.json
|
|
92
|
+
exploration-boundary.draft.json
|
|
93
|
+
generation-review.json
|
|
94
|
+
contract-tests.json
|
|
95
|
+
REVIEW.md
|
|
96
|
+
|
|
97
|
+
.synapsor/
|
|
98
|
+
generation-lock.json
|
|
99
|
+
review-report.json
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The `.synapsor.sql` files compile through `@synapsor/dsl` into the canonical
|
|
103
|
+
`@synapsor/spec` JSON contract. The generation lock records non-secret
|
|
104
|
+
fingerprints of the inspected schema, compiler/spec version, exact database
|
|
105
|
+
role, grants, ownership, and RLS posture. Generated read drafts and action
|
|
106
|
+
candidates start disabled. Auto Boundary never replaces an active contract.
|
|
107
|
+
|
|
108
|
+
No source rows, credentials, tenant values, or principal values are written to
|
|
109
|
+
these files.
|
|
110
|
+
|
|
111
|
+
## Review The Boundary
|
|
112
|
+
|
|
113
|
+
The Workbench requires a human to narrow and confirm:
|
|
114
|
+
|
|
115
|
+
- development or staging deployment profile;
|
|
116
|
+
- trusted tenant and principal bindings supplied outside model arguments;
|
|
117
|
+
- included resources and one-hop relationships;
|
|
118
|
+
- selectable fields;
|
|
119
|
+
- filterable fields and allowed operators;
|
|
120
|
+
- sortable and groupable fields;
|
|
121
|
+
- aggregate-safe numeric measures;
|
|
122
|
+
- identifiers allowed only for `count_distinct`;
|
|
123
|
+
- timestamp fields and permitted day/week/month buckets;
|
|
124
|
+
- kept-out fields;
|
|
125
|
+
- counted entity and relationship cardinality;
|
|
126
|
+
- minimum cohort size;
|
|
127
|
+
- row, group, measure, dimension, time-range, response, rate, extraction, and
|
|
128
|
+
differencing budgets;
|
|
129
|
+
- the current schema fingerprint and exact database-role/RLS posture.
|
|
130
|
+
|
|
131
|
+
Kept-out fields are unavailable for selection, filtering, sorting, grouping,
|
|
132
|
+
joining, aggregation, and `count_distinct`.
|
|
133
|
+
|
|
134
|
+
Raw visibility and aggregate use are separate permissions. A reviewer may
|
|
135
|
+
allow `count_distinct(customer_id)` while keeping every `customer_id` value out
|
|
136
|
+
of results.
|
|
137
|
+
|
|
138
|
+
Workbench activation requires every generated decision, the operator identity,
|
|
139
|
+
and the exact confirmation:
|
|
140
|
+
|
|
141
|
+
```text
|
|
142
|
+
ACTIVATE sha256:...
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The immutable digest covers the reviewed resources, field permissions,
|
|
146
|
+
relationships, scope, role posture, generation lock, compiler/spec version,
|
|
147
|
+
profile, and every query/privacy budget. Model arguments cannot widen it.
|
|
148
|
+
|
|
149
|
+
## Add The Authoring Tools To Cursor
|
|
150
|
+
|
|
151
|
+
After activation, let Runner manage only its own project entry:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
synapsor-runner mcp install cursor \
|
|
155
|
+
--project \
|
|
156
|
+
--authoring \
|
|
157
|
+
--project-root . \
|
|
158
|
+
--yes
|
|
159
|
+
|
|
160
|
+
synapsor-runner mcp status cursor --project
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The Cursor config contains command paths and package identity, not database
|
|
164
|
+
URLs, credential values, tenant values, or principal values. Authoring mode
|
|
165
|
+
uses local stdio and advertises exactly:
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
app.describe_data
|
|
169
|
+
app.explore_data
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`app.describe_data` is bounded and paginated over only the activated resource
|
|
173
|
+
pack. `app.explore_data` accepts a structured plan. Neither tool exposes SQL,
|
|
174
|
+
approval, apply, activation, commit, or revert.
|
|
175
|
+
|
|
176
|
+
## Scoped Row Explore
|
|
177
|
+
|
|
178
|
+
A row plan can select, filter, sort, and limit only fields and operators in the
|
|
179
|
+
activated boundary. Runner injects tenant and principal scope outside model
|
|
180
|
+
arguments and compiles the validated plan into parameterized SQL.
|
|
181
|
+
|
|
182
|
+
Scoped Explore does not accept:
|
|
183
|
+
|
|
184
|
+
- a SQL string or fragment;
|
|
185
|
+
- arbitrary identifiers, functions, expressions, aliases, or subqueries;
|
|
186
|
+
- a model-supplied tenant or principal;
|
|
187
|
+
- unreviewed fields or relationships;
|
|
188
|
+
- model-widened row, byte, time, rate, or extraction limits.
|
|
189
|
+
|
|
190
|
+
## Scoped Aggregate Explore
|
|
191
|
+
|
|
192
|
+
The aggregate surface is a small reviewed analytical cube, not a generic
|
|
193
|
+
analytics database tool. It supports:
|
|
194
|
+
|
|
195
|
+
- `count`;
|
|
196
|
+
- `count_distinct` on explicitly reviewed identifiers;
|
|
197
|
+
- `sum` and `avg` on explicitly reviewed numeric measures;
|
|
198
|
+
- reviewed categorical dimensions;
|
|
199
|
+
- day, week, and month buckets on reviewed timestamps;
|
|
200
|
+
- typed bounded filters;
|
|
201
|
+
- ordering by a returned aggregate;
|
|
202
|
+
- bounded top-N results;
|
|
203
|
+
- at most two reviewed time ranges;
|
|
204
|
+
- one resource by default;
|
|
205
|
+
- at most one inspected, reviewed many-to-one foreign-key path with maximum
|
|
206
|
+
fan-out one.
|
|
207
|
+
|
|
208
|
+
It does not support arbitrary `DISTINCT`, `HAVING`, formulas, window functions,
|
|
209
|
+
unions, nested queries, many-to-many joins, system catalogs, user-defined
|
|
210
|
+
functions, or a general join planner. Scope is enforced independently on every
|
|
211
|
+
participating relation. Runner refuses a plan when cardinality, fan-out,
|
|
212
|
+
counted entity, or scope cannot be proven.
|
|
213
|
+
|
|
214
|
+
Before returning groups, Runner enforces the reviewed minimum cohort size.
|
|
215
|
+
Small groups are suppressed and revealing totals are withheld. Durable
|
|
216
|
+
per-session extraction and differencing budgets block repeated slightly
|
|
217
|
+
different queries that could reconstruct a suppressed cohort. Pagination
|
|
218
|
+
cannot bypass the maximum group count.
|
|
219
|
+
|
|
220
|
+
Results describe changes, comparisons, correlations, and likely contributors.
|
|
221
|
+
They do not establish causation.
|
|
222
|
+
|
|
223
|
+
## Runtime Enforcement
|
|
224
|
+
|
|
225
|
+
Scoped Explore is disabled by default and authoring-only. It starts only when
|
|
226
|
+
all of these are true:
|
|
227
|
+
|
|
228
|
+
- the profile is explicitly `development` or `staging`;
|
|
229
|
+
- the transport is local stdio or secured loopback Workbench traffic;
|
|
230
|
+
- the exact exploration-boundary digest is active;
|
|
231
|
+
- the generation lock and compiler/spec versions are current;
|
|
232
|
+
- the role/grant/ownership/RLS fingerprint still matches;
|
|
233
|
+
- the credential is demonstrably SELECT-only and non-owner;
|
|
234
|
+
- every query also runs in an enforced read-only transaction.
|
|
235
|
+
|
|
236
|
+
Missing, malformed, unknown, and production profiles are treated as
|
|
237
|
+
production. A superuser, relation owner, write-capable role, `BYPASSRLS` role,
|
|
238
|
+
or unverifiable role may inspect metadata with a warning but cannot read source
|
|
239
|
+
rows through Scoped Explore.
|
|
240
|
+
|
|
241
|
+
Shared HTTP, Streamable HTTP, remote, and non-loopback runtimes never register
|
|
242
|
+
or advertise broad Explore tools.
|
|
243
|
+
|
|
244
|
+
## Audit And Temporary Protect State
|
|
245
|
+
|
|
246
|
+
Every successful call records a normalized query audit in
|
|
247
|
+
`.synapsor/local.db`. Audit may retain:
|
|
248
|
+
|
|
249
|
+
- active boundary digest;
|
|
250
|
+
- reviewed resource/relationship aliases;
|
|
251
|
+
- operators and time buckets;
|
|
252
|
+
- keyed hashes of filter literals;
|
|
253
|
+
- timing, suppression decisions, and result-size metadata.
|
|
254
|
+
|
|
255
|
+
It does not retain returned rows/groups, credentials, raw sensitive literals,
|
|
256
|
+
or trusted tenant/principal values.
|
|
257
|
+
|
|
258
|
+
A successful query also creates encrypted, expiring local Protect state. The
|
|
259
|
+
Workbench discovers recent queries itself; developers do not copy opaque
|
|
260
|
+
handles.
|
|
261
|
+
|
|
262
|
+
## Protect This Query
|
|
263
|
+
|
|
264
|
+
Choose the useful query in Workbench. Protect freezes:
|
|
265
|
+
|
|
266
|
+
- resources and reviewed relationship path;
|
|
267
|
+
- counted entity, measures, dimensions, and bucket structure;
|
|
268
|
+
- filters, ordering, top-N, and comparison shape;
|
|
269
|
+
- tenant/principal as trusted bindings;
|
|
270
|
+
- cohort suppression and query/privacy budgets.
|
|
271
|
+
|
|
272
|
+
Reviewed literals remain fixed by default. A human may convert selected
|
|
273
|
+
literals into typed bounded arguments.
|
|
274
|
+
|
|
275
|
+
Protect writes:
|
|
276
|
+
|
|
277
|
+
```text
|
|
278
|
+
synapsor/protected/drafts/analytics__churn_contributors_by_week/
|
|
279
|
+
capability.synapsor.sql
|
|
280
|
+
synapsor.contract.json
|
|
281
|
+
contract-tests.json
|
|
282
|
+
REVIEW.md
|
|
283
|
+
draft.json
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The public DSL compiles into the canonical Spec. The generated capability
|
|
287
|
+
starts disabled and includes positive, scope, suppression, differencing,
|
|
288
|
+
join-safety, deny, drift, and boundary tests. It becomes active only after a
|
|
289
|
+
human reviews and confirms its exact contract digest outside MCP.
|
|
290
|
+
|
|
291
|
+
When activation disables temporary Explore, the named capability remains
|
|
292
|
+
available. Update Cursor from authoring mode to the production config:
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
synapsor-runner mcp install cursor \
|
|
296
|
+
--project \
|
|
297
|
+
--config ./synapsor.runner.json \
|
|
298
|
+
--store ./.synapsor/local.db \
|
|
299
|
+
--yes
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Production then advertises only reviewed named capabilities. It does not
|
|
303
|
+
advertise `app.explore_data`.
|
|
304
|
+
|
|
305
|
+
## Schema Drift
|
|
306
|
+
|
|
307
|
+
Check generated authority against the current database:
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
synapsor-runner boundary status --json
|
|
311
|
+
synapsor-runner boundary diff --json
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Additive schema fields and objects receive no authority. A changed schema,
|
|
315
|
+
database role, grant, ownership, RLS posture, compiler, or canonical Spec
|
|
316
|
+
invalidates the generation lock. Lock-bound generated authority fails closed
|
|
317
|
+
until the operator regenerates, reviews the semantic diff, and activates the
|
|
318
|
+
new exact digest.
|
|
319
|
+
|
|
320
|
+
This drift lifecycle applies only to generated authority explicitly bound to a
|
|
321
|
+
generation lock. Existing manually authored projects without a lock retain
|
|
322
|
+
their previous startup, `doctor`, contract, and tool behavior.
|
|
323
|
+
|
|
324
|
+
## Verify The Reference Journey
|
|
325
|
+
|
|
326
|
+
The packaged synthetic fixture is under `examples/auto-boundary-churn`.
|
|
327
|
+
|
|
328
|
+
From a source checkout:
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
corepack pnpm test:auto-boundary-explore
|
|
332
|
+
corepack pnpm test:auto-boundary-explore:packed
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
The packed gate proves the PostgreSQL + Next.js + Prisma + Cursor-compatible
|
|
336
|
+
MCP + Workbench flow, all aggregate denial/suppression/budget checks, Protect,
|
|
337
|
+
production Explore absence, protected-capability survival, durable redacted
|
|
338
|
+
audit, and an unchanged source database.
|
|
@@ -180,6 +180,35 @@ near-duplicate tools. These checks make breadth drift visible; they do not
|
|
|
180
180
|
change canonical validity or replace human review. See [Contract
|
|
181
181
|
Review](contract-review.md) for exact codes and behavior.
|
|
182
182
|
|
|
183
|
+
## Auto-Generated And Protected Reads
|
|
184
|
+
|
|
185
|
+
For a new application, you do not need to hand-author the initial DSL.
|
|
186
|
+
`start --from-env DATABASE_URL` can deterministically inspect the whole staging
|
|
187
|
+
schema, combine static Prisma/Drizzle/OpenAPI/Synapsor evidence, and emit
|
|
188
|
+
disabled `.synapsor.sql`, canonical JSON, tests, review evidence, and a
|
|
189
|
+
generation lock. It does not sample source rows before activation or use an
|
|
190
|
+
LLM.
|
|
191
|
+
|
|
192
|
+
After a human activates the exact local exploration boundary, Cursor receives
|
|
193
|
+
only `app.describe_data` and `app.explore_data`. A useful typed row or
|
|
194
|
+
aggregate plan can be converted through Protect This Query into a named
|
|
195
|
+
capability. Protect emits public DSL such as `PROTECTED READ ROWS` or
|
|
196
|
+
`PROTECTED READ AGGREGATE`, compiles it to canonical `protected_read`
|
|
197
|
+
authority, and starts it disabled. Exact-digest activation is still outside
|
|
198
|
+
MCP.
|
|
199
|
+
|
|
200
|
+
The named protected capability freezes resources, optional reviewed
|
|
201
|
+
many-to-one relationship, predicates, projection or aggregate measures,
|
|
202
|
+
dimensions, bucket, ordering, result bounds, suppression, and privacy/query
|
|
203
|
+
budgets. Only explicitly selected typed literal positions become arguments.
|
|
204
|
+
Tenant and principal remain trusted bindings.
|
|
205
|
+
|
|
206
|
+
This is additive. Existing DSL, canonical JSON, TypeScript authoring, direct
|
|
207
|
+
Runner config, active contracts, and CI/headless routes do not require Auto
|
|
208
|
+
Boundary, Workbench, generation locks, or schema rescans. See [Auto Boundary,
|
|
209
|
+
Scoped Explore, And Protect](auto-boundary-and-scoped-explore.md) and the
|
|
210
|
+
[protected-read DSL](dsl-reference.md#protected-named-reads).
|
|
211
|
+
|
|
183
212
|
## DSL / JSON Capability Parity
|
|
184
213
|
|
|
185
214
|
The DSL compiles to canonical `@synapsor/spec` JSON. It must not silently weaken
|
|
@@ -196,12 +225,14 @@ reviewed runner JSON capabilities. Current parity:
|
|
|
196
225
|
| arg `enum` | `ARG risk_level STRING ENUM('low', 'medium', 'high') REQUIRED` | 1.4.121 | Supports 1..64 same-type string, number, or boolean values; duplicates and mixed types fail compilation. |
|
|
197
226
|
| proposal `numeric_bounds` | `BOUND column 1..2500`, `BOUND column ..2500`, or `BOUND column 1..` | 0.1.8 | Applies to patched numeric columns. Strict mode warns when a NUMBER arg is patched without arg min/max or a matching `BOUND`. |
|
|
198
227
|
| proposal `transition_guards` | `TRANSITION status ALLOW pending -> approved\|rejected` or `TRANSITION status FROM current_status ALLOW open -> closed` | 0.1.8 | Values are state strings; use `|` for multiple target states. |
|
|
199
|
-
| proposal `conflict_guard` | `CONFLICT GUARD updated_at` | 0.1 |
|
|
228
|
+
| proposal `conflict_guard` | `CONFLICT GUARD updated_at` | 0.1 | Required for ordinary UPDATE authoring. It compiles to an exact version-column check. Omission fails instead of silently weakening concurrency protection. |
|
|
229
|
+
| proposal weak `conflict_guard` | `CONFLICT GUARD WEAK ROW HASH ACKNOWLEDGED` | 1.4.4 | Explicit legacy escape hatch for an ordinary single-row source-DB UPDATE only. It hashes the captured projection and may miss concurrent changes outside that projection. It is rejected for DELETE, reversible writes, bounded sets, and Runner-ledger authority. |
|
|
200
230
|
| proposal `approval` | `APPROVAL ROLE billing_lead` | 0.1 | Local mode records the required role; enforcement is still outside the model-facing MCP tool. |
|
|
201
231
|
| proposal `approval.required_approvals` | `REQUIRE 2 APPROVALS` | 1.1 | Optional 1..10 distinct-reviewer quorum; defaults to 1. |
|
|
202
232
|
| proposal `writeback` | `WRITEBACK DIRECT SQL`, `WRITEBACK APP HANDLER EXECUTOR name`, `WRITEBACK CLOUD WORKER`, `WRITEBACK NONE` | 0.1.7 | Handler URLs/tokens stay in `synapsor.runner.json`; contracts carry only the handler name. |
|
|
203
233
|
| proposal `reversibility` | `REVERSIBLE` | 1.4 | Direct SQL only. Captures a bounded inverse after unambiguous apply; operator `revert` creates a new independently approved proposal. |
|
|
204
234
|
| evidence options | `REQUIRE EVIDENCE` | 0.1 | Detailed evidence sources/handle prefixes are not expressible in DSL yet; use embedded JSON or generated contract JSON for those. |
|
|
235
|
+
| capability `protected_read` | `PROTECTED READ ROWS\|AGGREGATE` plus `BOUNDARY DIGEST`, `GENERATION LOCK`, reviewed filters/read or aggregate clauses, and `PROTECTED LIMITS` | 1.5 | Optional default-deny named authority produced by Protect. Existing contracts without this field retain their previous normalization and digest. |
|
|
205
236
|
|
|
206
237
|
## Direct Runner Config Path
|
|
207
238
|
|
|
@@ -523,6 +554,8 @@ Canonical Synapsor names use dots, such as `billing.inspect_invoice`. Some
|
|
|
523
554
|
clients require function-safe names. Use:
|
|
524
555
|
|
|
525
556
|
```bash
|
|
557
|
+
export SYNAPSOR_RUNNER_HTTP_TOKEN="$(node -e 'process.stdout.write(require("node:crypto").randomBytes(32).toString("base64url"))')"
|
|
558
|
+
|
|
526
559
|
synapsor-runner mcp serve-streamable-http \
|
|
527
560
|
--config ./synapsor.runner.json \
|
|
528
561
|
--store ./.synapsor/local.db \
|
|
@@ -533,6 +566,8 @@ synapsor-runner mcp serve-streamable-http \
|
|
|
533
566
|
The model sees aliases such as `billing__inspect_invoice`. Runner includes the
|
|
534
567
|
canonical name in tool metadata and descriptions so audit/replay still use the
|
|
535
568
|
real capability name.
|
|
569
|
+
This loopback token is opaque endpoint access, not tenant identity. See
|
|
570
|
+
[HTTP MCP](http-mcp.md) before using a network listener.
|
|
536
571
|
|
|
537
572
|
## Why Not `execute_sql`
|
|
538
573
|
|
package/docs/cloud-mode.md
CHANGED
|
@@ -19,6 +19,24 @@ The local Runner still keeps database credentials in your environment. MCP
|
|
|
19
19
|
client config snippets contain command paths and Runner arguments, not database
|
|
20
20
|
URLs or write credentials.
|
|
21
21
|
|
|
22
|
+
Keep Cloud and MCP credentials distinct:
|
|
23
|
+
|
|
24
|
+
| Credential | Purpose | Issuer/provisioner | Never substitutes for |
|
|
25
|
+
| --- | --- | --- | --- |
|
|
26
|
+
| Cloud user session | Human control-panel/CLI identity and subscription/RBAC checks | Synapsor Cloud identity system | MCP tenant identity or database access |
|
|
27
|
+
| Cloud service/API key | Contract push and scoped Cloud administration | Synapsor Cloud | MCP endpoint access |
|
|
28
|
+
| Source-scoped Runner token | Runner registration, heartbeat, lease, activity, and result calls for one Cloud source | Synapsor Cloud Connect Runner flow | Model-facing MCP access or operator approval |
|
|
29
|
+
| Opaque MCP endpoint token | Loopback or explicitly single-tenant HTTP endpoint access | Customer operator/secret manager | User/tenant identity |
|
|
30
|
+
| Signed MCP access JWT | Shared HTTP client identity and verified tenant/principal claims | Customer identity provider/authorization server | Cloud subscription credential or DB role |
|
|
31
|
+
| Operator credential | Activation, approval, apply, reconcile, dead-letter, and revert authority | Customer operator identity system | Model-facing MCP capability |
|
|
32
|
+
| Database/handler credential | Local read or trusted post-approval effect | Customer secret manager/database/application | Cloud or MCP authentication |
|
|
33
|
+
|
|
34
|
+
Cloud-linked mode does not require networked MCP: a desktop client can still
|
|
35
|
+
launch the local Runner over stdio. If the local Runner exposes Streamable HTTP,
|
|
36
|
+
the same [HTTP MCP](http-mcp.md) channel and identity profiles apply. Cloud API
|
|
37
|
+
keys and source-scoped Runner tokens are never accepted as shortcuts for MCP
|
|
38
|
+
session authentication.
|
|
39
|
+
|
|
22
40
|
## Trust Boundary
|
|
23
41
|
|
|
24
42
|
```text
|