@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
@@ -1,5 +1,26 @@
1
1
  # Reviewed Candidates From Prisma, Drizzle, And OpenAPI
2
2
 
3
+ Runner 1.6.0 uses these same bounded static parsers as evidence in the
4
+ whole-application [Auto Boundary](auto-boundary-and-scoped-explore.md) flow.
5
+ The database catalog remains authoritative for database structure; Prisma,
6
+ Drizzle, OpenAPI, and existing Synapsor definitions contribute deterministic
7
+ names, relationships, and structured review evidence. None of them
8
+ independently grant authority.
9
+
10
+ For a fresh interactive staging project, prefer:
11
+
12
+ ```bash
13
+ synapsor-runner start --from-env DATABASE_URL
14
+ ```
15
+
16
+ That combines all detected structured sources into one disabled candidate
17
+ graph, generation lock, and Workbench review. It never executes adopter code,
18
+ samples source rows before activation, uses an LLM, or overwrites an active
19
+ contract.
20
+
21
+ The individual commands below remain useful for CI, focused review, and
22
+ backward-compatible workflows.
23
+
3
24
  Use these generators after schema or API inspection when you want a review
4
25
  starting point without granting authority:
5
26
 
@@ -42,11 +63,17 @@ They do not decide which tenant or principal is authoritative, which fields are
42
63
  safe to expose, which writes are valid, business bounds, approval policy, or
43
64
  auto-approval. A field-name heuristic is not data classification.
44
65
 
45
- Review `generation-review.json` and `REVIEW.md`, replace every
66
+ In the individual-generator path, review `generation-review.json` and
67
+ `REVIEW.md`, replace every
46
68
  `review_required_*` placeholder, run the generated tests and a
47
69
  [Shadow study](shadow-studies.md), then deliberately copy reviewed definitions
48
70
  into an active contract through code review.
49
71
 
72
+ In Auto Boundary, Workbench presents the unresolved scope, field, aggregate,
73
+ relationship, privacy, and role-posture decisions together. Exact-digest
74
+ activation enables only the reviewed local authoring boundary. Generated named
75
+ capabilities still begin disabled and require their own activation.
76
+
50
77
  ## Input Safety
51
78
 
52
79
  Input is bounded to 2 MiB, 50 objects, 128 fields per object, and 200 generated
@@ -39,17 +39,52 @@ The model does not receive:
39
39
  - contract activation tools;
40
40
  - approval tools;
41
41
  - commit/writeback tools;
42
+ - MCP endpoint tokens, JWTs, refresh tokens, client secrets, or TLS keys;
42
43
  - trusted tenant or principal authority as ordinary model arguments.
43
44
 
45
+ ## Network transport and identity
46
+
47
+ Local stdio is the preferred one-client path. The MCP client launches Runner and
48
+ communicates over the child process's standard input/output, so Runner opens no
49
+ network listener and HTTP authentication does not apply.
50
+
51
+ A listening HTTP endpoint has no inherent client identity. Runner therefore
52
+ requires Bearer authentication by default. On loopback or an explicitly
53
+ single-tenant service, Bearer may carry one high-entropy opaque endpoint token;
54
+ that token only answers "may this service call the endpoint?" and is not a user
55
+ or tenant identity. Shared services require signed per-session JWTs, preferably
56
+ RS256/ES256 from an external identity provider, with exact issuer,
57
+ audience/resource, time, scope, tenant, and principal validation on every
58
+ request. An MCP session ID only routes protocol state and never authenticates a
59
+ caller.
60
+
61
+ Non-loopback HTTP refuses to bind unless Runner owns TLS, an explicitly trusted
62
+ TLS proxy protects the public and private hops, or an authenticated emergency
63
+ cleartext break-glass mode is selected. TLS protects the channel; mTLS also
64
+ authenticates a workload certificate but does not replace per-user/tenant claims
65
+ in a shared service. Exact Origin and Host policy, request/session limits, and
66
+ credential-pinned MCP sessions narrow browser, DNS-rebinding, exhaustion, and
67
+ session-swap attacks.
68
+
69
+ These controls remain independent: transport authentication permits endpoint
70
+ access; verified session claims provide tenant/principal context; the contract
71
+ authorizes semantic capabilities; database roles/RLS constrain the connection;
72
+ operator identity authorizes activation, approval, apply, reconcile, and
73
+ revert. See [HTTP MCP](http-mcp.md).
74
+
44
75
  Reviewed aggregate tools expose one scalar only. Their function, column,
45
76
  tenant key, optional equality selection, and minimum-group threshold are fixed
46
77
  in the contract; the model receives no predicate arguments or member rows.
47
78
  Suppression reduces single-record inference but does not replace statistical
48
79
  privacy review. See [Bounded Aggregate Reads](aggregate-reads.md).
49
80
 
50
- Trusted context comes from local configuration, environment bindings, or Cloud
51
- session context in Cloud mode. Tenant, principal, and authorization scope must
52
- not be accepted from the model as authority.
81
+ Trusted context comes from operator-controlled process bindings for local or
82
+ explicitly single-tenant use, verified signed HTTP claims for a shared service,
83
+ or verified Cloud session context in Cloud mode. Tenant, principal, and
84
+ authorization scope must not be accepted from the model as authority.
85
+ Canonical `SESSION` content is rejected by Runner with
86
+ `SESSION_BINDING_UNSUPPORTED`; Runner never aliases it to environment values,
87
+ arbitrary headers, MCP metadata, or model arguments.
53
88
 
54
89
  With a shared credential, these checks are application-level enforcement and
55
90
  depend on Runner's fixed predicate implementation. Optional PostgreSQL RLS mode
@@ -33,20 +33,108 @@ sqlite3 -readonly ./.synapsor/local.db '.tables'
33
33
  Do not mutate the database directly. Its internal tables may change between
34
34
  releases and are not a public storage API; use Runner commands for automation.
35
35
 
36
- ## Inspect the ledger
36
+ ## Inspect one complete lifecycle
37
37
 
38
- | Question | Command |
38
+ Start with no id:
39
+
40
+ ```bash
41
+ synapsor-runner lifecycle --store ./.synapsor/local.db
42
+ ```
43
+
44
+ `lifecycle`, `lifecycle show`, and `lifecycle show latest` are exact aliases.
45
+ They choose the newest proposal deterministically by `created_at` descending,
46
+ then proposal id as the tie-breaker. The default view answers:
47
+
48
+ - what capability and business object the model requested;
49
+ - which trusted tenant and principal scoped it;
50
+ - approval state and progress;
51
+ - whether a writeback job or intent exists;
52
+ - the latest guarded outcome and whether the source changed;
53
+ - replay and Cloud-link status; and
54
+ - the next safe read-only or operator command.
55
+
56
+ Use `--details` for one causal proposal-to-receipt/replay timeline, or `--json`
57
+ for one stable `synapsor.lifecycle-view.v1` JSON document:
58
+
59
+ ```bash
60
+ synapsor-runner lifecycle --details --store ./.synapsor/local.db
61
+ synapsor-runner lifecycle --json --store ./.synapsor/local.db
62
+ ```
63
+
64
+ The JSON is a domain view, not a dump of internal SQLite/PostgreSQL tables.
65
+ Absent stages are represented explicitly as empty arrays or `null`; Runner
66
+ does not fabricate a job, receipt, replay, or Cloud synchronization.
67
+
68
+ ### Browse and filter without copying ids
69
+
70
+ ```bash
71
+ synapsor-runner lifecycle list --limit 20 --store ./.synapsor/local.db
72
+ synapsor-runner lifecycle show --object invoice:INV-3001 --details --store ./.synapsor/local.db
73
+ synapsor-runner lifecycle list --tenant acme --capability billing.propose_late_fee_waiver
74
+ synapsor-runner lifecycle show --principal support-agent-17 --status applied
75
+ ```
76
+
77
+ List/show support tenant, principal, capability, business object, status/state,
78
+ and time-window filters. Filtered `show` selects the newest match and reports
79
+ the total match count; use `lifecycle list` to browse all matches. List JSON
80
+ uses `synapsor.lifecycle-list.v1`.
81
+
82
+ ### Start from any handle you already have
83
+
84
+ ```bash
85
+ synapsor-runner lifecycle show wrp_... --details
86
+ synapsor-runner lifecycle show ev_... --details
87
+ synapsor-runner lifecycle show replay_... --details
88
+ synapsor-runner lifecycle show wbj_... --details
89
+ synapsor-runner lifecycle show wbi:wbj_... --details
90
+ synapsor-runner lifecycle show receipt:42 --details
91
+ synapsor-runner lifecycle show audit:17 --details
92
+ ```
93
+
94
+ Runner resolves proposal, evidence, replay, writeback-job, writeback-intent,
95
+ receipt, and query-audit handles to the owning proposal. Numeric receipt and
96
+ query-audit ids require the `receipt:` or `audit:` namespace; a bare number is
97
+ rejected rather than guessed.
98
+
99
+ Lifecycle inspection is read-only. It does not contact the source database or
100
+ Cloud, create a writeback job, acquire a worker lease, approve, apply, reconcile,
101
+ retry, or synchronize anything. It applies the same tenant/principal visibility
102
+ rules and secret/kept-out-field protections as the focused views.
103
+
104
+ ### Shared PostgreSQL runtime store
105
+
106
+ For `storage.shared_postgres.mode = "runtime_store"`, pass the reviewed config:
107
+
108
+ ```bash
109
+ synapsor-runner lifecycle --config ./synapsor.runner.json
110
+ synapsor-runner lifecycle show --object invoice:INV-3001 --details \
111
+ --config ./synapsor.runner.json
112
+ ```
113
+
114
+ Runner reuses the existing bounded read bridge to the authoritative shared
115
+ ledger. It does not create or synchronize a persistent local mirror during
116
+ inspection. Connection failures fail safely; the command never falls back to an
117
+ unrelated local store.
118
+
119
+ ## Focused inspection commands
120
+
121
+ The existing commands remain useful when you need one record type:
122
+
123
+ | Question | Focused command |
39
124
  | --- | --- |
40
- | What did the model propose? | `synapsor-runner proposals show <proposal-id> --details` |
125
+ | What did the model propose? | `synapsor-runner proposals show latest --details` |
41
126
  | What data supported it? | `synapsor-runner evidence list --proposal <proposal-id>` then `evidence show <evidence-id> --details` |
42
127
  | What query was run? | `synapsor-runner query-audit list --proposal <proposal-id>` |
43
- | Who approved or rejected it? | `synapsor-runner proposals show <proposal-id> --details` |
44
128
  | Did guarded writeback apply? | `synapsor-runner receipts list --proposal <proposal-id>` |
45
- | What happened end to end? | `synapsor-runner replay show --proposal <proposal-id> --details` |
129
+ | What replay snapshot exists? | `synapsor-runner replay show latest --details` |
46
130
  | What happened to one object? | `synapsor-runner activity search --object invoice:INV-3001` |
47
131
  | What are the latest events? | `synapsor-runner events tail` |
48
132
  | How large is the store? | `synapsor-runner store stats --store ./.synapsor/local.db` |
49
133
 
134
+ `proposals writeback-job` is intentionally absent from inspection examples:
135
+ it materializes a job and is therefore an operator mutation, not a read-only
136
+ view.
137
+
50
138
  ## Server leases
51
139
 
52
140
  MCP server modes write a small lease file next to the store:
@@ -12,6 +12,104 @@ Use JSON for automation:
12
12
  npx -y -p @synapsor/runner synapsor-runner doctor --first-run --json
13
13
  ```
14
14
 
15
+ ## Fresh Start Did Not Enter Auto Boundary
16
+
17
+ Auto Boundary is the default only for a fresh interactive `start --from-env`
18
+ with no existing config, selector, answers file, machine-output flag, or other
19
+ automation input. This preserves every established 1.x route.
20
+
21
+ Check the generated state:
22
+
23
+ ```bash
24
+ synapsor-runner boundary status --json
25
+ ```
26
+
27
+ To draft explicitly without prompts or a browser:
28
+
29
+ ```bash
30
+ synapsor-runner boundary draft \
31
+ --from-env DATABASE_URL \
32
+ --schema public \
33
+ --project-root . \
34
+ --json
35
+ ```
36
+
37
+ ## Scoped Explore Is Not Advertised
38
+
39
+ This is correct unless all authoring prerequisites pass. Explore is disabled by
40
+ default and never appears in production, unknown-profile, shared HTTP, remote,
41
+ or non-loopback `tools/list`.
42
+
43
+ Check:
44
+
45
+ ```bash
46
+ synapsor-runner boundary status --json
47
+ synapsor-runner boundary diff --json
48
+ synapsor-runner mcp status cursor --project
49
+ ```
50
+
51
+ The exact boundary digest must be active, the profile must explicitly be
52
+ `development` or `staging`, the generation lock and compiler/Spec versions
53
+ must be current, and the inspected database role must still be SELECT-only,
54
+ non-owner, non-superuser, and not `BYPASSRLS`. Runner also enforces a read-only
55
+ transaction for every Explore call.
56
+
57
+ A write-capable, owner, superuser, `BYPASSRLS`, or unverifiable credential may
58
+ still inspect metadata with a warning. It cannot enable source-row Explore.
59
+ Use a dedicated staging reader instead of weakening this check.
60
+
61
+ ## Cursor Has Production Tools Instead Of Authoring Tools
62
+
63
+ Install the managed local authoring entry only after boundary activation:
64
+
65
+ ```bash
66
+ synapsor-runner mcp install cursor \
67
+ --project \
68
+ --authoring \
69
+ --project-root . \
70
+ --yes
71
+ ```
72
+
73
+ Authoring status reports exactly `app.describe_data` and `app.explore_data`.
74
+ After Protect and exact-digest activation, replace that entry with the
75
+ production config. The protected named capability remains available while
76
+ Explore disappears:
77
+
78
+ ```bash
79
+ synapsor-runner mcp install cursor \
80
+ --project \
81
+ --config ./synapsor.runner.json \
82
+ --store ./.synapsor/local.db \
83
+ --yes
84
+ ```
85
+
86
+ ## Boundary Became Stale After A Schema Or Grant Change
87
+
88
+ Generated authority is bound to the schema, role, grants, ownership, RLS
89
+ posture, compiler, and canonical Spec fingerprint:
90
+
91
+ ```bash
92
+ synapsor-runner boundary diff --json
93
+ ```
94
+
95
+ Additive fields receive no implicit authority. Breaking or posture drift fails
96
+ closed only for lock-bound generated authority. Regenerate, review the semantic
97
+ diff, and activate the new exact digest. Existing manually authored projects
98
+ without a generation lock retain their previous behavior.
99
+
100
+ ## Aggregate Explore Suppressed Or Refused A Result
101
+
102
+ Suppression and budget failures are security behavior, not query failures.
103
+ Workbench shows the reviewed minimum cohort, maximum groups, response limits,
104
+ and durable extraction/differencing budgets. You cannot widen them in a model
105
+ argument.
106
+
107
+ Use a larger legitimate cohort or protect a narrower reviewed question. Do not
108
+ work around suppression with repeated slightly different filters: Runner
109
+ records normalized redacted plan metadata and exhausts the differencing budget.
110
+ Returned rows/groups, trusted tenant/principal values, credentials, and raw
111
+ sensitive literals are not stored in the query audit.
112
+
15
113
  ## Safe Action Draft Does Not Appear As A Tool
16
114
 
17
115
  This is expected before activation. `start --action`, agent edits, `action
@@ -0,0 +1,23 @@
1
+ # Auto Boundary Churn Fixture
2
+
3
+ This synthetic staging fixture is the measured Runner 1.6.0 golden path:
4
+ PostgreSQL, Next.js, Prisma, Cursor-compatible local MCP, and the local
5
+ Workbench.
6
+
7
+ The database role is SELECT-only, non-owner, non-superuser, and subject to
8
+ forced tenant/principal RLS. The data includes reviewed churn cohorts, a
9
+ cross-tenant denial case, kept-out fields, and cohorts below the privacy
10
+ threshold.
11
+
12
+ Run the automated acceptance path from the repository root:
13
+
14
+ ```bash
15
+ corepack pnpm test:auto-boundary-explore
16
+ corepack pnpm test:auto-boundary-explore:packed
17
+ ```
18
+
19
+ The packed gate uses only packed public artifacts for the complete journey:
20
+ disabled whole-schema draft, digest activation, two Cursor authoring tools,
21
+ privacy-suppressed weekly churn analysis, denial and differencing tests,
22
+ Protect This Query, production Explore removal, and the surviving named
23
+ capability. No source row is mutated.
@@ -0,0 +1,8 @@
1
+ export default function ChurnOverview() {
2
+ return (
3
+ <main>
4
+ <h1>Subscription churn review</h1>
5
+ <p>This synthetic staging application is used by the Synapsor Auto Boundary acceptance test.</p>
6
+ </main>
7
+ );
8
+ }
@@ -0,0 +1,16 @@
1
+ services:
2
+ postgres:
3
+ image: postgres:16
4
+ environment:
5
+ POSTGRES_DB: synapsor_auto_boundary
6
+ POSTGRES_USER: synapsor_admin
7
+ POSTGRES_PASSWORD: synapsor_admin_password
8
+ ports:
9
+ - "127.0.0.1:55460:5432"
10
+ healthcheck:
11
+ test: ["CMD-SHELL", "test \"$(psql -U synapsor_admin -d synapsor_auto_boundary -tAc \"SELECT pg_postmaster_start_time() > initialized_at FROM public.synapsor_fixture_ready LIMIT 1\" 2>/dev/null | xargs)\" = t"]
12
+ interval: 1s
13
+ timeout: 3s
14
+ retries: 40
15
+ volumes:
16
+ - ./seed/postgres.sql:/docker-entrypoint-initdb.d/001-schema.sql:ro
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "synapsor-auto-boundary-churn",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev"
7
+ },
8
+ "dependencies": {
9
+ "@prisma/client": "^6.0.0",
10
+ "next": "^15.0.0",
11
+ "react": "^19.0.0",
12
+ "react-dom": "^19.0.0"
13
+ },
14
+ "devDependencies": {
15
+ "prisma": "^6.0.0",
16
+ "typescript": "^5.8.0"
17
+ }
18
+ }
@@ -0,0 +1,36 @@
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ }
4
+
5
+ datasource db {
6
+ provider = "postgresql"
7
+ url = env("DATABASE_URL")
8
+ }
9
+
10
+ model Account {
11
+ id String @id
12
+ tenantId String @map("tenant_id")
13
+ ownerId String @map("owner_id")
14
+ region String
15
+ segment String
16
+ customerEmail String @map("customer_email")
17
+ internalRiskScore Int @map("internal_risk_score")
18
+ churnEvents ChurnEvent[]
19
+
20
+ @@map("accounts")
21
+ }
22
+
23
+ model ChurnEvent {
24
+ id String @id
25
+ tenantId String @map("tenant_id")
26
+ ownerId String @map("owner_id")
27
+ accountId String @map("account_id")
28
+ reasonCategory String @map("reason_category")
29
+ monthlyRevenueCents Int @map("monthly_revenue_cents")
30
+ churnedAt DateTime @map("churned_at") @db.Timestamptz(6)
31
+ privateNote String @map("private_note")
32
+ account Account @relation(fields: [accountId], references: [id])
33
+
34
+ @@index([tenantId, churnedAt])
35
+ @@map("churn_events")
36
+ }
@@ -0,0 +1,126 @@
1
+ CREATE TABLE public.accounts (
2
+ id text PRIMARY KEY,
3
+ tenant_id text NOT NULL,
4
+ owner_id text NOT NULL,
5
+ region text NOT NULL CHECK (region IN ('east', 'north', 'south', 'west')),
6
+ segment text NOT NULL CHECK (segment IN ('enterprise', 'growth', 'startup')),
7
+ customer_email text NOT NULL,
8
+ internal_risk_score integer NOT NULL
9
+ );
10
+
11
+ CREATE TABLE public.churn_events (
12
+ id text PRIMARY KEY,
13
+ tenant_id text NOT NULL,
14
+ owner_id text NOT NULL,
15
+ account_id text NOT NULL REFERENCES public.accounts(id) ON DELETE RESTRICT,
16
+ reason_category text NOT NULL CHECK (reason_category IN ('onboarding', 'price', 'product', 'service')),
17
+ monthly_revenue_cents integer NOT NULL CHECK (monthly_revenue_cents >= 0),
18
+ churned_at timestamptz NOT NULL,
19
+ private_note text NOT NULL
20
+ );
21
+
22
+ CREATE INDEX churn_events_tenant_time_idx
23
+ ON public.churn_events(tenant_id, churned_at);
24
+
25
+ WITH groups(prefix, tenant_id, owner_id, region, segment, reason_category, churned_at, group_size, revenue) AS (
26
+ VALUES
27
+ ('p1-west-price', 'acme', 'pm-1', 'west', 'growth', 'price', '2026-06-03T12:00:00Z'::timestamptz, 5, 12000),
28
+ ('p1-east-service', 'acme', 'pm-1', 'east', 'enterprise', 'service', '2026-06-10T12:00:00Z'::timestamptz, 5, 22000),
29
+ ('p1-north-product', 'acme', 'pm-1', 'north', 'startup', 'product', '2026-06-17T12:00:00Z'::timestamptz, 2, 5000),
30
+ ('p2-west-price', 'acme', 'pm-1', 'west', 'growth', 'price', '2026-07-08T12:00:00Z'::timestamptz, 10, 13000),
31
+ ('p2-east-service', 'acme', 'pm-1', 'east', 'enterprise', 'service', '2026-07-15T12:00:00Z'::timestamptz, 7, 23000),
32
+ ('p2-south-onboarding', 'acme', 'pm-1', 'south', 'startup', 'onboarding', '2026-07-22T12:00:00Z'::timestamptz, 5, 7000),
33
+ ('p2-north-product', 'acme', 'pm-1', 'north', 'startup', 'product', '2026-07-22T12:00:00Z'::timestamptz, 1, 6000),
34
+ ('other-west-price', 'globex', 'pm-2', 'west', 'enterprise', 'price', '2026-07-08T12:00:00Z'::timestamptz, 8, 99000)
35
+ )
36
+ INSERT INTO public.accounts (
37
+ id,
38
+ tenant_id,
39
+ owner_id,
40
+ region,
41
+ segment,
42
+ customer_email,
43
+ internal_risk_score
44
+ )
45
+ SELECT
46
+ prefix || '-' || item,
47
+ tenant_id,
48
+ owner_id,
49
+ region,
50
+ segment,
51
+ prefix || '-' || item || '@example.invalid',
52
+ 900 + item
53
+ FROM groups
54
+ CROSS JOIN LATERAL generate_series(1, group_size) AS item;
55
+
56
+ WITH groups(prefix, tenant_id, owner_id, reason_category, churned_at, group_size, revenue) AS (
57
+ VALUES
58
+ ('p1-west-price', 'acme', 'pm-1', 'price', '2026-06-03T12:00:00Z'::timestamptz, 5, 12000),
59
+ ('p1-east-service', 'acme', 'pm-1', 'service', '2026-06-10T12:00:00Z'::timestamptz, 5, 22000),
60
+ ('p1-north-product', 'acme', 'pm-1', 'product', '2026-06-17T12:00:00Z'::timestamptz, 2, 5000),
61
+ ('p2-west-price', 'acme', 'pm-1', 'price', '2026-07-08T12:00:00Z'::timestamptz, 10, 13000),
62
+ ('p2-east-service', 'acme', 'pm-1', 'service', '2026-07-15T12:00:00Z'::timestamptz, 7, 23000),
63
+ ('p2-south-onboarding', 'acme', 'pm-1', 'onboarding', '2026-07-22T12:00:00Z'::timestamptz, 5, 7000),
64
+ ('p2-north-product', 'acme', 'pm-1', 'product', '2026-07-22T12:00:00Z'::timestamptz, 1, 6000),
65
+ ('other-west-price', 'globex', 'pm-2', 'price', '2026-07-08T12:00:00Z'::timestamptz, 8, 99000)
66
+ )
67
+ INSERT INTO public.churn_events (
68
+ id,
69
+ tenant_id,
70
+ owner_id,
71
+ account_id,
72
+ reason_category,
73
+ monthly_revenue_cents,
74
+ churned_at,
75
+ private_note
76
+ )
77
+ SELECT
78
+ 'event-' || prefix || '-' || item,
79
+ tenant_id,
80
+ owner_id,
81
+ prefix || '-' || item,
82
+ reason_category,
83
+ revenue,
84
+ churned_at,
85
+ 'synthetic kept-out note ' || prefix || '-' || item
86
+ FROM groups
87
+ CROSS JOIN LATERAL generate_series(1, group_size) AS item;
88
+
89
+ ALTER TABLE public.accounts ENABLE ROW LEVEL SECURITY;
90
+ ALTER TABLE public.accounts FORCE ROW LEVEL SECURITY;
91
+ ALTER TABLE public.churn_events ENABLE ROW LEVEL SECURITY;
92
+ ALTER TABLE public.churn_events FORCE ROW LEVEL SECURITY;
93
+
94
+ CREATE POLICY accounts_trusted_scope ON public.accounts
95
+ FOR SELECT
96
+ USING (
97
+ tenant_id = current_setting('app.tenant_id', true)
98
+ AND owner_id = current_setting('app.principal', true)
99
+ );
100
+
101
+ CREATE POLICY churn_events_trusted_scope ON public.churn_events
102
+ FOR SELECT
103
+ USING (
104
+ tenant_id = current_setting('app.tenant_id', true)
105
+ AND owner_id = current_setting('app.principal', true)
106
+ );
107
+
108
+ DO $$
109
+ BEGIN
110
+ CREATE ROLE synapsor_churn_reader LOGIN PASSWORD 'synapsor_churn_reader_password';
111
+ EXCEPTION WHEN duplicate_object THEN NULL;
112
+ END
113
+ $$;
114
+
115
+ ALTER ROLE synapsor_churn_reader SET default_transaction_read_only = on;
116
+ GRANT CONNECT ON DATABASE synapsor_auto_boundary TO synapsor_churn_reader;
117
+ GRANT USAGE ON SCHEMA public TO synapsor_churn_reader;
118
+ GRANT SELECT ON public.accounts, public.churn_events TO synapsor_churn_reader;
119
+
120
+ -- The image entrypoint runs these statements under a temporary postmaster and
121
+ -- then restarts PostgreSQL. Compose reports healthy only after that final
122
+ -- postmaster starts, preventing first-client disconnects in CI.
123
+ CREATE TABLE public.synapsor_fixture_ready (
124
+ initialized_at timestamptz NOT NULL
125
+ );
126
+ INSERT INTO public.synapsor_fixture_ready (initialized_at) VALUES (clock_timestamp());
@@ -40,7 +40,7 @@ OpenAI Agents Streamable HTTP example smoke passed.
40
40
  export DATABASE_URL="<postgres-or-mysql-read-url>"
41
41
  export SYNAPSOR_TENANT_ID="acme"
42
42
  export SYNAPSOR_PRINCIPAL="openai_agent_demo"
43
- export SYNAPSOR_RUNNER_HTTP_TOKEN="dev-token"
43
+ export SYNAPSOR_RUNNER_HTTP_TOKEN="$(node -e 'process.stdout.write(require("node:crypto").randomBytes(32).toString("base64url"))')"
44
44
 
45
45
  npx -y -p @synapsor/runner synapsor-runner mcp serve-streamable-http \
46
46
  --config ./synapsor.runner.json \
@@ -49,6 +49,10 @@ npx -y -p @synapsor/runner synapsor-runner mcp serve-streamable-http \
49
49
  --alias-mode openai
50
50
  ```
51
51
 
52
+ The operator must provision that same opaque value to the authorized client.
53
+ It is endpoint access for this loopback/single-service example, not user or
54
+ tenant identity. Do not put it in source code or the client config.
55
+
52
56
  ## Terminal 2: Run The Agent
53
57
 
54
58
  ```bash
@@ -58,7 +62,7 @@ pip install -r requirements.txt
58
62
 
59
63
  export OPENAI_API_KEY="..."
60
64
  export SYNAPSOR_RUNNER_HTTP_URL="http://127.0.0.1:8766/mcp"
61
- export SYNAPSOR_RUNNER_HTTP_TOKEN="dev-token"
65
+ read -rsp "Provisioned Synapsor endpoint token: " SYNAPSOR_MCP_ACCESS_TOKEN; printf '\n'; export SYNAPSOR_MCP_ACCESS_TOKEN
62
66
  export SYNAPSOR_INVOICE_ID="INV-3001"
63
67
 
64
68
  python agent.py
@@ -74,5 +78,7 @@ Expected behavior:
74
78
  - no SQL/write/approval tool is exposed to the model;
75
79
  - evidence/query audit are saved in the local Runner store.
76
80
 
77
- For production-like deployment, keep HTTP MCP behind private networking/TLS,
78
- bearer auth, and rate limits. See [HTTP MCP](../../docs/http-mcp.md).
81
+ For production-like deployment, non-loopback HTTP requires Runner TLS or an
82
+ explicitly trusted TLS proxy. A shared user/tenant service uses a short-lived
83
+ signed access token from an external identity provider rather than distributing
84
+ one opaque token. See [HTTP MCP](../../docs/http-mcp.md).
@@ -13,14 +13,14 @@ except ImportError as exc:
13
13
 
14
14
 
15
15
  async def main() -> None:
16
- required = ["OPENAI_API_KEY", "SYNAPSOR_RUNNER_HTTP_URL", "SYNAPSOR_RUNNER_HTTP_TOKEN"]
16
+ required = ["OPENAI_API_KEY", "SYNAPSOR_RUNNER_HTTP_URL", "SYNAPSOR_MCP_ACCESS_TOKEN"]
17
17
  missing = [name for name in required if not os.environ.get(name)]
18
18
  if missing:
19
19
  raise SystemExit(f"Missing required environment variables: {', '.join(missing)}")
20
20
 
21
21
  invoice_id = os.environ.get("SYNAPSOR_INVOICE_ID", "INV-3001")
22
22
  mcp_url = os.environ["SYNAPSOR_RUNNER_HTTP_URL"]
23
- token = os.environ["SYNAPSOR_RUNNER_HTTP_TOKEN"]
23
+ token = os.environ["SYNAPSOR_MCP_ACCESS_TOKEN"]
24
24
 
25
25
  async with MCPServerStreamableHttp(
26
26
  params={
@@ -1,11 +1,16 @@
1
1
  FROM node:22-bookworm-slim
2
2
 
3
+ RUN apt-get update \
4
+ && apt-get install -y --no-install-recommends openssl \
5
+ && rm -rf /var/lib/apt/lists/*
6
+
3
7
  RUN corepack enable && corepack prepare pnpm@10.14.0 --activate
4
8
 
5
9
  WORKDIR /app
6
10
  COPY . .
7
11
  RUN corepack pnpm install --frozen-lockfile \
8
- && corepack pnpm build:runner-package
12
+ && corepack pnpm build:runner-package \
13
+ && chmod +x examples/runner-fleet/start-tls-runner.sh
9
14
 
10
- ENTRYPOINT ["node", "apps/runner/dist/cli.js"]
15
+ ENTRYPOINT ["examples/runner-fleet/start-tls-runner.sh"]
11
16
  CMD ["mcp", "serve", "--transport", "streamable-http", "--host", "0.0.0.0", "--port", "8766", "--config", "/config/synapsor.runner.json"]