@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.
- package/CHANGELOG.md +47 -1
- package/README.md +59 -3
- package/THREAT_MODEL.md +29 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/local-ui.d.ts +3 -0
- package/dist/local-ui.d.ts.map +1 -1
- package/dist/runner.mjs +6587 -918
- package/docs/README.md +16 -3
- package/docs/capability-authoring.md +2 -1
- package/docs/conformance.md +5 -0
- package/docs/current-scope.md +6 -2
- package/docs/dsl-reference.md +25 -0
- package/docs/http-mcp.md +45 -1
- package/docs/limitations.md +22 -7
- package/docs/migrating-to-synapsor-spec.md +21 -0
- package/docs/oss-vs-cloud.md +4 -3
- package/docs/production.md +219 -7
- package/docs/release-notes.md +47 -7
- package/docs/release-policy.md +58 -6
- package/docs/runner-config-reference.md +208 -5
- package/docs/running-a-runner-fleet.md +349 -0
- package/examples/runner-fleet/Dockerfile +11 -0
- package/examples/runner-fleet/README.md +52 -0
- package/examples/runner-fleet/docker-compose.yml +76 -0
- package/examples/runner-fleet/mint-dev-token.mjs +19 -0
- package/examples/runner-fleet/seed/mysql.sql +20 -0
- package/examples/runner-fleet/seed/postgres.sql +73 -0
- package/examples/runner-fleet/synapsor.runner.json +105 -0
- package/examples/support-plan-credit/README.md +16 -1
- package/examples/support-plan-credit/contract.synapsor.sql +2 -0
- package/examples/support-plan-credit/synapsor.contract.json +15 -0
- package/fixtures/protocol/MANIFEST.json +1 -1
- package/package.json +3 -1
- package/schemas/change-set.v1.schema.json +2 -1
- package/schemas/synapsor.runner.schema.json +208 -0
package/docs/release-policy.md
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# Release Policy
|
|
2
2
|
|
|
3
|
-
Synapsor Runner `0.1.0`
|
|
4
|
-
|
|
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 `
|
|
61
|
-
through the `
|
|
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 `
|
|
77
|
-
inspection commands should remain the supported way
|
|
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
|
-
{
|
|
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 }`.
|
|
152
|
-
|
|
153
|
-
|
|
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.
|