@synapsor/runner 0.1.16 → 1.0.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.
@@ -26,10 +26,15 @@ Current fixture groups:
26
26
  - `proposal-capability`
27
27
  - `kept-out-fields`
28
28
  - `manual-approval`
29
+ - `auto-approval`
30
+ - `aggregate-policy-limits`
31
+ - `numeric-bounds`
29
32
 
30
33
  The fixture set is intentionally small in 0.1. It covers the runner-supported
31
34
  semantic surface first: trusted context, scoped reads, evidence handles,
32
35
  proposal boundaries, kept-out fields, manual approval, and replay envelopes.
36
+ The aggregate-policy fixture additionally proves that reviewed daily ceilings
37
+ fall back to human review atomically and record the limit that tripped.
33
38
 
34
39
  Additional 0.1 parity coverage currently lives in tests and verification
35
40
  scripts rather than separate `cloud-push/` or `dsl-json-parity/` conformance
@@ -2,7 +2,7 @@
2
2
 
3
3
  The canonical scope page is [Current Limitations](limitations.md).
4
4
 
5
- Current `0.1.x` scope:
5
+ Current `1.0` scope:
6
6
 
7
7
  - local semantic MCP tools for Postgres/MySQL-backed business actions;
8
8
  - schema inspection and guided config generation;
@@ -13,7 +13,7 @@ Current `0.1.x` scope:
13
13
  business transactions;
14
14
  - stdio MCP, Streamable HTTP MCP, and a small JSON-RPC bridge.
15
15
 
16
- Stable `0.1.x` compatibility covers the documented `synapsor-runner` binary,
16
+ Stable `1.x` compatibility covers the documented `synapsor-runner` binary,
17
17
  config schema version `1`, result envelope v2 with v1 opt-out, stdio/Streamable
18
18
  HTTP MCP surfaces, documented MCP client snippets, proposal/evidence/replay
19
19
  inspection commands, direct SQL writeback, and app-owned executor contracts.
@@ -149,6 +149,8 @@ patched column. Values may be identifiers or quoted strings.
149
149
  ```sql
150
150
  APPROVAL ROLE billing_lead
151
151
  AUTO APPROVE WHEN amount_cents <= 2500
152
+ LIMIT 20 PER DAY
153
+ LIMIT TOTAL 100000 PER DAY
152
154
  WRITEBACK DIRECT SQL
153
155
  ```
154
156
 
@@ -160,6 +162,22 @@ Cloud boundary.
160
162
  numeric patched field and must follow `APPROVAL ROLE`. Its maximum cannot exceed
161
163
  the field's `BOUND`. Policy approval still does not apply the write.
162
164
 
165
+ Aggregate limits follow the auto-approval clause:
166
+
167
+ ```sql
168
+ LIMIT 20 PER DAY
169
+ LIMIT TOTAL 100000 PER DAY
170
+ ```
171
+
172
+ `LIMIT n PER DAY` caps policy approvals for the trusted tenant and policy in
173
+ the UTC calendar day. `LIMIT TOTAL n PER DAY` sums the policy's numeric patch
174
+ field over the same scope. Use `PER OBJECT DAY` instead of `PER DAY` to scope a
175
+ limit to one trusted tenant, policy, and business object. The check and approval
176
+ are one atomic ledger transaction. When any ceiling would be exceeded, Runner
177
+ leaves the proposal in `pending_review` and records
178
+ `policy_auto_approval_deferred` with observed, proposed, and projected values.
179
+ It does not reject or auto-apply the proposal.
180
+
163
181
  Writeback forms:
164
182
 
165
183
  | Clause | Meaning |
package/docs/http-mcp.md CHANGED
@@ -72,6 +72,47 @@ sessions: in-memory
72
72
 
73
73
  Use `/mcp` as the MCP endpoint. Health is available at `/healthz`.
74
74
 
75
+ ## TLS And mTLS
76
+
77
+ For a non-local long-running service, terminate TLS at a trusted proxy or start
78
+ Runner with env-backed PEM material:
79
+
80
+ ```bash
81
+ export SYNAPSOR_TLS_CERT_PEM="$(cat ./server.crt)"
82
+ export SYNAPSOR_TLS_KEY_PEM="$(cat ./server.key)"
83
+
84
+ synapsor-runner mcp serve-streamable-http \
85
+ --host 0.0.0.0 \
86
+ --port 8766 \
87
+ --config ./synapsor.runner.json \
88
+ --store ./.synapsor/local.db \
89
+ --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN \
90
+ --tls-cert-env SYNAPSOR_TLS_CERT_PEM \
91
+ --tls-key-env SYNAPSOR_TLS_KEY_PEM
92
+ ```
93
+
94
+ To require client certificates:
95
+
96
+ ```bash
97
+ export SYNAPSOR_TLS_CA_PEM="$(cat ./client-ca.crt)"
98
+
99
+ synapsor-runner mcp serve-streamable-http \
100
+ --host 0.0.0.0 \
101
+ --port 8766 \
102
+ --config ./synapsor.runner.json \
103
+ --store ./.synapsor/local.db \
104
+ --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN \
105
+ --tls-cert-env SYNAPSOR_TLS_CERT_PEM \
106
+ --tls-key-env SYNAPSOR_TLS_KEY_PEM \
107
+ --tls-ca-env SYNAPSOR_TLS_CA_PEM \
108
+ --require-client-cert
109
+ ```
110
+
111
+ The CLI reads PEM contents from environment variables and never prints them.
112
+ Runner-owned mTLS currently protects the Streamable HTTP MCP boundary. For
113
+ app-owned `http_handler` executors, terminate mTLS in your service mesh/proxy
114
+ or handler process and keep bearer/signature checks enabled in the handler.
115
+
75
116
  ## Start The JSON-RPC Bridge
76
117
 
77
118
  ```bash
@@ -6,7 +6,8 @@ Synapsor Runner is intentionally narrow in the current alpha.
6
6
 
7
7
  - Stdio MCP server for semantic database capabilities.
8
8
  - Local read and proposal tools.
9
- - Local SQLite evidence/proposal/query-audit/replay store.
9
+ - Local SQLite evidence/proposal/query-audit/replay store by default.
10
+ - Optional shared Postgres proposal/evidence/replay runtime store for MCP serving.
10
11
  - Human approval through CLI commands.
11
12
  - Public protocol objects:
12
13
  - `synapsor.change-set.v1`
@@ -6,7 +6,8 @@ contract, but they solve different operational problems.
6
6
  Runner is the open-source runtime that stays next to your application and
7
7
  database. It serves reviewed MCP tools, binds trusted context, stores local
8
8
  evidence and proposals, keeps approval outside MCP, applies or routes approved
9
- writeback, and records receipts and replay in a local SQLite ledger.
9
+ writeback, and records receipts and replay in the default local SQLite ledger
10
+ or an opt-in shared Postgres runtime store.
10
11
 
11
12
  Cloud is the team control plane. It stores versioned contracts, produces
12
13
  downloadable Runner bundles, and provides shared activity, evidence, approval,
@@ -18,7 +19,7 @@ and investigation surfaces for enabled design-partner deployments.
18
19
  | Contract source | Local files reviewed in Git | Shared registry with immutable versions and digests |
19
20
  | Trusted context | Local environment/session bindings | Registered bindings plus deployment-specific Cloud session context |
20
21
  | Capabilities | Local semantic MCP tools | Registry, version history, and capability inspection |
21
- | Evidence and replay | Local SQLite ledger | Shared activity and evidence surfaces where enabled |
22
+ | Evidence and replay | Local SQLite ledger by default; optional shared Postgres runtime store | Shared activity and evidence surfaces where enabled |
22
23
  | Approval | Local CLI or localhost UI | Team approval surfaces where enabled |
23
24
  | Writeback | Guarded one-row update or app-owned executor | Cloud-linked jobs with local execution; managed production orchestration is future work |
24
25
  | MCP risk audit | Static local audit | Organization-wide continuous audit is future work |
@@ -150,12 +150,163 @@ Useful commands:
150
150
  ```bash
151
151
  synapsor-runner store stats --store ./.synapsor/local.db
152
152
  synapsor-runner events tail --store ./.synapsor/local.db --follow
153
+ synapsor-runner metrics show --store ./.synapsor/local.db
153
154
  synapsor-runner store prune --store ./.synapsor/local.db --older-than 30d --dry-run
154
155
  synapsor-runner store vacuum --store ./.synapsor/local.db
155
156
  ```
156
157
 
157
158
  Details: [Store Lifecycle](store-lifecycle.md).
158
159
 
160
+ ## Shared Postgres Ledger Setup
161
+
162
+ The default runtime ledger is SQLite. For shared deployments, Runner now ships a
163
+ Postgres ledger setup surface that creates the schema used for shared audit
164
+ entries, proposal locks, and worker leases:
165
+
166
+ ```bash
167
+ export SYNAPSOR_LEDGER_DATABASE_URL="postgresql://ledger_writer:..."
168
+
169
+ synapsor-runner store shared-postgres migration --schema synapsor_runner
170
+ synapsor-runner store shared-postgres apply-migration \
171
+ --url-env SYNAPSOR_LEDGER_DATABASE_URL \
172
+ --schema synapsor_runner \
173
+ --yes
174
+ synapsor-runner store shared-postgres status \
175
+ --url-env SYNAPSOR_LEDGER_DATABASE_URL \
176
+ --schema synapsor_runner
177
+ synapsor-runner store shared-postgres sync \
178
+ --store ./.synapsor/local.db \
179
+ --url-env SYNAPSOR_LEDGER_DATABASE_URL \
180
+ --schema synapsor_runner \
181
+ --yes
182
+ synapsor-runner store shared-postgres restore \
183
+ --store ./.synapsor/restored.db \
184
+ --url-env SYNAPSOR_LEDGER_DATABASE_URL \
185
+ --schema synapsor_runner \
186
+ --yes
187
+ ```
188
+
189
+ These commands do not print the database URL. `sync` upserts a stable snapshot
190
+ of the local proposal/evidence/replay ledger into Postgres for shared audit,
191
+ backup, and retention. `restore` rebuilds a local SQLite store from those
192
+ shared ledger entries for recovery or offline investigation.
193
+
194
+ Runner supports two shared Postgres modes:
195
+
196
+ - `mirror`: bounded CLI mutations restore from Postgres into local SQLite,
197
+ mutate locally, then sync back under a schema-scoped advisory lock.
198
+ - `runtime_store`: MCP serving uses Postgres as the primary
199
+ proposal/evidence/replay store instead of opening a local SQLite store.
200
+
201
+ Local SQLite remains the default. Use `runtime_store` when several MCP sessions
202
+ or runner processes need to share proposal/evidence/replay state through one
203
+ ledger database. Use `mirror` when you want bounded operator handoff while still
204
+ running CLI mutations against a local SQLite file.
205
+
206
+ Mirror mode config:
207
+
208
+ ```json
209
+ {
210
+ "storage": {
211
+ "sqlite_path": "./.synapsor/local.db",
212
+ "shared_postgres": {
213
+ "mode": "mirror",
214
+ "url_env": "SYNAPSOR_LEDGER_DATABASE_URL",
215
+ "schema": "synapsor_runner",
216
+ "lock_timeout_ms": 10000
217
+ }
218
+ }
219
+ }
220
+ ```
221
+
222
+ ```bash
223
+ export SYNAPSOR_LEDGER_DATABASE_URL="postgresql://ledger_writer:..."
224
+
225
+ synapsor-runner doctor \
226
+ --config ./synapsor.runner.json \
227
+ --store ./.synapsor/local.db
228
+
229
+ synapsor-runner propose support.propose_plan_credit --sample \
230
+ --config ./synapsor.runner.json \
231
+ --store ./.synapsor/local.db
232
+
233
+ synapsor-runner proposals approve latest --yes \
234
+ --config ./synapsor.runner.json \
235
+ --store ./.synapsor/local.db
236
+
237
+ synapsor-runner apply --all-approved --yes \
238
+ --config ./synapsor.runner.json \
239
+ --store ./.synapsor/local.db
240
+ ```
241
+
242
+ When `storage.shared_postgres.mode` is `mirror`, `doctor` checks that the
243
+ ledger URL environment variable is present and that `ledger_entries`,
244
+ `proposal_locks`, and `worker_leases` exist in the configured schema. It reports
245
+ environment variable names and table readiness only; it does not print database
246
+ URLs or initialize the schema.
247
+
248
+ Mirror mode restores the Postgres ledger into the local store before a mutation
249
+ and syncs the local store back after the command, including failure events when
250
+ they were recorded locally. While it runs, Runner holds a schema-scoped
251
+ Postgres advisory lock so concurrent mirror-mode operators do not restore,
252
+ mutate, and sync over one another. The default lock wait is 10 seconds; tune it
253
+ with `--shared-ledger-lock-timeout-ms` or
254
+ `SYNAPSOR_SHARED_LEDGER_LOCK_TIMEOUT_MS`.
255
+
256
+ Mirror mode is explicit because the local SQLite store still executes the CLI
257
+ mutation. It is useful for bounded operator workflows and finite workers
258
+ (`worker run --once` or `--drain`); use `runtime_store` for long-lived MCP
259
+ serving and shared worker loops that need one proposal/evidence/replay ledger.
260
+
261
+ Runtime-store mode config:
262
+
263
+ ```json
264
+ {
265
+ "storage": {
266
+ "shared_postgres": {
267
+ "mode": "runtime_store",
268
+ "url_env": "SYNAPSOR_LEDGER_DATABASE_URL",
269
+ "schema": "synapsor_runner",
270
+ "lock_timeout_ms": 10000
271
+ }
272
+ }
273
+ }
274
+ ```
275
+
276
+ When MCP serving starts in `runtime_store` mode, Runner opens the Postgres URL
277
+ from `url_env`, auto-runs the shared-ledger migration, and serializes runtime
278
+ mutations with a transaction-scoped advisory lock. The MCP tools still expose no
279
+ database URLs or write credentials to the model.
280
+
281
+ `runtime_store` covers MCP serving, CLI approval/apply, and supervised worker
282
+ runs. For CLI mutations, Runner restores the shared ledger into a temporary
283
+ local store while holding the Postgres advisory lock, runs the existing local
284
+ mutation, then syncs the resulting ledger entries back to Postgres. A
285
+ long-running `worker run --yes` repeats bounded drain cycles under that lock
286
+ and sleeps between idle polls, so multiple workers can share one Postgres ledger
287
+ without holding the lock while idle.
288
+
289
+ For unattended policy-approved queues, declare reviewed aggregate `LIMIT`
290
+ clauses first, then use the explicit batch command:
291
+
292
+ ```bash
293
+ synapsor-runner apply --all-approved --yes \
294
+ --config ./synapsor.runner.json --store ./.synapsor/local.db \
295
+ --capability support.propose_plan_credit --tenant acme --max 20
296
+ ```
297
+
298
+ Each proposal is independent: a stale-row conflict does not abort later jobs.
299
+ The final summary reports applied, conflict, and skipped IDs. Re-running is
300
+ idempotent through durable receipts. Do not schedule batch apply for a policy
301
+ that has no reviewed aggregate limits.
302
+
303
+ Runner writes newline-delimited JSON events to stderr for model-facing tool
304
+ rejections, operator decisions, and terminal writeback outcomes. These events
305
+ contain safe codes and identifiers, never tool arguments, row values, database
306
+ URLs, tokens, private keys, or free-form driver errors. Prometheus/OpenMetrics
307
+ counters are available with `metrics show` and are grouped by trusted tenant
308
+ and reviewed capability.
309
+
159
310
  ## Restart And Recovery
160
311
 
161
312
  Runner stores proposal/evidence/replay state before writeback.
@@ -183,7 +334,26 @@ synapsor-runner activity search --proposal wrp_... --store ./.synapsor/local.db
183
334
  ### Docker Compose Shape
184
335
 
185
336
  Use Compose to run the MCP server next to your app and database network. Keep
186
- secrets in environment variables or your platform secret manager.
337
+ secrets in environment variables or your platform secret manager. To hydrate
338
+ Runner env vars from AWS Secrets Manager at startup, pass a JSON map through
339
+ `SYNAPSOR_SECRET_MAP`:
340
+
341
+ ```bash
342
+ export SYNAPSOR_SECRET_MAP='{
343
+ "SYNAPSOR_DATABASE_READ_URL": "prod/synapsor/runner#read_url",
344
+ "SYNAPSOR_DATABASE_WRITE_URL": "prod/synapsor/runner#write_url",
345
+ "SYNAPSOR_RUNNER_HTTP_TOKEN": "prod/synapsor/runner#http_token"
346
+ }'
347
+
348
+ synapsor-runner mcp serve-streamable-http \
349
+ --config ./synapsor.runner.json \
350
+ --store ./.synapsor/local.db \
351
+ --secrets-provider aws-secretsmanager-cli \
352
+ --secret-map-env SYNAPSOR_SECRET_MAP
353
+ ```
354
+
355
+ The AWS provider shells out to `aws secretsmanager get-secret-value`; the runner
356
+ logs only how many target env vars were loaded or skipped.
187
357
 
188
358
  ```yaml
189
359
  services:
@@ -1,7 +1,7 @@
1
1
  # Release Notes
2
2
 
3
- These notes track public Synapsor Runner behavior. Starting with `0.1.0`, the
4
- normal install path uses the untagged stable package:
3
+ These notes track public Synapsor Runner behavior. Starting with `1.0.0`, the
4
+ documented production-loop compatibility line uses the untagged stable package:
5
5
 
6
6
  ```bash
7
7
  npx -y -p @synapsor/runner synapsor-runner demo --quick
@@ -12,6 +12,29 @@ for the Synapsor Cloud CLI.
12
12
 
13
13
  ## Unreleased
14
14
 
15
+ ## 1.0.0
16
+
17
+ ### Production Approval Loop
18
+
19
+ - Adds batch apply for approved proposals with independent per-proposal
20
+ outcomes, rerun-safe idempotency, and `--capability`, `--tenant`, and
21
+ `--max` filters.
22
+ - Adds aggregate policy ceilings to canonical contracts and DSL authoring so
23
+ small proposals fall back to human review once daily count or total limits are
24
+ reached.
25
+ - Adds signed operator-key approval/apply enforcement, tamper-evident approval
26
+ records, operational counters/logs, and a supervised local writeback worker.
27
+ - Adds shared Postgres ledger support, runtime-store mode, per-session trusted
28
+ context, managed secret hydration, token rotation hooks, and Streamable HTTP
29
+ mTLS for scale-out deployments.
30
+ - Keeps local SQLite as the default while allowing runtime-store workers to run
31
+ long-lived shared ledger drain loops under a Postgres advisory lock.
32
+ - Declares the first semver contract for the documented CLI, schema, contract,
33
+ MCP result, writeback, approval, metrics, and replay surfaces.
34
+
35
+ Prepared package versions: `@synapsor/spec@1.0.0`,
36
+ `@synapsor/dsl@1.0.0`, and `@synapsor/runner@1.0.0`.
37
+
15
38
  ## 0.1.16
16
39
 
17
40
  ### Fleet-Lab Runner Hardening
@@ -478,10 +501,9 @@ not change and must not be republished for this release.
478
501
 
479
502
  ## Stable Release Policy
480
503
 
481
- Use untagged `@synapsor/runner` for stable installs. Use `@alpha` or an exact
482
- prerelease only when intentionally testing preview behavior. Stable `0.1.x`
483
- releases should keep the compatibility promise documented in
484
- `docs/release-policy.md`.
504
+ Use untagged `@synapsor/runner` for stable installs. Use an exact prerelease
505
+ only when intentionally testing preview behavior. Stable `1.x` releases should
506
+ keep the compatibility promise documented in `docs/release-policy.md`.
485
507
 
486
508
  The first stable `0.1.0` release was gated on:
487
509
 
@@ -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,37 @@ 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
+
58
88
  ## Stable Compatibility Promise
59
89
 
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:
90
+ Starting with `1.0.0`, Synapsor Runner keeps these public surfaces compatible
91
+ through the `1.x` line unless a release note marks a deprecation first:
62
92
 
63
93
  - the `synapsor-runner` binary name and README quickstart commands;
64
94
  - `synapsor.runner.json` schema version `1` for documented fields;
@@ -73,8 +103,9 @@ through the `0.1.x` line unless a release note marks a deprecation first:
73
103
  Stable does not promise production SLA, hosted Cloud features, compliance
74
104
  certification, physical Postgres/MySQL branching, generic SQL writeback,
75
105
  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.
106
+ internals. Local store migrations may happen inside `1.x` and later minor
107
+ versions, but documented CLI inspection commands should remain the supported way
108
+ to read the store.
78
109
 
79
110
  Alpha users should pin an exact alpha version in package.json, CI, and MCP
80
111
  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,7 @@ 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. |
37
38
  | `executors` | No | App-owned writeback wiring. |
38
39
  | `cloud` | Cloud mode | Scoped Cloud adapter configuration. |
39
40
 
@@ -50,13 +51,46 @@ Unknown keys fail when `strict` is true (the default).
50
51
  ## Storage
51
52
 
52
53
  ```json
53
- { "storage": { "sqlite_path": "./.synapsor/local.db" } }
54
+ {
55
+ "storage": {
56
+ "sqlite_path": "./.synapsor/local.db",
57
+ "shared_postgres": {
58
+ "mode": "mirror",
59
+ "url_env": "SYNAPSOR_LEDGER_DATABASE_URL",
60
+ "schema": "synapsor_runner",
61
+ "lock_timeout_ms": 10000
62
+ }
63
+ }
64
+ }
54
65
  ```
55
66
 
56
67
  The ledger stores proposals, visible evidence copies, query audit, approvals,
57
68
  receipts, events, and replay. New POSIX stores are owner-only (`0600`). Protect
58
69
  the file like a database extract; see [Store Lifecycle](store-lifecycle.md).
59
70
 
71
+ `storage.shared_postgres.mode = "mirror"` makes mutating CLI commands restore
72
+ from the shared Postgres ledger before the local mutation and sync back after
73
+ the command while holding a schema-scoped Postgres advisory lock. Mirror mode is
74
+ a bounded operator handoff bridge; MCP serving still uses the local SQLite
75
+ store.
76
+
77
+ `storage.shared_postgres.mode = "runtime_store"` makes MCP serving use the
78
+ shared Postgres ledger as the primary proposal/evidence/replay store. Runner
79
+ creates a connection from the environment variable named by `url_env`, auto-runs
80
+ the shared-ledger migration at startup, and uses a schema-scoped transaction
81
+ advisory lock around runtime mutations. The URL is still referenced by
82
+ environment variable name; never put the database URL in the config file.
83
+ Bounded CLI approval/apply/worker commands bridge through the same Postgres
84
+ ledger by restoring into a temporary local store under the Postgres advisory
85
+ lock, running the existing mutation, and syncing back. Long-running worker
86
+ processes repeat bounded drain cycles and release the advisory lock while idle.
87
+
88
+ `synapsor-runner doctor --config ./synapsor.runner.json` checks shared Postgres
89
+ wiring when this block is present: the URL env var must be set, and the
90
+ configured schema must already contain `ledger_entries`, `proposal_locks`, and
91
+ `worker_leases`. Doctor reports env var names and table readiness only; it does
92
+ not print database URLs or create the schema.
93
+
60
94
  ## Sources
61
95
 
62
96
  ```json
@@ -100,6 +134,33 @@ Providers are `environment`, `static_dev`, `http_claims`, and `cloud_session`.
100
134
  Capabilities may reference a context by name. The model never receives tenant
101
135
  or principal as an overridable argument.
102
136
 
137
+ For multi-tenant Streamable HTTP services, use `http_claims` plus signed
138
+ session auth:
139
+
140
+ ```json
141
+ {
142
+ "trusted_context": {
143
+ "provider": "http_claims",
144
+ "values": {
145
+ "tenant_id_key": "tenant_id",
146
+ "principal_key": "sub"
147
+ }
148
+ },
149
+ "session_auth": {
150
+ "provider": "jwt_hs256",
151
+ "secret_env": "SYNAPSOR_SESSION_JWT_SECRET",
152
+ "previous_secret_env": "SYNAPSOR_PREVIOUS_SESSION_JWT_SECRET",
153
+ "issuer": "https://identity.example",
154
+ "audience": "synapsor-runner"
155
+ }
156
+ }
157
+ ```
158
+
159
+ `previous_secret_env` is optional and is only for rotation windows. Runner tries
160
+ the active secret first, then the previous secret. Existing MCP sessions remain
161
+ bound to the exact token fingerprint, so clients cannot swap tenant/principal
162
+ identity inside an initialized session.
163
+
103
164
  ## Contracts and embedded capabilities
104
165
 
105
166
  ```json
@@ -148,9 +209,53 @@ See [Writeback Executors](writeback-executors.md).
148
209
 
149
210
  `policies` is an array of reviewed policy objects. Local approval policies use
150
211
  `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.
212
+ "max": 2500 }`. Canonical policies may also carry daily `count` or `total`
213
+ limits scoped to `tenant_policy` or `tenant_policy_object`. These limits come
214
+ from reviewed DSL `LIMIT` clauses, not an unreviewed local override. A tripped
215
+ limit falls back to human review and is recorded in the ledger.
216
+ `approvals.disable_auto_approval: true` disables local policy auto-approval
217
+ without changing the reviewed contract. Approval never becomes an MCP tool.
218
+
219
+ ## Operator identity
220
+
221
+ Local development compatibility uses an explicitly unverified environment
222
+ identity:
223
+
224
+ ```json
225
+ {
226
+ "operator_identity": {
227
+ "provider": "dev_env",
228
+ "actor_env": "SYNAPSOR_OPERATOR_ID",
229
+ "roles_env": "SYNAPSOR_OPERATOR_ROLES",
230
+ "apply_roles": ["writeback_operator"]
231
+ }
232
+ }
233
+ ```
234
+
235
+ Shared or production-like operation should register signed operator keys:
236
+
237
+ ```json
238
+ {
239
+ "operator_identity": {
240
+ "provider": "signed_key",
241
+ "apply_roles": ["writeback_operator"],
242
+ "operators": {
243
+ "alice": {
244
+ "public_key_path": "./operators/alice.pub.pem",
245
+ "roles": ["support_reviewer", "writeback_operator"]
246
+ }
247
+ }
248
+ }
249
+ }
250
+ ```
251
+
252
+ The public key path resolves relative to `synapsor.runner.json`. Keep the
253
+ private key outside the repository and invoke decisions with `--identity alice
254
+ --identity-key /secure/path/alice.pem`. Runner verifies key possession and the
255
+ contract's required role, then binds the exact proposal hash/version, action,
256
+ subject, timestamp, roles, signature, and integrity hash into the approval
257
+ ledger. `apply_roles` independently gates writeback. The local browser UI
258
+ refuses approval/rejection in signed-key mode so it cannot bypass this check.
154
259
 
155
260
  ## Cloud mode
156
261
 
@@ -196,10 +196,15 @@ The auto-approval policy lives in `contract.synapsor.sql`:
196
196
 
197
197
  ```sql
198
198
  AUTO APPROVE WHEN plan_credit_cents <= 2500
199
+ LIMIT 20 PER DAY
200
+ LIMIT TOTAL 100000 PER DAY
199
201
  ```
200
202
 
201
203
  That compiles into `synapsor.contract.json` as a reviewed approval policy. It is
202
- stored in git, validated by `@synapsor/spec`, and included in Cloud push payloads.
204
+ stored in git, validated by `@synapsor/spec`, and included in Cloud push
205
+ payloads. The policy may approve at most 20 qualifying credits or 100,000 cents
206
+ in total per trusted tenant and UTC day. The first ceiling reached leaves later
207
+ proposals in `pending_review` and records why the policy deferred to a human.
203
208
 
204
209
  To disable local policy approval without changing the contract:
205
210
 
@@ -214,6 +219,16 @@ To disable local policy approval without changing the contract:
214
219
  Auto-approval never applies the write. It only records an approval row and audit
215
220
  event with actor `policy:<policy_name>`.
216
221
 
222
+ After reviewing the ceilings and running `doctor`, an operator can drain the
223
+ approved queue without letting one stale-row conflict abort the remaining jobs:
224
+
225
+ ```bash
226
+ synapsor-runner apply --all-approved --yes \
227
+ --capability support.propose_plan_credit --tenant acme --max 20 \
228
+ --config examples/support-plan-credit/synapsor.runner.json \
229
+ --store ./tmp/support-plan-credit/local.db
230
+ ```
231
+
217
232
  ## Cloud
218
233
 
219
234
  Dry-run the Cloud payload:
@@ -46,6 +46,8 @@ CREATE CAPABILITY support.propose_plan_credit
46
46
  BOUND plan_credit_cents 1..50000
47
47
  APPROVAL ROLE support_reviewer
48
48
  AUTO APPROVE WHEN plan_credit_cents <= 2500
49
+ LIMIT 20 PER DAY
50
+ LIMIT TOTAL 100000 PER DAY
49
51
  WRITEBACK DIRECT SQL
50
52
  END
51
53