@synapsor/runner 0.1.16 → 1.1.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 (35) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/README.md +59 -3
  3. package/THREAT_MODEL.md +29 -2
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/local-ui.d.ts +3 -0
  6. package/dist/local-ui.d.ts.map +1 -1
  7. package/dist/runner.mjs +6587 -918
  8. package/docs/README.md +16 -3
  9. package/docs/capability-authoring.md +2 -1
  10. package/docs/conformance.md +5 -0
  11. package/docs/current-scope.md +6 -2
  12. package/docs/dsl-reference.md +25 -0
  13. package/docs/http-mcp.md +45 -1
  14. package/docs/limitations.md +22 -7
  15. package/docs/migrating-to-synapsor-spec.md +21 -0
  16. package/docs/oss-vs-cloud.md +4 -3
  17. package/docs/production.md +219 -7
  18. package/docs/release-notes.md +47 -7
  19. package/docs/release-policy.md +58 -6
  20. package/docs/runner-config-reference.md +208 -5
  21. package/docs/running-a-runner-fleet.md +349 -0
  22. package/examples/runner-fleet/Dockerfile +11 -0
  23. package/examples/runner-fleet/README.md +52 -0
  24. package/examples/runner-fleet/docker-compose.yml +76 -0
  25. package/examples/runner-fleet/mint-dev-token.mjs +19 -0
  26. package/examples/runner-fleet/seed/mysql.sql +20 -0
  27. package/examples/runner-fleet/seed/postgres.sql +73 -0
  28. package/examples/runner-fleet/synapsor.runner.json +105 -0
  29. package/examples/support-plan-credit/README.md +16 -1
  30. package/examples/support-plan-credit/contract.synapsor.sql +2 -0
  31. package/examples/support-plan-credit/synapsor.contract.json +15 -0
  32. package/fixtures/protocol/MANIFEST.json +1 -1
  33. package/package.json +3 -1
  34. package/schemas/change-set.v1.schema.json +2 -1
  35. package/schemas/synapsor.runner.schema.json +208 -0
@@ -1,7 +1,10 @@
1
1
  # Release Policy
2
2
 
3
- Synapsor Runner `0.1.0` is the first stable local runner line. Use the stable
4
- package for normal installs:
3
+ Synapsor Runner `0.1.0` was the first stable local runner line. Synapsor
4
+ Runner `1.0.0` is the first production approval-loop semver line: batch apply,
5
+ aggregate policy limits, verified operator identity, structured operations, and
6
+ shared runtime-store deployment are part of the documented compatibility
7
+ surface. Use the stable package for normal installs:
5
8
 
6
9
  ```bash
7
10
  npx -y -p @synapsor/runner synapsor-runner demo --quick
@@ -55,10 +58,58 @@ A stable `0.1.0` release should only be tagged after:
55
58
  - there are no known docs/code mismatches around transport, credentials,
56
59
  receipt tables, or handler expectations.
57
60
 
61
+ ## 1.0 Stability Gate
62
+
63
+ Do not tag `1.0.0` only because the package is useful. `1.0.0` is the public
64
+ semver contract for the Runner production approval loop, and it should be cut
65
+ only after the following are true in the current tree and release artifacts:
66
+
67
+ - batch apply can apply all approved proposals independently, reports
68
+ applied/conflict/skipped IDs, is safe to rerun through idempotency receipts,
69
+ and supports `--capability`, `--tenant`, and `--max`;
70
+ - aggregate auto-approval limits are authored in DSL, represented in the
71
+ canonical contract spec, enforced as human-review fallback, persisted with
72
+ tripped-limit details, and visible in doctor/tools preview output;
73
+ - approve/reject/apply can require a verified operator identity, enforce
74
+ contract reviewer roles and apply roles, and bind tamper-evident identity
75
+ records into the proposal ledger;
76
+ - designed rejections and writeback outcomes emit structured logs, operational
77
+ counters are available per tenant and capability, and owner-only local store
78
+ permissions remain under test;
79
+ - the documented public surfaces below have release-gate coverage from packed
80
+ artifacts, not only source-tree tests.
81
+
82
+ After `1.0.0`, changes to the documented CLI, schema, contract, MCP result,
83
+ writeback, approval, metrics, and replay surfaces must follow semantic
84
+ versioning. Breaking changes require a new major version, except for security
85
+ fixes that close a vulnerability while preserving the safest possible
86
+ compatibility path.
87
+
88
+ ## 1.1 Fleet Gate
89
+
90
+ Do not publish `1.1.0` until all 1.0 gates remain green and the local synthetic
91
+ fleet verifier proves:
92
+
93
+ - claim/context contradictions fail before serving;
94
+ - two claim-bound Runners preserve tenant isolation and one active proposal;
95
+ - shared fixed-window rate limits reject with `retry_after_ms`;
96
+ - distinct verified reviewers satisfy canonical quorum and apply remains
97
+ blocked before `N/N`;
98
+ - Postgres/MySQL pool acquisition and queue limits fail fast;
99
+ - worker termination before write and after commit recovers without a duplicate
100
+ effect;
101
+ - backup digest, clean restore, and archive-before-retention preserve active
102
+ records;
103
+ - `/healthz`, `/readyz`, and separately authorized `/metrics` expose no secrets
104
+ or business object/principal labels.
105
+
106
+ Run `corepack pnpm test:fleet`. Only homogeneous 1.1 fleet operation is part of
107
+ this gate; mixed-minor operation must be tested before it is claimed.
108
+
58
109
  ## Stable Compatibility Promise
59
110
 
60
- Starting with `0.1.0`, Synapsor Runner keeps these public surfaces compatible
61
- through the `0.1.x` line unless a release note marks a deprecation first:
111
+ Starting with `1.0.0`, Synapsor Runner keeps these public surfaces compatible
112
+ through the `1.x` line unless a release note marks a deprecation first:
62
113
 
63
114
  - the `synapsor-runner` binary name and README quickstart commands;
64
115
  - `synapsor.runner.json` schema version `1` for documented fields;
@@ -73,8 +124,9 @@ through the `0.1.x` line unless a release note marks a deprecation first:
73
124
  Stable does not promise production SLA, hosted Cloud features, compliance
74
125
  certification, physical Postgres/MySQL branching, generic SQL writeback,
75
126
  generic multi-row writes, or compatibility for undocumented local SQLite
76
- internals. Local store migrations may happen inside `0.1.x`, but documented CLI
77
- inspection commands should remain the supported way to read the store.
127
+ internals. Local store migrations may happen inside `1.x` and later minor
128
+ versions, but documented CLI inspection commands should remain the supported way
129
+ to read the store.
78
130
 
79
131
  Alpha users should pin an exact alpha version in package.json, CI, and MCP
80
132
  client snippets. Use `@alpha` only when intentionally testing the moving
@@ -26,7 +26,7 @@ Unknown keys fail when `strict` is true (the default).
26
26
  | `mode` | Yes | `read_only`, `shadow`, `review`, or `cloud`. |
27
27
  | `result_format` | No | `1` legacy or `2` stable `ok/summary/data/proposal/error` envelope. Default `1`. |
28
28
  | `strict` | No | Reject unknown config keys. Default `true`. |
29
- | `storage` | Local | Local SQLite ledger. |
29
+ | `storage` | Local | Local SQLite ledger plus optional shared Postgres mirror or runtime-store wiring. |
30
30
  | `sources` | Local | Named Postgres/MySQL source wiring. |
31
31
  | `trusted_context` | Conditional | Default trusted tenant/principal binding. |
32
32
  | `contexts` | No | Named trusted contexts referenced by capabilities/contracts. |
@@ -34,6 +34,10 @@ Unknown keys fail when `strict` is true (the default).
34
34
  | `capabilities` | Conditional | Embedded compatibility capabilities; may be `[]` with `contracts`. |
35
35
  | `policies` | No | Embedded reviewed policies; contract policies merge into the same catalog. |
36
36
  | `approvals` | No | Local approval overrides. |
37
+ | `operator_identity` | No | Verified operator identity and apply-role wiring for approve/reject/apply. |
38
+ | `session_auth` | HTTP claims | HS256 development or asymmetric RS256/ES256 session-token verification. |
39
+ | `rate_limits` | No | Operational fixed-window limits; fleet-wide only with shared `runtime_store`. |
40
+ | `metrics` | No | Separately authorized scrapeable HTTP metrics. Disabled by default. |
37
41
  | `executors` | No | App-owned writeback wiring. |
38
42
  | `cloud` | Cloud mode | Scoped Cloud adapter configuration. |
39
43
 
@@ -50,13 +54,54 @@ Unknown keys fail when `strict` is true (the default).
50
54
  ## Storage
51
55
 
52
56
  ```json
53
- { "storage": { "sqlite_path": "./.synapsor/local.db" } }
57
+ {
58
+ "storage": {
59
+ "sqlite_path": "./.synapsor/local.db",
60
+ "shared_postgres": {
61
+ "mode": "mirror",
62
+ "url_env": "SYNAPSOR_LEDGER_DATABASE_URL",
63
+ "schema": "synapsor_runner",
64
+ "lock_timeout_ms": 10000,
65
+ "max_entries": 10000
66
+ }
67
+ }
68
+ }
54
69
  ```
55
70
 
56
71
  The ledger stores proposals, visible evidence copies, query audit, approvals,
57
72
  receipts, events, and replay. New POSIX stores are owner-only (`0600`). Protect
58
73
  the file like a database extract; see [Store Lifecycle](store-lifecycle.md).
59
74
 
75
+ `storage.shared_postgres.mode = "mirror"` makes mutating CLI commands restore
76
+ from the shared Postgres ledger before the local mutation and sync back after
77
+ the command while holding a schema-scoped Postgres advisory lock. Mirror mode is
78
+ a bounded operator handoff bridge; MCP serving still uses the local SQLite
79
+ store.
80
+
81
+ `storage.shared_postgres.mode = "runtime_store"` makes MCP serving use the
82
+ shared Postgres ledger as the primary proposal/evidence/replay store. Runner
83
+ creates a connection from the environment variable named by `url_env`, auto-runs
84
+ the shared-ledger migration at startup, and uses a schema-scoped transaction
85
+ advisory lock around runtime mutations. The URL is still referenced by
86
+ environment variable name; never put the database URL in the config file.
87
+ Bounded CLI approval/apply/worker commands bridge through the same Postgres
88
+ ledger by restoring into a temporary local store under the Postgres advisory
89
+ lock, running the existing mutation, and syncing back. Long-running worker
90
+ processes repeat bounded drain cycles and release the advisory lock while idle.
91
+
92
+ The bridge loads at most `max_entries` ledger records per operation. The default
93
+ is 10,000; valid values are 100 through 100,000. It fails closed with
94
+ `POSTGRES_RUNTIME_STORE_CAPACITY_EXCEEDED` above the bound. Reviewer CLI reads
95
+ and the local UI use the same shared queue. This is a serialized small-fleet
96
+ design, not an unbounded high-throughput primary store.
97
+
98
+ `synapsor-runner doctor --config ./synapsor.runner.json` checks shared Postgres
99
+ wiring when this block is present: the URL env var must be set, and the
100
+ configured schema must already contain `ledger_entries`, `proposal_locks`,
101
+ `worker_leases`, and `rate_limit_buckets`. Doctor reports env var names and
102
+ table readiness only; it does
103
+ not print database URLs or create the schema.
104
+
60
105
  ## Sources
61
106
 
62
107
  ```json
@@ -81,6 +126,13 @@ timeout. `ssl` carries adapter-specific reviewed SSL options when used.
81
126
 
82
127
  A contract's `SOURCE billing_postgres` must exactly match a `sources` key.
83
128
 
129
+ Long-running servers reuse native driver pools. Optional `pool` keys are
130
+ `max_connections` (default 10), `connection_timeout_ms` (3000),
131
+ `idle_timeout_ms` (30000), `queue_timeout_ms` (5000), and `queue_limit`
132
+ (default `max(10, max_connections * 4)`). Queue overflow returns
133
+ `SOURCE_POOL_QUEUE_FULL`; acquisition timeout returns `SOURCE_POOL_TIMEOUT`.
134
+ One-shot CLI commands close their pools.
135
+
84
136
  ## Trusted context
85
137
 
86
138
  ```json
@@ -100,6 +152,62 @@ Providers are `environment`, `static_dev`, `http_claims`, and `cloud_session`.
100
152
  Capabilities may reference a context by name. The model never receives tenant
101
153
  or principal as an overridable argument.
102
154
 
155
+ For multi-tenant Streamable HTTP services, use `http_claims` plus signed
156
+ session auth:
157
+
158
+ ```json
159
+ {
160
+ "trusted_context": {
161
+ "provider": "http_claims",
162
+ "values": {
163
+ "tenant_id_key": "tenant_id",
164
+ "principal_key": "sub"
165
+ }
166
+ },
167
+ "session_auth": {
168
+ "provider": "jwt_hs256",
169
+ "secret_env": "SYNAPSOR_SESSION_JWT_SECRET",
170
+ "previous_secret_env": "SYNAPSOR_PREVIOUS_SESSION_JWT_SECRET",
171
+ "issuer": "https://identity.example",
172
+ "audience": "synapsor-runner"
173
+ }
174
+ }
175
+ ```
176
+
177
+ `previous_secret_env` is optional and is only for rotation windows. Runner tries
178
+ the active secret first, then the previous secret. Existing MCP sessions remain
179
+ bound to the exact token fingerprint, so clients cannot swap tenant/principal
180
+ identity inside an initialized session.
181
+
182
+ For networked deployments prefer asymmetric public-key verification:
183
+
184
+ ```json
185
+ {
186
+ "session_auth": {
187
+ "provider": "jwt_asymmetric",
188
+ "algorithms": ["RS256"],
189
+ "jwks_url_env": "SYNAPSOR_SESSION_JWKS_URL",
190
+ "issuer": "https://identity.example",
191
+ "audience": "synapsor-runner",
192
+ "tenant_claim": "tenant_id",
193
+ "principal_claim": "sub",
194
+ "clock_skew_seconds": 30,
195
+ "jwks_cache_seconds": 600,
196
+ "jwks_cooldown_seconds": 30,
197
+ "fetch_timeout_ms": 3000,
198
+ "max_response_bytes": 1048576
199
+ }
200
+ }
201
+ ```
202
+
203
+ `jwt_asymmetric` requires an explicit `RS256`/`ES256` allowlist and exactly one
204
+ public-key source: `jwks_url_env`, `public_key_env`, or `public_key_path`.
205
+ JWKS selection uses `kid`; unknown keys get one controlled refresh. Fetches are
206
+ timeout/size bounded and do not follow redirects. Private JWK fields are
207
+ rejected. Every effective named context in an `http_claims` server must bind
208
+ tenant and principal from claims; environment/static contradictions fail with
209
+ `TRUSTED_CONTEXT_PROVIDER_CONFLICT` before serving.
210
+
103
211
  ## Contracts and embedded capabilities
104
212
 
105
213
  ```json
@@ -148,9 +256,104 @@ See [Writeback Executors](writeback-executors.md).
148
256
 
149
257
  `policies` is an array of reviewed policy objects. Local approval policies use
150
258
  `kind: "approval"` and numeric rules such as `{ "field": "amount_cents",
151
- "max": 2500 }`. `approvals.disable_auto_approval: true` disables local policy
152
- auto-approval without changing the reviewed contract. Approval never becomes an
153
- MCP tool.
259
+ "max": 2500 }`. Canonical policies may also carry daily `count` or `total`
260
+ limits scoped to `tenant_policy` or `tenant_policy_object`. These limits come
261
+ from reviewed DSL `LIMIT` clauses, not an unreviewed local override. A tripped
262
+ limit falls back to human review and is recorded in the ledger.
263
+ `approvals.disable_auto_approval: true` disables local policy auto-approval
264
+ without changing the reviewed contract. Approval never becomes an MCP tool.
265
+ Portable `approval.required_approvals` defaults to `1` and accepts `1..10`.
266
+ Distinct verified subjects fill quorum slots; one subject cannot count twice.
267
+ A rejection is terminal. Policy auto-approval is deferred when quorum is
268
+ greater than one.
269
+
270
+ ## Rate limits
271
+
272
+ ```json
273
+ {
274
+ "rate_limits": {
275
+ "enabled": true,
276
+ "default": { "requests": 60, "window_seconds": 60 },
277
+ "capabilities": {
278
+ "billing.propose_refund": { "requests": 10, "window_seconds": 60 }
279
+ }
280
+ }
281
+ }
282
+ ```
283
+
284
+ Rules are fixed-window deployment controls keyed by trusted tenant plus
285
+ reviewed capability. There is no separate burst allowance: all configured
286
+ requests may arrive immediately, but the next request is rejected until the
287
+ aligned window resets. They are process-local with SQLite and fleet-wide/atomic
288
+ with shared `runtime_store`. `RATE_LIMITED` includes `retry_after_ms` to that
289
+ reset boundary, records a counter, and creates no proposal. Rate limits are
290
+ runtime wiring and never portable contract fields.
291
+
292
+ ## HTTP metrics
293
+
294
+ ```json
295
+ {
296
+ "metrics": {
297
+ "enabled": true,
298
+ "token_env": "SYNAPSOR_METRICS_TOKEN"
299
+ }
300
+ }
301
+ ```
302
+
303
+ `/metrics` is disabled by default. Non-loopback exposure requires a separate
304
+ metrics token; the MCP bearer does not authorize metrics. Labels are bounded to
305
+ trusted tenant, capability, source, engine, and readiness component. Object
306
+ IDs, principals, URLs, tokens, and raw errors are never labels.
307
+
308
+ ## Operator identity
309
+
310
+ Local development compatibility uses an explicitly unverified environment
311
+ identity:
312
+
313
+ ```json
314
+ {
315
+ "operator_identity": {
316
+ "provider": "dev_env",
317
+ "actor_env": "SYNAPSOR_OPERATOR_ID",
318
+ "roles_env": "SYNAPSOR_OPERATOR_ROLES",
319
+ "apply_roles": ["writeback_operator"]
320
+ }
321
+ }
322
+ ```
323
+
324
+ Shared or production-like operation should register signed operator keys:
325
+
326
+ ```json
327
+ {
328
+ "operator_identity": {
329
+ "provider": "signed_key",
330
+ "apply_roles": ["writeback_operator"],
331
+ "operators": {
332
+ "alice": {
333
+ "public_key_path": "./operators/alice.pub.pem",
334
+ "roles": ["support_reviewer", "writeback_operator"]
335
+ }
336
+ }
337
+ }
338
+ }
339
+ ```
340
+
341
+ The public key path resolves relative to `synapsor.runner.json`. Keep the
342
+ private key outside the repository and invoke decisions with `--identity alice
343
+ --identity-key /secure/path/alice.pem`. Runner verifies key possession and the
344
+ contract's required role, then binds the exact proposal hash/version, action,
345
+ subject, timestamp, roles, signature, and integrity hash into the approval
346
+ ledger. `apply_roles` independently gates writeback. The local browser UI
347
+ refuses approval/rejection in signed-key mode so it cannot bypass this check.
348
+
349
+ For OIDC-style operator identity, use `provider: "jwt_oidc"` with the same
350
+ asymmetric public-key options as session auth, plus one token source:
351
+ `token_env`, `token_file_env`, or `token_stdin: true`. With stdin, pipe or paste
352
+ one token and close stdin; combine it with `--yes` so confirmation does not
353
+ compete for the stream. Also configure `roles_claim`, `subject_claim`, and an
354
+ `attestation_secret_env` containing at least 32 bytes. Runner persists verified
355
+ subject/roles/issuer/key ID and an attested decision proof, never the bearer
356
+ token. Tokens must not be passed as command-line arguments.
154
357
 
155
358
  ## Cloud mode
156
359
 
@@ -0,0 +1,349 @@
1
+ # Running A Small Runner Fleet
2
+
3
+ Synapsor Runner 1.1 supports a bounded small-fleet deployment. It does not
4
+ turn the OSS package into Synapsor Cloud and it does not promise an unbounded,
5
+ multi-region, or SLA-backed control plane.
6
+
7
+ ```text
8
+ TLS load balancer
9
+ -> N stateless Streamable HTTP Runners
10
+ -> shared Postgres runtime ledger
11
+ -> bounded Postgres/MySQL source pools
12
+ -> supervised writeback workers
13
+ -> restricted source writer or app-owned executor
14
+ ```
15
+
16
+ The model still receives only reviewed semantic tools. Approval, apply,
17
+ operator credentials, database credentials, and shared-ledger authority remain
18
+ outside MCP.
19
+
20
+ ## Audit Before Deployment
21
+
22
+ Audit the model-facing surface before connecting a production-like database:
23
+
24
+ ```bash
25
+ npx -y @synapsor/runner audit --example dangerous-db-mcp
26
+ synapsor-runner audit ./tools-list.json
27
+ synapsor-runner audit https://mcp.example.com --bearer-env MCP_AUDIT_TOKEN
28
+ ```
29
+
30
+ Treat generic SQL/query tools, model-controlled tenant/principal inputs, and
31
+ model-facing approval/apply tools as blockers. Audit is a static review, not a
32
+ security proof; follow it with `tools preview`, `doctor`, and the live fleet
33
+ verification.
34
+
35
+ ## What 1.1 Guarantees
36
+
37
+ With `storage.shared_postgres.mode = "runtime_store"`:
38
+
39
+ - each mutation holds one schema-scoped Postgres advisory lock;
40
+ - concurrent schema migration is serialized under that lock;
41
+ - two instances cannot create two active proposals for the same canonical
42
+ tenant/capability/object tuple;
43
+ - distinct verified reviewers append decisions without losing one another;
44
+ - one worker claim/effect completes, conflicts, retries, or enters dead letter;
45
+ - direct SQL and app-owned handler retries use durable source-side receipts;
46
+ - fixed-window rate limits are atomic across instances and keyed from verified
47
+ tenant context plus reviewed capability;
48
+ - CLI and local UI review reads use the same shared queue as MCP serving.
49
+
50
+ The current implementation restores the shared ledger into a transient SQLite
51
+ store while holding the advisory lock, performs one bounded operation, then
52
+ syncs the result. `max_entries` defaults to 10,000 and may be set from 100 to
53
+ 100,000. Runner fails closed with `POSTGRES_RUNTIME_STORE_CAPACITY_EXCEEDED`
54
+ rather than copying an unbounded ledger. This serialized design is appropriate
55
+ for a small self-hosted fleet, not high-throughput horizontal scale.
56
+
57
+ ## Verified Local Topology
58
+
59
+ The repository includes a synthetic Compose fixture:
60
+
61
+ ```bash
62
+ docker compose --profile fleet \
63
+ -f examples/runner-fleet/docker-compose.yml up --build -d --wait
64
+ ```
65
+
66
+ The stronger verification command is:
67
+
68
+ ```bash
69
+ corepack pnpm test:fleet
70
+ ```
71
+
72
+ Prerequisites: Node 22, pnpm through Corepack, Docker Engine, and Docker
73
+ Compose. It needs no AWS, Cloud account, external identity provider, or
74
+ internet service after container images and dependencies are available.
75
+
76
+ Latest verified result: **2026-07-12 (America/Los_Angeles), PASS**. Assertions:
77
+
78
+ ```text
79
+ two claim-bound Runners share one bounded Postgres ledger
80
+ cross-tenant reads are denied
81
+ one active proposal survives concurrent creation
82
+ rate limits hold across instances
83
+ two verified reviewers satisfy a 2-person quorum
84
+ apply is blocked at 1/2 and succeeds at 2/2
85
+ simultaneous reviewer processes preserve both decisions
86
+ two competing workers create exactly one source effect
87
+ worker termination before write recovers without an effect
88
+ worker termination during an open write transaction rolls back and recovers
89
+ worker termination after commit recovers as already-applied
90
+ source-down, read-only-ledger, and timeout readiness failures recover without restart
91
+ shared dead letters requeue or discard with receipts and events preserved
92
+ Postgres and MySQL pool queues fail fast at configured bounds
93
+ backup digest and clean restore match
94
+ retention archives before delete and preserves active proposals
95
+ ```
96
+
97
+ All source rows, identities, keys, tokens, and credentials are synthetic. The
98
+ test tears down its volumes.
99
+
100
+ ## Claim-Bound Sessions
101
+
102
+ Use asymmetric verification for a networked fleet:
103
+
104
+ ```json
105
+ {
106
+ "trusted_context": {
107
+ "provider": "http_claims",
108
+ "values": {
109
+ "tenant_id_key": "tenant_id",
110
+ "principal_key": "sub"
111
+ }
112
+ },
113
+ "session_auth": {
114
+ "provider": "jwt_asymmetric",
115
+ "algorithms": ["RS256"],
116
+ "jwks_url_env": "SYNAPSOR_SESSION_JWKS_URL",
117
+ "issuer": "https://identity.example",
118
+ "audience": "synapsor-runner",
119
+ "tenant_claim": "tenant_id",
120
+ "principal_claim": "sub",
121
+ "clock_skew_seconds": 30,
122
+ "jwks_cache_seconds": 600,
123
+ "jwks_cooldown_seconds": 30,
124
+ "fetch_timeout_ms": 3000,
125
+ "max_response_bytes": 1048576
126
+ }
127
+ }
128
+ ```
129
+
130
+ Every served capability context must bind tenant and principal from
131
+ `HTTP_CLAIM`. Runner rejects an environment-bound contract mixed into this
132
+ server with `TRUSTED_CONTEXT_PROVIDER_CONFLICT` before tools are served.
133
+
134
+ Use an HTTPS JWKS URL from an allowlisted identity system. Runner rejects
135
+ redirects, oversized responses, unknown algorithms, `none`, missing/unknown
136
+ `kid`, bad issuer/audience, expired/not-yet-valid tokens, and private JWK
137
+ material. Unknown `kid` triggers at most one controlled refresh. For offline
138
+ deployments, configure one public PEM through `public_key_env` or
139
+ `public_key_path`; never place a private key in Runner config.
140
+
141
+ ## Ledger And Source Pools
142
+
143
+ ```json
144
+ {
145
+ "storage": {
146
+ "shared_postgres": {
147
+ "mode": "runtime_store",
148
+ "url_env": "SYNAPSOR_LEDGER_DATABASE_URL",
149
+ "schema": "synapsor_runner",
150
+ "lock_timeout_ms": 5000,
151
+ "max_entries": 10000
152
+ }
153
+ },
154
+ "sources": {
155
+ "app_postgres": {
156
+ "engine": "postgres",
157
+ "read_url_env": "APP_POSTGRES_READ_URL",
158
+ "write_url_env": "APP_POSTGRES_WRITE_URL",
159
+ "statement_timeout_ms": 3000,
160
+ "pool": {
161
+ "max_connections": 4,
162
+ "connection_timeout_ms": 2000,
163
+ "idle_timeout_ms": 30000,
164
+ "queue_timeout_ms": 1000,
165
+ "queue_limit": 16
166
+ }
167
+ }
168
+ }
169
+ }
170
+ ```
171
+
172
+ Budget database connections across every replica and worker:
173
+
174
+ ```text
175
+ replicas * sum(source max_connections) + readiness/admin headroom
176
+ ```
177
+
178
+ Keep that below database role and instance limits. Queue overflow fails with
179
+ `SOURCE_POOL_QUEUE_FULL`; acquisition timeout fails with
180
+ `SOURCE_POOL_TIMEOUT`. Neither waits indefinitely or creates a proposal.
181
+
182
+ Use separate roles:
183
+
184
+ - source reader: `CONNECT`, schema `USAGE`, and `SELECT` only on reviewed
185
+ tables/views;
186
+ - direct writer: reviewed column-level `UPDATE` plus source receipt-table
187
+ `SELECT/INSERT/UPDATE`;
188
+ - ledger role: only the configured ledger schema and migration rights;
189
+ - migration administrator: run migrations separately when Runner should not
190
+ own DDL authority.
191
+
192
+ ## Fleet-Wide Rate Limits
193
+
194
+ ```json
195
+ {
196
+ "rate_limits": {
197
+ "enabled": true,
198
+ "default": { "requests": 60, "window_seconds": 60 },
199
+ "capabilities": {
200
+ "billing.propose_refund": { "requests": 10, "window_seconds": 60 }
201
+ }
202
+ }
203
+ }
204
+ ```
205
+
206
+ Semantics are fixed-window, keyed by a hash of verified tenant ID and
207
+ capability. There is no separate burst allowance: a tenant may consume the
208
+ entire configured count immediately, then must wait for the aligned window to
209
+ reset. In local SQLite mode limits are process-local. In `runtime_store` mode
210
+ they use an atomic shared Postgres bucket. Rejection returns `RATE_LIMITED` and
211
+ `retry_after_ms` to the reset boundary, increments a metric, and creates no
212
+ proposal.
213
+
214
+ ## Probes And Metrics
215
+
216
+ - `GET /healthz`: unauthenticated cheap process liveness; no dependency
217
+ checks or configuration inventory.
218
+ - `GET /readyz`: 200 only when catalog, required sources, authoritative ledger,
219
+ and required writeback/executor dependencies are ready; otherwise 503.
220
+ - `GET /metrics`: disabled unless configured. A non-loopback bind requires a
221
+ separate metrics bearer token.
222
+
223
+ ```json
224
+ {
225
+ "metrics": {
226
+ "enabled": true,
227
+ "token_env": "SYNAPSOR_METRICS_TOKEN"
228
+ }
229
+ }
230
+ ```
231
+
232
+ Do not use the model-facing MCP token as the metrics token. Metrics include
233
+ controlled tenant/capability/source/component labels, never object IDs,
234
+ principals, database URLs, tokens, or raw errors.
235
+
236
+ ## Reviewers, Quorum, And Workers
237
+
238
+ Use `signed_key` or `jwt_oidc` operator identity. `dev_env` is explicitly
239
+ unverified and is only for local fixtures. A capability may require multiple
240
+ distinct reviewers in the canonical contract:
241
+
242
+ ```sql
243
+ APPROVAL REQUIRED ROLE billing_lead
244
+ REQUIRE 2 APPROVALS
245
+ ```
246
+
247
+ The same verified subject cannot fill two slots. Policy auto-approval does not
248
+ satisfy a multi-human quorum. Apply and workers reject the proposal until
249
+ progress reaches `N/N`. A rejection is terminal and auditable.
250
+
251
+ Use the shared queue explicitly:
252
+
253
+ ```bash
254
+ synapsor-runner proposals list --config ./synapsor.runner.json
255
+ synapsor-runner proposals show latest --config ./synapsor.runner.json
256
+ synapsor-runner ui --config ./synapsor.runner.json
257
+ synapsor-runner worker run --yes --config ./synapsor.runner.json
258
+ ```
259
+
260
+ Dead-letter operations require verified operator identity:
261
+
262
+ ```bash
263
+ synapsor-runner worker dead-letter list --config ./synapsor.runner.json
264
+ synapsor-runner worker dead-letter show wrp_... --config ./synapsor.runner.json
265
+ synapsor-runner worker dead-letter requeue wrp_... --retry-budget 3 --yes \
266
+ --config ./synapsor.runner.json --identity alice --identity-key /secure/alice.pem
267
+ synapsor-runner worker dead-letter discard wrp_... --reason "closed" --yes \
268
+ --config ./synapsor.runner.json --identity alice --identity-key /secure/alice.pem
269
+ ```
270
+
271
+ Requeue preserves history and refuses if a durable applied/already-applied
272
+ receipt already proves the effect. Discard closes the queue item without
273
+ deleting proposals, receipts, or events.
274
+
275
+ ## Backup, Restore, And Retention
276
+
277
+ ```bash
278
+ synapsor-runner store shared-postgres backup \
279
+ --url-env SYNAPSOR_LEDGER_DATABASE_URL --schema synapsor_runner \
280
+ --output ./ledger-backup.json
281
+ synapsor-runner store shared-postgres verify-backup --input ./ledger-backup.json
282
+ synapsor-runner store shared-postgres restore-backup \
283
+ --input ./ledger-backup.json --url-env SYNAPSOR_LEDGER_DATABASE_URL \
284
+ --schema synapsor_runner_restore --yes
285
+ ```
286
+
287
+ Restore only accepts an empty target schema and verifies the post-restore
288
+ manifest digest. Store archives as sensitive database extracts with owner-only
289
+ permissions.
290
+
291
+ Retention is archive-before-delete:
292
+
293
+ ```bash
294
+ synapsor-runner store shared-postgres retention --older-than 30d \
295
+ --url-env SYNAPSOR_LEDGER_DATABASE_URL --schema synapsor_runner --dry-run
296
+ synapsor-runner store shared-postgres retention --older-than 30d \
297
+ --url-env SYNAPSOR_LEDGER_DATABASE_URL --schema synapsor_runner \
298
+ --output ./ledger-archive.json --yes
299
+ ```
300
+
301
+ The archive is written and digest-verified before deletion. Pending review,
302
+ approved, pending-worker, retry, failed, and dead-letter records are retained.
303
+ The operation appends an immutable retention event.
304
+
305
+ Restore runbook:
306
+
307
+ 1. Stop ingress and workers.
308
+ 2. Verify the backup digest.
309
+ 3. Restore into a clean schema.
310
+ 4. Run `store shared-postgres status` and `/readyz` against the restore.
311
+ 5. Inspect at least one proposal, receipt, and replay through CLI.
312
+ 6. Point one canary Runner at the restored schema.
313
+ 7. Resume ingress, then workers, and watch readiness/dead-letter metrics.
314
+
315
+ ## Shutdown, Rotation, And Upgrades
316
+
317
+ Terminate with `SIGTERM`; Runner stops accepting work, closes HTTP sessions and
318
+ source pools, and releases ledger locks. Give workers enough grace for the
319
+ configured handler timeout. A hard kill is recoverable through leases and
320
+ source-side idempotency receipts, but should remain an incident signal.
321
+
322
+ Rotate JWKS keys by publishing the new `kid`, waiting at least the configured
323
+ cache lifetime, switching issuers, then retiring the old key. Rotate metrics,
324
+ handler, database, and ledger credentials independently through their named
325
+ environment variables.
326
+
327
+ Only homogeneous Runner 1.1 instances are verified by `test:fleet`. Mixed
328
+ 1.0/1.1 operation is not claimed. For rolling upgrades:
329
+
330
+ 1. back up and verify the ledger;
331
+ 2. run schema/config validation with the target version;
332
+ 3. drain one worker and remove one Runner from the load balancer;
333
+ 4. upgrade one canary, verify `/readyz`, MCP tools, proposal inspection, and
334
+ metrics;
335
+ 5. continue one instance at a time;
336
+ 6. upgrade workers after HTTP Runners.
337
+
338
+ Rollback the binary only while contracts/config remain accepted by the prior
339
+ minor. If the prior version does not understand a new canonical field or
340
+ ledger entry, restore the verified pre-upgrade backup into a clean schema and
341
+ roll back the full fleet. Never mix versions merely because both are `1.x`.
342
+
343
+ ## TLS Boundary
344
+
345
+ Terminate public TLS or mTLS at a trusted load balancer, or configure Runner's
346
+ tested TLS/mTLS options directly. Keep Runner-to-database TLS verification on,
347
+ use private networking where possible, restrict `/metrics`, and never forward
348
+ untrusted identity headers as claims. The bearer JWT itself, not a proxy-added
349
+ tenant header, is the session authority.