@synapsor/runner 1.6.2 → 1.6.3

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 (47) hide show
  1. package/CHANGELOG.md +77 -2
  2. package/README.md +33 -19
  3. package/SECURITY.md +10 -2
  4. package/THREAT_MODEL.md +27 -4
  5. package/dist/authoring.mjs +53 -3
  6. package/dist/cli.d.ts +19 -2
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/local-ui.d.ts +85 -1
  9. package/dist/local-ui.d.ts.map +1 -1
  10. package/dist/runner.mjs +36107 -22949
  11. package/dist/runtime.mjs +3581 -237
  12. package/dist/shadow.mjs +2423 -65
  13. package/docs/README.md +22 -2
  14. package/docs/agent-guided-setup.md +239 -0
  15. package/docs/approval-roles-and-operator-identity.md +353 -0
  16. package/docs/auto-boundary-and-scoped-explore.md +32 -6
  17. package/docs/capability-authoring.md +19 -0
  18. package/docs/current-scope.md +20 -1
  19. package/docs/dsl-json-parity.md +75 -0
  20. package/docs/dsl-reference.md +10 -0
  21. package/docs/fresh-developer-usability.md +155 -80
  22. package/docs/getting-started-own-database.md +5 -4
  23. package/docs/guarded-crud-writeback.md +19 -0
  24. package/docs/guided-onboarding.md +331 -0
  25. package/docs/human-attention-notifications.md +367 -0
  26. package/docs/limitations.md +19 -3
  27. package/docs/local-mode.md +16 -2
  28. package/docs/migrating-to-synapsor-spec.md +38 -0
  29. package/docs/production.md +39 -0
  30. package/docs/release-notes.md +66 -2
  31. package/docs/runner-config-reference.md +132 -0
  32. package/docs/running-a-runner-fleet.md +16 -1
  33. package/docs/security-boundary.md +24 -0
  34. package/docs/store-lifecycle.md +20 -1
  35. package/docs/supervised-automatic-apply.md +267 -0
  36. package/docs/troubleshooting-first-run.md +35 -0
  37. package/examples/fitflow-guided-onboarding/README.md +43 -0
  38. package/examples/fitflow-guided-onboarding/app/page.tsx +8 -0
  39. package/examples/fitflow-guided-onboarding/docker-compose.yml +16 -0
  40. package/examples/fitflow-guided-onboarding/package.json +18 -0
  41. package/examples/fitflow-guided-onboarding/prisma/schema.prisma +98 -0
  42. package/examples/fitflow-guided-onboarding/seed/postgres.sql +401 -0
  43. package/examples/operator-oidc/issuer.mjs +188 -0
  44. package/examples/support-plan-credit/README.md +6 -2
  45. package/package.json +5 -3
  46. package/schemas/schema-candidate-review.schema.json +42 -0
  47. package/schemas/synapsor.runner.schema.json +227 -4
package/docs/README.md CHANGED
@@ -18,6 +18,10 @@ no-database demo, wire your database, then read deeper concepts.
18
18
  you adopt Runner.
19
19
  - [README](../README.md): audit-first proof, no-database demo, safety diagram,
20
20
  and the shortest own-database path.
21
+ - [Database To First Safe Tool](guided-onboarding.md): one public command,
22
+ five-minute first read, ten-minute Explore to Protect, fifteen-minute
23
+ proposal, resumable setup, guarded development writeback, and host-neutral
24
+ client handoff.
21
25
  - [Troubleshooting First Run](troubleshooting-first-run.md): common first-run
22
26
  failures, redacted diagnostics, and fixes.
23
27
 
@@ -47,6 +51,8 @@ no-database demo, wire your database, then read deeper concepts.
47
51
 
48
52
  ## 04 Connect Your DB
49
53
 
54
+ - [Database To First Safe Tool](guided-onboarding.md): the complete
55
+ review-by-exception golden path using the packed npm package.
50
56
  - [Auto Boundary, Scoped Explore, And
51
57
  Protect](auto-boundary-and-scoped-explore.md): deterministically inspect a
52
58
  whole staging application, review one digest-bound boundary, ask bounded row
@@ -95,6 +101,9 @@ no-database demo, wire your database, then read deeper concepts.
95
101
  - [Capability Authoring](capability-authoring.md): define read/proposal
96
102
  capabilities, model-facing descriptions, result envelopes, trusted context,
97
103
  and writeback guards.
104
+ - [Approval Roles And Verified Operator Identity](approval-roles-and-operator-identity.md):
105
+ trace `APPROVAL ROLE` through IdP claims, immutable approval evidence, and
106
+ the independent apply-role decision.
98
107
  - [DSL Reference](dsl-reference.md): complete supported grammar, clause order,
99
108
  compiled meaning, and constraints.
100
109
  - [Runner Config Reference](runner-config-reference.md): every public wiring
@@ -141,6 +150,14 @@ no-database demo, wire your database, then read deeper concepts.
141
150
 
142
151
  - [Local Mode](local-mode.md): local store, proposals, approval, replay, and
143
152
  writeback flow.
153
+ - [Operator-Supervised Automatic Apply](supervised-automatic-apply.md):
154
+ separate approval from execution, require contract and deployment opt-in for
155
+ one exact digest, run the trusted worker, and understand revalidation,
156
+ eligibility, retries, controls, and UNKNOWN outcomes.
157
+ - [Human Attention And Notifications](human-attention-notifications.md):
158
+ durable attention events, the no-ID Workbench inbox, quiet defaults,
159
+ coalescing and digests, signed generic webhooks, delivery recovery, and the
160
+ optional supervision-health gate.
144
161
  - [Shadow Studies](shadow-studies.md): compare what an agent would propose
145
162
  with explicit authorized outcomes before granting write authority.
146
163
  - [Writeback Executors](writeback-executors.md): app-owned writeback handlers
@@ -162,8 +179,11 @@ no-database demo, wire your database, then read deeper concepts.
162
179
  - [Store Lifecycle](store-lifecycle.md): active-store leases, prune safety,
163
180
  deleted-store behavior, and concurrent server guardrails.
164
181
  - `synapsor-runner activity search`, `evidence`, `query-audit`, `receipts`,
165
- `events tail`, and `events webhook`: local evidence, audit, receipt, replay,
166
- and lifecycle inspection.
182
+ `events tail`, `attention show`, and `notifications status`: local evidence,
183
+ audit, receipt, replay, complete lifecycle history, and human-attention
184
+ inspection. The legacy development lifecycle webhook remains documented in
185
+ [Local Mode](local-mode.md); production attention delivery uses the signed,
186
+ quiet notification pipeline.
167
187
  - `examples/mysql-refund-agent/`: MySQL order/refund review example using the
168
188
  same proposal, approval, guarded writeback, and replay loop.
169
189
  - [Scoped Ledger Reports](compliance-reports.md): object/principal JSON,
@@ -0,0 +1,239 @@
1
+ # Agent-Guided Synapsor Setup
2
+
3
+ This guide lets Claude Code, Codex, Cursor, or another coding agent help a
4
+ developer set up Synapsor Runner without taking over a human security decision.
5
+ It uses the same local-first flow a developer runs directly. No Synapsor
6
+ account, Cloud login, model API key, global installation, or database
7
+ replication is required.
8
+
9
+ ## Copyable Prompt
10
+
11
+ ```text
12
+ Set up Synapsor Runner for this project.
13
+
14
+ Use DATABASE_URL from my environment. Run the setup yourself and explain each
15
+ step briefly. Stop only when I must review the data boundary or make another
16
+ human-authority decision. Never ask me to paste credentials into chat, and
17
+ never activate, approve, or apply anything for me.
18
+
19
+ Finish by driving one safe tool, showing what the agent can and cannot access,
20
+ and giving me the MCP configuration for my client.
21
+ ```
22
+
23
+ ## Rules For The Coding Agent
24
+
25
+ The coding agent may inspect project files, check tool availability, run the
26
+ documented CLI, explain failures, and resume completed work.
27
+
28
+ The coding agent must not:
29
+
30
+ - ask the user to paste a database URL, password, token, or private key into
31
+ chat;
32
+ - print or persist the value of `DATABASE_URL` or another credential;
33
+ - choose a tenant or principal boundary;
34
+ - decide that a sensitive or uncertain field is safe to expose;
35
+ - activate an exploration boundary or named capability;
36
+ - approve, reject, apply, reconcile, or revert a proposal;
37
+ - run writeback setup with `--apply`;
38
+ - weaken a failed role, RLS, schema-lock, profile, or transport check;
39
+ - replace an existing generated project or use `--force` without a human
40
+ choosing the destructive path;
41
+ - fetch or execute setup code other than the documented npm package command.
42
+
43
+ The model-facing MCP surface never contains activation, approval, or apply
44
+ tools. A coding agent operating the terminal must preserve the same separation.
45
+
46
+ ## Guided Procedure
47
+
48
+ ### 1. Verify Local Prerequisites
49
+
50
+ Run:
51
+
52
+ ```bash
53
+ node --version
54
+ test -n "${DATABASE_URL:-}"
55
+ npx -y @synapsor/runner@latest --version
56
+ ```
57
+
58
+ Do not run `echo "$DATABASE_URL"` or otherwise reveal its value. Runner
59
+ requires Node 22.13 or newer.
60
+
61
+ Report one sentence:
62
+
63
+ ```text
64
+ Node, the Runner package, and an exported DATABASE_URL are available.
65
+ Next: inspect metadata and open the local boundary review.
66
+ ```
67
+
68
+ If `DATABASE_URL` is absent, stop and say:
69
+
70
+ ```text
71
+ DATABASE_URL is not exported in this shell. Export it locally, without pasting
72
+ it into chat, then ask me to continue.
73
+ ```
74
+
75
+ ### 2. Start Or Resume The Guided Project
76
+
77
+ From the intended project directory, run:
78
+
79
+ ```bash
80
+ npx -y @synapsor/runner@latest start --from-env DATABASE_URL
81
+ ```
82
+
83
+ For a fresh interactive project, Runner performs metadata-only inspection,
84
+ creates disabled review artifacts, validates the generated project, initializes
85
+ the local ledger, and starts the secured loopback Workbench. It prints one
86
+ local URL if a browser cannot be opened.
87
+
88
+ For an existing guided project, the same command resumes it. Resume and Try do
89
+ not rescan the database, rewrite files, or change a digest. Do not add
90
+ `--force`. A schema rescan or reset is a separate human choice in Workbench.
91
+
92
+ Report:
93
+
94
+ ```text
95
+ Runner created or resumed a disabled local boundary. No source rows were read
96
+ and the database was not changed.
97
+ Next: review the security exceptions in Workbench.
98
+ ```
99
+
100
+ Then stop. The human must review:
101
+
102
+ - fields kept out and fields still uncertain;
103
+ - exact row identity;
104
+ - trusted tenant and principal scope;
105
+ - aggregate-safe dimensions and measures;
106
+ - database-role and RLS posture;
107
+ - the exact digest being activated.
108
+
109
+ Do not click or call activation endpoints for the user.
110
+
111
+ ### 3. Continue After Human Activation
112
+
113
+ After the user confirms that Workbench shows an active reviewed boundary,
114
+ continue in the same Workbench session. The built-in Explore form performs the
115
+ first bounded read and PM-style aggregate without SQL or plan JSON.
116
+
117
+ The coding agent may verify the active local surface without copying an opaque
118
+ handle:
119
+
120
+ ```bash
121
+ npx -y @synapsor/runner@latest try call --list --format json
122
+ ```
123
+
124
+ If no named capability is active yet, that is expected: a temporary local
125
+ Scoped Explore boundary is not a production named tool. Ask the user to run the
126
+ suggested aggregate in Workbench and select **Protect this analysis**.
127
+
128
+ Protect creates public DSL, canonical JSON, tests, and a disabled named
129
+ capability. Stop again for exact-digest human activation. Do not activate it
130
+ for the user.
131
+
132
+ ### 4. Verify The First Named Tool
133
+
134
+ After the human activates a protected named capability, run:
135
+
136
+ ```bash
137
+ npx -y @synapsor/runner@latest try call --list --format json
138
+ ```
139
+
140
+ Use the tool name returned by that command. For a tool with generated sample
141
+ input, run:
142
+
143
+ ```bash
144
+ npx -y @synapsor/runner@latest try call <tool-name> --sample --json
145
+ ```
146
+
147
+ Do not invent tenant or principal arguments. Trusted scope comes from the
148
+ reviewed environment/session binding outside model input.
149
+
150
+ Report:
151
+
152
+ ```text
153
+ Your first safe named tool is working.
154
+ The result used only reviewed fields and trusted scope.
155
+ Source database changed: no.
156
+ Next: connect the same reviewed tool surface to your MCP client.
157
+ ```
158
+
159
+ ### 5. Present MCP Setup
160
+
161
+ Workbench displays ready-to-copy configurations for:
162
+
163
+ - Cursor project MCP;
164
+ - Claude-compatible local stdio MCP;
165
+ - Codex;
166
+ - generic stdio MCP.
167
+
168
+ All hosts launch the same reviewed Runner project. None receives a broader
169
+ authority surface.
170
+
171
+ For a terminal-rendered generic configuration, run:
172
+
173
+ ```bash
174
+ npx -y @synapsor/runner@latest mcp config --absolute-paths
175
+ ```
176
+
177
+ Do not write into a client configuration without explicit user consent. Cursor
178
+ has a separate consent-gated installer:
179
+
180
+ ```bash
181
+ npx -y @synapsor/runner@latest mcp install cursor --project --dry-run
182
+ ```
183
+
184
+ Show the dry-run result first. Installation remains a human choice.
185
+
186
+ ## Guided Write Action
187
+
188
+ After the first safe read, Workbench can generate one bounded write action.
189
+ Schema structure supplies candidates only. The human decides business intent,
190
+ allowed fields, values or transitions, trusted scope, version guard, reviewer,
191
+ limits, optional bounded policy approval, receipt mode, and compensation.
192
+
193
+ The generated action starts disabled. Its first model-facing call creates an
194
+ exact proposal and must say:
195
+
196
+ ```text
197
+ Proposal created.
198
+ Source database changed: no.
199
+ The model cannot approve or apply this proposal.
200
+ ```
201
+
202
+ Stop at action activation, proposal approval, writeback setup, apply, and
203
+ revert. Those are human or trusted-operator decisions outside MCP.
204
+
205
+ Manual apply is the default. An operator may separately configure
206
+ digest-bound supervised execution for eligible production actions. In that
207
+ mode, the response must disclose that reviewed policy may approve and a trusted
208
+ worker may later apply the proposal without a per-request click. The coding
209
+ agent still cannot enable, start, pause, configure, or control that worker. See
210
+ [Operator-Supervised Automatic Apply](supervised-automatic-apply.md).
211
+
212
+ ## Recovery
213
+
214
+ Use the one next action Runner prints. Preserve the current project and review
215
+ state.
216
+
217
+ | Failure | Required response |
218
+ | --- | --- |
219
+ | Connection failed | Confirm the local environment variable and network path without printing the credential, then rerun `start`. |
220
+ | Privileged, writable, owner, `BYPASSRLS`, or unverifiable read role | Stop source-row exploration. Ask the user to supply a verifiably read-only role. |
221
+ | Tenant, principal, or row identity unresolved | Stop and direct the human to the highlighted Workbench exception. |
222
+ | Sensitive field blocked | Keep it out unless a human records a reasoned field decision. |
223
+ | Existing generated project | Run the same `start` command and choose Resume. Do not use `--force`. |
224
+ | Stale generation lock | Preserve active named capabilities; choose Rescan and review changes. |
225
+ | Workbench port unavailable | Use the single alternate loopback URL Runner prints or free the occupied local port. |
226
+ | Writeback prerequisites missing | Keep the proposal unchanged. Use `writeback setup` for a preview only and stop for human review. |
227
+
228
+ ## What The Developer Should Understand
229
+
230
+ At the end, ask the developer to explain these four facts:
231
+
232
+ 1. The model never receives raw SQL authority.
233
+ 2. Reads are limited to reviewed fields under trusted tenant and principal
234
+ scope.
235
+ 3. Model-facing writes create proposals rather than committing changes.
236
+ 4. The model cannot activate authority, approve proposals, or apply writes.
237
+
238
+ If any answer is unclear, reopen the Workbench Overview. Do not expand authority
239
+ to make onboarding appear successful.
@@ -0,0 +1,353 @@
1
+ # Approval Roles And Verified Operator Identity
2
+
3
+ This guide explains how a role written in a Synapsor capability becomes a
4
+ verified human approval. The short version is:
5
+
6
+ ```text
7
+ Contract says who may approve
8
+ -> identity provider says who the operator is and which groups they hold
9
+ -> Runner verifies a fresh token and exact role
10
+ -> ledger records the exact decision without storing the bearer token
11
+ -> a separate verified operator authorizes apply
12
+ ```
13
+
14
+ Approval and apply are outside the model-facing MCP tool surface. An agent
15
+ cannot supply an operator token, select its own role, approve a proposal, or
16
+ commit a write.
17
+
18
+ ## What `APPROVAL ROLE` Means
19
+
20
+ This public DSL:
21
+
22
+ ```sql
23
+ CREATE CAPABILITY membership.freeze_membership
24
+ ...
25
+ APPROVAL ROLE membership_reviewer
26
+ ...
27
+ ;
28
+ ```
29
+
30
+ means that an operator approving this capability's proposal must have the exact
31
+ role string `membership_reviewer` in a verified identity.
32
+
33
+ `membership_reviewer` is an author-chosen Synapsor contract role. It is not:
34
+
35
+ - a PostgreSQL role;
36
+ - an operating-system user or group;
37
+ - an MCP or model role;
38
+ - an API key or secret;
39
+ - permission to apply the write.
40
+
41
+ The contract fixes the required role. The model cannot put a different role in
42
+ a tool argument.
43
+
44
+ ## The Complete Identity Chain
45
+
46
+ 1. A contract author writes `APPROVAL ROLE membership_reviewer`.
47
+ 2. An identity administrator maps real people to an IdP group. For example,
48
+ members of `example-membership-reviewers` receive
49
+ `membership_reviewer` in the token's `groups` claim.
50
+ 3. Runner's `operator_identity` configuration fixes the token source, allowed
51
+ algorithm, public-key source, issuer, audience, subject claim, roles claim,
52
+ and apply roles.
53
+ 4. For every approval, Runner reads a fresh token from environment, a protected
54
+ token file, or stdin.
55
+ 5. Runner verifies the asymmetric algorithm, signature and JWKS key, issuer,
56
+ audience, `exp`, and `nbf`. It then reads a safe subject and the configured
57
+ roles claim.
58
+ 6. Runner requires an exact role match. `membership_reviewer_backup` does not
59
+ satisfy `membership_reviewer`.
60
+ 7. The approval is bound to the immutable proposal ID, version, and hash, the
61
+ action `approve`, the verified subject, and the decision time.
62
+ 8. Runner stores the verified identity metadata and an attested decision proof.
63
+ It never stores the bearer token.
64
+ 9. Apply is a second operator decision. A fresh verified identity must satisfy
65
+ `operator_identity.apply_roles`; approval does not imply commit authority.
66
+
67
+ At apply time Runner also verifies the stored approval proof against the exact
68
+ proposal. A changed proposal ID, version, hash, role, subject, decision, or
69
+ attestation fails before source mutation.
70
+
71
+ ## Development-Only Identity
72
+
73
+ For one-user local development, `dev_env` preserves the lightweight workflow:
74
+
75
+ ```json
76
+ {
77
+ "operator_identity": {
78
+ "provider": "dev_env",
79
+ "actor_env": "SYNAPSOR_OPERATOR_ID",
80
+ "roles_env": "SYNAPSOR_OPERATOR_ROLES",
81
+ "apply_roles": ["writeback_operator"]
82
+ }
83
+ }
84
+ ```
85
+
86
+ ```bash
87
+ export SYNAPSOR_OPERATOR_ID="local-reviewer"
88
+ export SYNAPSOR_OPERATOR_ROLES="membership_reviewer,writeback_operator"
89
+
90
+ synapsor-runner proposals approve latest \
91
+ --config ./synapsor.runner.json \
92
+ --store ./.synapsor/local.db \
93
+ --yes
94
+ ```
95
+
96
+ `dev_env` is explicitly unverified. Environment text is trusted as entered. Do
97
+ not use it for a shared Runner, production, or a decision that must prove who
98
+ approved it. Use `jwt_oidc` or `signed_key` there.
99
+
100
+ ## Production-Style OIDC Configuration
101
+
102
+ The following block is parsed and used by the packed OIDC integration test.
103
+ The simulated issuer uses the same shape, but this is not Okta, Entra, Auth0,
104
+ or any other vendor certification.
105
+
106
+ <!-- synapsor-oidc-operator-config:start -->
107
+ ```json
108
+ {
109
+ "provider": "jwt_oidc",
110
+ "apply_roles": ["writeback_operator"],
111
+ "token_env": "SYNAPSOR_OPERATOR_TOKEN",
112
+ "roles_claim": "groups",
113
+ "subject_claim": "sub",
114
+ "attestation_secret_env": "SYNAPSOR_OPERATOR_ATTESTATION_SECRET",
115
+ "algorithms": ["RS256"],
116
+ "jwks_url_env": "SYNAPSOR_OPERATOR_JWKS_URL",
117
+ "issuer": "https://identity.example.test/oidc",
118
+ "audience": "synapsor-operators",
119
+ "clock_skew_seconds": 0,
120
+ "jwks_cache_seconds": 60,
121
+ "jwks_cooldown_seconds": 1,
122
+ "fetch_timeout_ms": 2000,
123
+ "max_response_bytes": 8192
124
+ }
125
+ ```
126
+ <!-- synapsor-oidc-operator-config:end -->
127
+
128
+ Place that object at `operator_identity` in `synapsor.runner.json`. The
129
+ configuration uses environment-variable names, not their secret values.
130
+
131
+ The IdP mapping for this example is:
132
+
133
+ | IdP group | JWT `groups` value | Synapsor purpose |
134
+ | --- | --- | --- |
135
+ | `example-membership-reviewers` | `membership_reviewer` | Satisfies the capability's exact `APPROVAL ROLE`. |
136
+ | `example-writeback-operators` | `writeback_operator` | Satisfies the independent `apply_roles` gate. |
137
+
138
+ Configure this mapping in the IdP. Runner does not infer it from database roles
139
+ or group names.
140
+
141
+ ### What Each Setting Does
142
+
143
+ | Setting | Meaning |
144
+ | --- | --- |
145
+ | `algorithms` | Exact asymmetric algorithms Runner accepts. OIDC operator identity supports `RS256` and `ES256`; do not infer this from the token header. |
146
+ | `jwks_url_env` | Environment variable containing the IdP's public JWKS URL. Remote URLs must use HTTPS; HTTP is accepted only on loopback for development. |
147
+ | `issuer` / `audience` | Values Runner requires in `iss` and `aud`. |
148
+ | `subject_claim` | Verified top-level claim used as the operator identity. |
149
+ | `roles_claim` | Verified top-level claim containing exact Synapsor role strings. |
150
+ | `token_env` | Environment variable holding the short-lived operator bearer token for this command. |
151
+ | `attestation_secret_env` | Environment variable holding at least 32 bytes of Runner-side key material used to attest the redacted stored decision. |
152
+ | `apply_roles` | Roles accepted for the separate apply decision. |
153
+
154
+ The attestation secret is not the OIDC token and not an IdP client secret.
155
+ Runner uses it locally to make the stored, token-free identity proof
156
+ tamper-evident. Every trusted process that verifies the same ledger needs the
157
+ same protected value. Keep it out of source control, MCP configuration, logs,
158
+ and model context.
159
+
160
+ ## Run The Local OIDC/JWKS Fixture
161
+
162
+ The npm package includes a synthetic localhost-only issuer. It generates
163
+ ephemeral RS256 private keys in memory, serves only public keys at `/jwks`, and
164
+ mints fixed test identities. It must never be used as a real identity provider.
165
+
166
+ In terminal 1:
167
+
168
+ ```bash
169
+ export SYNAPSOR_EXAMPLE_OIDC_PORT=9411
170
+ node ./node_modules/@synapsor/runner/examples/operator-oidc/issuer.mjs
171
+ ```
172
+
173
+ It prints one JSON object describing:
174
+
175
+ ```text
176
+ JWKS: http://127.0.0.1:9411/jwks
177
+ Issuer: https://identity.example.test/oidc
178
+ Audience: synapsor-operators
179
+ ```
180
+
181
+ The server holds the private keys only in memory. Stop it after the walkthrough.
182
+
183
+ In terminal 2, from a Synapsor project that has one pending
184
+ `membership_reviewer` proposal:
185
+
186
+ ```bash
187
+ export SYNAPSOR_OPERATOR_JWKS_URL="http://127.0.0.1:9411/jwks"
188
+ export SYNAPSOR_OPERATOR_ATTESTATION_SECRET="$(openssl rand -base64 32)"
189
+
190
+ export SYNAPSOR_OPERATOR_TOKEN="$(
191
+ curl -fsS http://127.0.0.1:9411/token/reviewer |
192
+ node -pe 'JSON.parse(require("node:fs").readFileSync(0,"utf8")).access_token'
193
+ )"
194
+
195
+ synapsor-runner proposals approve latest \
196
+ --config ./synapsor.runner.json \
197
+ --store ./.synapsor/local.db \
198
+ --yes \
199
+ --json
200
+
201
+ unset SYNAPSOR_OPERATOR_TOKEN
202
+ ```
203
+
204
+ The token is supplied through the environment and is not a command-line
205
+ argument. For a real deployment, inject a short-lived token through a protected
206
+ process environment, protected token file, or stdin. Never commit it.
207
+
208
+ The fixture's apply identity uses a rotated key. Keep the old and new public
209
+ keys overlapped during rotation:
210
+
211
+ ```bash
212
+ curl -fsS -X POST http://127.0.0.1:9411/rotate
213
+
214
+ export SYNAPSOR_OPERATOR_TOKEN="$(
215
+ curl -fsS http://127.0.0.1:9411/token/applier |
216
+ node -pe 'JSON.parse(require("node:fs").readFileSync(0,"utf8")).access_token'
217
+ )"
218
+
219
+ synapsor-runner apply latest \
220
+ --config ./synapsor.runner.json \
221
+ --store ./.synapsor/local.db \
222
+ --json
223
+
224
+ unset SYNAPSOR_OPERATOR_TOKEN
225
+ ```
226
+
227
+ This second token has `writeback_operator`, not
228
+ `membership_reviewer`. It authorizes apply only after Runner verifies the
229
+ stored approval and all guarded-write prerequisites.
230
+
231
+ ### Token File Or Stdin
232
+
233
+ Configure exactly one token source.
234
+
235
+ For a protected token file, replace `token_env` with:
236
+
237
+ ```json
238
+ {
239
+ "token_file_env": "SYNAPSOR_OPERATOR_TOKEN_FILE"
240
+ }
241
+ ```
242
+
243
+ Set the environment variable to a mode-`0600` file outside the repository.
244
+ Delete the file after the decision.
245
+
246
+ For stdin, replace `token_env` with:
247
+
248
+ ```json
249
+ {
250
+ "token_stdin": true
251
+ }
252
+ ```
253
+
254
+ Use `--yes` so confirmation does not compete for stdin:
255
+
256
+ ```bash
257
+ printf '%s\n' "$SHORT_LIVED_OPERATOR_TOKEN" |
258
+ synapsor-runner proposals approve "$PROPOSAL_ID" \
259
+ --config ./synapsor.runner.json \
260
+ --store ./.synapsor/local.db \
261
+ --yes
262
+ ```
263
+
264
+ Do not pass a token as a CLI argument.
265
+
266
+ ## What The Ledger Stores
267
+
268
+ Inspect the full lifecycle without copying an opaque ID:
269
+
270
+ ```bash
271
+ synapsor-runner replay show latest \
272
+ --config ./synapsor.runner.json \
273
+ --store ./.synapsor/local.db \
274
+ --json
275
+ ```
276
+
277
+ The approval record has this shape:
278
+
279
+ ```json
280
+ {
281
+ "proposal_version": 1,
282
+ "proposal_hash": "sha256:...",
283
+ "approver": "reviewer@example.test",
284
+ "status": "approved",
285
+ "identity": {
286
+ "provider": "jwt_oidc",
287
+ "verified": true,
288
+ "subject": "reviewer@example.test",
289
+ "roles": ["membership_reviewer"],
290
+ "key_id": "fitflow-key-1",
291
+ "algorithm": "RS256",
292
+ "issuer": "https://identity.example.test/oidc",
293
+ "decision": {
294
+ "schema_version": "synapsor.operator-decision.v1",
295
+ "action": "approve",
296
+ "proposal_id": "wrp_...",
297
+ "proposal_version": 1,
298
+ "proposal_hash": "sha256:...",
299
+ "subject": "reviewer@example.test",
300
+ "issued_at": "..."
301
+ },
302
+ "decision_hash": "sha256:...",
303
+ "signature": "...",
304
+ "integrity_hash": "sha256:..."
305
+ }
306
+ }
307
+ ```
308
+
309
+ `signature` is Runner's HMAC attestation over the redacted verified identity
310
+ and exact decision. It is not the original OIDC JWT signature. The bearer token
311
+ is absent.
312
+
313
+ The replay also contains a `writeback_authorized` event for the independently
314
+ verified apply identity and the normal guarded-write receipt.
315
+
316
+ ## JWKS Caching And Key Rotation
317
+
318
+ For a long-running Runner process, remote JWKS keys are held in memory for
319
+ `jwks_cache_seconds` (default 600 seconds). An unknown `kid` may trigger a
320
+ bounded refresh after `jwks_cooldown_seconds` (default 30 seconds). Fetches have
321
+ a timeout and maximum response size, reject redirects, and accept public keys
322
+ only. A one-shot CLI command starts with an empty in-memory cache.
323
+
324
+ During rotation:
325
+
326
+ 1. Publish the new public key while retaining the old key.
327
+ 2. Wait at least the configured cache lifetime plus expected clock skew.
328
+ 3. Start issuing tokens with the new `kid`.
329
+ 4. Keep the overlap long enough for long-running processes to refresh.
330
+ 5. Remove the old key only after old tokens and caches have expired.
331
+
332
+ Runner's verifier tests exercise an in-process unknown-`kid` refresh. The packed
333
+ approval integration uses the old key for approval, publishes an overlapping
334
+ new key, and uses the new key for the independent apply decision.
335
+
336
+ ## Fail-Closed Troubleshooting
337
+
338
+ | Failure | What Runner does | Remediation |
339
+ | --- | --- | --- |
340
+ | Missing or similar role | Refuses the decision; proposal stays pending. | Map the exact contract role into the configured roles claim. Do not rename the role at runtime. |
341
+ | Wrong issuer or audience | Rejects JWT verification. | Match the exact IdP issuer and application audience. |
342
+ | Unknown `kid` | Refreshes only within the bounded JWKS policy, then rejects if no public key matches. | Publish overlapping keys and wait for the cooldown/cache policy. |
343
+ | Invalid signature | Rejects the token. | Obtain a fresh token from the configured issuer; do not bypass verification. |
344
+ | Expired or not-yet-valid token | Rejects the token. | Fix clock synchronization and obtain a currently valid token. Keep clock skew narrow. |
345
+ | Unsafe subject or malformed roles | Refuses identity resolution. | Emit a safe top-level string subject and an array or delimited string of safe role names. |
346
+ | JWKS endpoint unavailable | Fails closed when a required key cannot be resolved. | Restore HTTPS/network/DNS service or use a reviewed public-key source. Do not fall back to `dev_env`. |
347
+ | Missing or short attestation secret | Refuses to record the decision. | Supply at least 32 bytes of protected Runner-side key material. |
348
+ | Stored identity proof changed | Apply fails before source mutation. | Investigate ledger integrity. Create a new proposal and approval; do not edit the proof. |
349
+ | Approver lacks an apply role | Apply is refused even after approval. | Use a separately verified operator whose token has one configured `apply_roles` value. |
350
+
351
+ Database availability, database roles, and MCP arguments never substitute for a
352
+ verified operator identity. Keep database least privilege and RLS beneath this
353
+ operator decision boundary.