@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.
Files changed (69) hide show
  1. package/CHANGELOG.md +82 -2
  2. package/README.md +131 -130
  3. package/dist/authoring.mjs +405 -9
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/local-ui.d.ts +2 -0
  6. package/dist/local-ui.d.ts.map +1 -1
  7. package/dist/runner.mjs +19532 -14246
  8. package/dist/runtime.mjs +10856 -8495
  9. package/dist/shadow.mjs +101 -1
  10. package/docs/README.md +14 -6
  11. package/docs/aggregate-reads.md +22 -0
  12. package/docs/auto-boundary-and-scoped-explore.md +338 -0
  13. package/docs/capability-authoring.md +36 -1
  14. package/docs/cloud-mode.md +18 -0
  15. package/docs/conformance.md +16 -0
  16. package/docs/current-scope.md +55 -28
  17. package/docs/cursor-plugin.md +20 -3
  18. package/docs/database-enforced-scope.md +8 -0
  19. package/docs/dsl-reference.md +123 -4
  20. package/docs/getting-started-own-database.md +42 -35
  21. package/docs/guarded-crud-writeback.md +10 -1
  22. package/docs/http-mcp.md +222 -207
  23. package/docs/limitations.md +33 -9
  24. package/docs/local-mode.md +5 -2
  25. package/docs/mcp-client-setup.md +24 -11
  26. package/docs/mcp-clients.md +10 -4
  27. package/docs/openai-agents-sdk.md +16 -2
  28. package/docs/production.md +72 -7
  29. package/docs/release-notes.md +77 -3
  30. package/docs/runner-bundles.md +7 -2
  31. package/docs/runner-config-reference.md +96 -6
  32. package/docs/running-a-runner-fleet.md +42 -13
  33. package/docs/schema-api-candidates.md +28 -1
  34. package/docs/security-boundary.md +38 -3
  35. package/docs/store-lifecycle.md +93 -5
  36. package/docs/troubleshooting-first-run.md +98 -0
  37. package/examples/auto-boundary-churn/README.md +23 -0
  38. package/examples/auto-boundary-churn/app/page.tsx +8 -0
  39. package/examples/auto-boundary-churn/docker-compose.yml +16 -0
  40. package/examples/auto-boundary-churn/package.json +18 -0
  41. package/examples/auto-boundary-churn/prisma/schema.prisma +36 -0
  42. package/examples/auto-boundary-churn/seed/postgres.sql +126 -0
  43. package/examples/openai-agents-http/README.md +10 -4
  44. package/examples/openai-agents-http/agent.py +2 -2
  45. package/examples/runner-fleet/Dockerfile +7 -2
  46. package/examples/runner-fleet/README.md +11 -7
  47. package/examples/runner-fleet/docker-compose.yml +4 -4
  48. package/examples/runner-fleet/mint-dev-token.mjs +1 -1
  49. package/examples/runner-fleet/start-tls-runner.sh +21 -0
  50. package/examples/runner-fleet/synapsor.runner.json +27 -1
  51. package/examples/support-plan-credit/README.md +6 -1
  52. package/examples/support-plan-credit/mcp-client-examples/generic-streamable-http.json +4 -1
  53. package/examples/support-plan-credit/mcp-client-examples/openai-agents-streamable-http.ts +4 -0
  54. package/examples/support-plan-credit/synapsor.contract.json +0 -3
  55. package/fixtures/compatibility/published-1.5.4/manifest.json +76 -0
  56. package/fixtures/compatibility/published-1.5.4/sources/packages/dsl/examples/aggregate-read.synapsor.sql +21 -0
  57. package/fixtures/compatibility/published-1.5.4/sources/packages/dsl/examples/billing-late-fee.synapsor.sql +56 -0
  58. package/fixtures/compatibility/published-1.5.4/sources/packages/dsl/examples/bounded-set-multi-term.synapsor.sql +30 -0
  59. package/fixtures/compatibility/published-1.5.4/sources/packages/dsl/examples/principal-row-scope.synapsor.sql +23 -0
  60. package/fixtures/compatibility/published-1.5.4/sources/packages/spec/fixtures/conformance/aggregate-read/contract.json +119 -0
  61. package/fixtures/compatibility/published-1.5.4/sources/packages/spec/fixtures/conformance/approval-quorum/contract.json +44 -0
  62. package/fixtures/compatibility/published-1.5.4/sources/packages/spec/fixtures/conformance/bounded-set-threats/contract.json +115 -0
  63. package/fixtures/compatibility/published-1.5.4/sources/packages/spec/fixtures/conformance/principal-row-scope/contract.json +78 -0
  64. package/fixtures/compatibility/published-1.5.4/sources/packages/spec/fixtures/conformance/proposal-capability/contract.json +101 -0
  65. package/fixtures/compatibility/published-1.5.4/sources/packages/spec/fixtures/conformance/reversible-change-sets/contract.json +98 -0
  66. package/fixtures/compatibility/published-1.5.4/sources/packages/spec/fixtures/valid/basic-read.contract.json +60 -0
  67. package/llms.txt +37 -0
  68. package/package.json +7 -4
  69. 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 finishQuery("SELECT * FROM proposals", clauses, params, filters.limit);
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, generate canonical reviewed artifacts, open
52
- the first-action workbench, add the tools to Cursor, and inspect the local
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 count/sum/avg,
105
- tenant scope, suppression, and no-row evidence.
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 v0.1 scope summary.
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
@@ -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 | If omitted, DSL emits an explicit weak-guard acknowledgement. Prefer a real row-version column. |
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
 
@@ -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