@synapsor/runner 1.6.1 → 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.
- package/CHANGELOG.md +98 -3
- package/README.md +33 -19
- package/SECURITY.md +10 -2
- package/THREAT_MODEL.md +27 -4
- package/dist/authoring.mjs +53 -3
- package/dist/cli.d.ts +19 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/local-ui.d.ts +85 -1
- package/dist/local-ui.d.ts.map +1 -1
- package/dist/runner.mjs +33542 -20382
- package/dist/runtime.mjs +3581 -237
- package/dist/shadow.mjs +2423 -65
- package/docs/README.md +22 -2
- package/docs/agent-guided-setup.md +239 -0
- package/docs/approval-roles-and-operator-identity.md +353 -0
- package/docs/auto-boundary-and-scoped-explore.md +32 -6
- package/docs/capability-authoring.md +19 -0
- package/docs/current-scope.md +20 -1
- package/docs/cursor-plugin.md +2 -2
- package/docs/dsl-json-parity.md +75 -0
- package/docs/dsl-reference.md +10 -0
- package/docs/fresh-developer-usability.md +155 -80
- package/docs/getting-started-own-database.md +5 -4
- package/docs/guarded-crud-writeback.md +19 -0
- package/docs/guided-onboarding.md +331 -0
- package/docs/human-attention-notifications.md +367 -0
- package/docs/limitations.md +19 -3
- package/docs/local-mode.md +16 -2
- package/docs/migrating-to-synapsor-spec.md +38 -0
- package/docs/production.md +39 -0
- package/docs/release-notes.md +87 -4
- package/docs/runner-config-reference.md +132 -0
- package/docs/running-a-runner-fleet.md +16 -1
- package/docs/security-boundary.md +24 -0
- package/docs/store-lifecycle.md +20 -1
- package/docs/supervised-automatic-apply.md +267 -0
- package/docs/troubleshooting-first-run.md +35 -0
- package/examples/app-owned-writeback/command-handler.mjs +0 -0
- package/examples/fitflow-guided-onboarding/README.md +43 -0
- package/examples/fitflow-guided-onboarding/app/page.tsx +8 -0
- package/examples/fitflow-guided-onboarding/docker-compose.yml +16 -0
- package/examples/fitflow-guided-onboarding/package.json +18 -0
- package/examples/fitflow-guided-onboarding/prisma/schema.prisma +98 -0
- package/examples/fitflow-guided-onboarding/seed/postgres.sql +401 -0
- package/examples/mcp-postgres-billing-app-handler/scripts/run-demo.sh +0 -0
- package/examples/operator-oidc/issuer.mjs +188 -0
- package/examples/reference-support-billing-app/scripts/run-demo.sh +0 -0
- package/examples/support-billing-agent/scripts/run-demo.sh +0 -0
- package/examples/support-billing-agent/scripts/run-evaluation.sh +0 -0
- package/examples/support-plan-credit/README.md +6 -2
- package/package.json +11 -9
- package/schemas/schema-candidate-review.schema.json +42 -0
- package/schemas/synapsor.runner.schema.json +227 -4
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# Guided Onboarding: Database To First Safe Tool
|
|
2
|
+
|
|
3
|
+
This is the shortest path from an existing staging Postgres/MySQL database to
|
|
4
|
+
one useful agent tool. It requires no Synapsor account, Cloud login, database
|
|
5
|
+
replication, global install, MCP host, model API key, or hand-authored config,
|
|
6
|
+
DSL, JSON, SQL, DDL, or grants.
|
|
7
|
+
|
|
8
|
+
## The One-Minute Mental Model
|
|
9
|
+
|
|
10
|
+
Synapsor is creating the small set of database powers your agent may use. It
|
|
11
|
+
does not give the agent SQL access.
|
|
12
|
+
|
|
13
|
+
- Reads return only fields a human reviewed, under tenant and principal scope
|
|
14
|
+
supplied outside model arguments.
|
|
15
|
+
- Scoped Explore is a temporary local development/staging authoring tool.
|
|
16
|
+
- Protect turns one useful analysis into a narrow named capability.
|
|
17
|
+
- A model-facing write call creates an exact proposal. It does not commit.
|
|
18
|
+
- The model cannot activate authority, approve a proposal, or apply a write.
|
|
19
|
+
- A trusted operator or worker rechecks the effect and records a receipt.
|
|
20
|
+
|
|
21
|
+
## Before You Start
|
|
22
|
+
|
|
23
|
+
Use a disposable or staging database and a dedicated SELECT-only, non-owner
|
|
24
|
+
credential. Keep row-level security, restricted views, or tenant-bound
|
|
25
|
+
credentials underneath Runner where available.
|
|
26
|
+
|
|
27
|
+
Export credentials in the launching shell. Do not put their values in project
|
|
28
|
+
files or chat:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
export DATABASE_URL='<staging read-only URL>'
|
|
32
|
+
export SYNAPSOR_TENANT_ID='<staging tenant>'
|
|
33
|
+
export SYNAPSOR_PRINCIPAL='<staging principal>'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Runner requires Node 22.13 or newer.
|
|
37
|
+
|
|
38
|
+
## One Command To Workbench
|
|
39
|
+
|
|
40
|
+
Run this from an empty project directory or your application root:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx -y @synapsor/runner@latest start --from-env DATABASE_URL
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The command automatically:
|
|
47
|
+
|
|
48
|
+
1. checks the database and credential posture;
|
|
49
|
+
2. inspects schema metadata without sampling source rows;
|
|
50
|
+
3. statically reads supported Prisma, Drizzle, OpenAPI, and existing Synapsor
|
|
51
|
+
definitions when present;
|
|
52
|
+
4. drafts disabled public DSL, canonical JSON, tests, and a generation lock;
|
|
53
|
+
5. creates a valid zero-authority Runner config and local SQLite ledger;
|
|
54
|
+
6. validates every generated artifact;
|
|
55
|
+
7. opens one secured loopback Workbench URL.
|
|
56
|
+
|
|
57
|
+
Before human activation, the agent has no generated authority and no source row
|
|
58
|
+
has been read.
|
|
59
|
+
|
|
60
|
+
Running the same command again resumes. Resume and Try do not rescan, rewrite
|
|
61
|
+
files, or change a digest. Rescan and destructive Start over remain explicit
|
|
62
|
+
human choices.
|
|
63
|
+
|
|
64
|
+
## Five Minutes: First Safe Read
|
|
65
|
+
|
|
66
|
+
Workbench starts with an overview, not a permission matrix. It shows:
|
|
67
|
+
|
|
68
|
+
- database and framework evidence found;
|
|
69
|
+
- resources inspected;
|
|
70
|
+
- fields already kept out;
|
|
71
|
+
- unresolved sensitive fields;
|
|
72
|
+
- row identity;
|
|
73
|
+
- tenant and principal scope;
|
|
74
|
+
- database-role and RLS posture;
|
|
75
|
+
- ready and blocked capabilities.
|
|
76
|
+
|
|
77
|
+
Review only the exceptions. Routine conservative defaults are already applied.
|
|
78
|
+
There is no global “approve everything” control.
|
|
79
|
+
|
|
80
|
+
For each selected resource, confirm the exact primary/unique row identity,
|
|
81
|
+
trusted scope, visible fields, kept-out fields, and any aggregate-only fields.
|
|
82
|
+
Then sign the displayed digest. Changing a reviewed decision creates a new
|
|
83
|
+
disabled digest.
|
|
84
|
+
|
|
85
|
+
Choose **Try first safe read**. Runner calls the real local runtime and shows:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
Your first safe tool is working.
|
|
89
|
+
|
|
90
|
+
Tool: <domain>.inspect_<resource>
|
|
91
|
+
Agent can see: <reviewed fields>
|
|
92
|
+
Agent cannot see: <kept-out fields>
|
|
93
|
+
Tenant scope: trusted outside model arguments
|
|
94
|
+
Principal scope: trusted outside model arguments
|
|
95
|
+
Source database changed: no
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
CLI fallback:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npx -y @synapsor/runner@latest try call --list --format json
|
|
102
|
+
npx -y @synapsor/runner@latest try call <returned-tool-name> --sample --json
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The second command is for an activated named tool with valid generated sample
|
|
106
|
+
input. It does not invent tenant or principal values.
|
|
107
|
+
|
|
108
|
+
## Ten Minutes: Explore And Protect
|
|
109
|
+
|
|
110
|
+
Scoped Explore is available only after its exact local authoring boundary is
|
|
111
|
+
activated. It is absent from production, unknown-profile, remote, shared HTTP,
|
|
112
|
+
and non-loopback `tools/list`.
|
|
113
|
+
|
|
114
|
+
In Workbench, choose **Ask an aggregate question**. Select only reviewed
|
|
115
|
+
resources, dimensions, measures, filters, and time buckets. No SQL or plan JSON
|
|
116
|
+
is required.
|
|
117
|
+
|
|
118
|
+
The first release supports bounded:
|
|
119
|
+
|
|
120
|
+
- `count` and reviewed `count_distinct`;
|
|
121
|
+
- `sum` and `avg` over reviewed numeric measures;
|
|
122
|
+
- reviewed categorical dimensions;
|
|
123
|
+
- day, week, and month buckets;
|
|
124
|
+
- typed filters and bounded top-N;
|
|
125
|
+
- at most one reviewed many-to-one relationship.
|
|
126
|
+
|
|
127
|
+
Minimum cohorts, group limits, response limits, query limits, extraction
|
|
128
|
+
budgets, and anti-differencing budgets are fixed by the activated boundary.
|
|
129
|
+
Small groups are suppressed. Results describe changes or contributors; they do
|
|
130
|
+
not prove causation.
|
|
131
|
+
|
|
132
|
+
CLI fallback for the Workbench-suggested aggregate:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npx -y @synapsor/runner@latest try explore --suggested --json
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Choose **Protect this analysis** directly on the successful result. Runner
|
|
139
|
+
freezes its reviewed resources, measures, grouping, time bucket, ordering,
|
|
140
|
+
scope, suppression, and limits into:
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
synapsor/protected/drafts/<namespace>__<name>/capability.synapsor.sql
|
|
144
|
+
synapsor/protected/drafts/<namespace>__<name>/synapsor.contract.json
|
|
145
|
+
synapsor/protected/drafts/<namespace>__<name>/contract-tests.json
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The capability remains disabled until a human activates its exact digest.
|
|
149
|
+
Disabling Scoped Explore does not remove the protected named capability.
|
|
150
|
+
|
|
151
|
+
CLI fallback, without copying an opaque query handle:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
npx -y @synapsor/runner@latest try protect \
|
|
155
|
+
--name analytics.reviewed_weekly_summary \
|
|
156
|
+
--json
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Fifteen Minutes: First Proposal
|
|
160
|
+
|
|
161
|
+
After the first read, choose **Add a safe action**. Schema metadata supplies
|
|
162
|
+
structure, never business permission. The human specifies:
|
|
163
|
+
|
|
164
|
+
1. the business action;
|
|
165
|
+
2. target resource and exact row identity;
|
|
166
|
+
3. fields that may change;
|
|
167
|
+
4. allowed values, transitions, and numeric bounds;
|
|
168
|
+
5. trusted tenant and principal scope;
|
|
169
|
+
6. conflict/version guard;
|
|
170
|
+
7. approval role;
|
|
171
|
+
8. optional small bounded auto-approval;
|
|
172
|
+
9. per-operation and daily limits;
|
|
173
|
+
10. optional reviewed compensation.
|
|
174
|
+
|
|
175
|
+
Runner generates public DSL, canonical JSON, tests, an exact-effect preview,
|
|
176
|
+
and a disabled action. Unsupported or unsafe combinations fail closed. In
|
|
177
|
+
particular, DELETE, bounded-set, quorum, auto-approval, and reversibility cannot
|
|
178
|
+
be combined beyond what the canonical DSL supports.
|
|
179
|
+
|
|
180
|
+
After exact-digest human activation, call the named proposal tool in Workbench
|
|
181
|
+
or through the host-neutral Try path. The result must say:
|
|
182
|
+
|
|
183
|
+
```text
|
|
184
|
+
Proposal created.
|
|
185
|
+
Source database changed: no.
|
|
186
|
+
The model cannot approve or apply this proposal.
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Inspect the latest lifecycle without copying a proposal ID:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
npx -y @synapsor/runner@latest lifecycle --details
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Approval and apply stay outside MCP:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
npx -y @synapsor/runner@latest proposals approve latest --yes
|
|
199
|
+
npx -y @synapsor/runner@latest apply latest
|
|
200
|
+
npx -y @synapsor/runner@latest lifecycle show latest --details
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Production should use a verified signed-key or OIDC operator identity rather
|
|
204
|
+
than unverified development identity. See [Approval Roles And Verified
|
|
205
|
+
Operator Identity](approval-roles-and-operator-identity.md).
|
|
206
|
+
|
|
207
|
+
These commands show the default human-approval/manual-apply path. A production
|
|
208
|
+
operator can separately opt an eligible action into exact-digest supervised
|
|
209
|
+
execution. That does not give the model an apply tool: the model creates a
|
|
210
|
+
bounded proposal, reviewed policy may approve it, and a separately trusted
|
|
211
|
+
worker repeats all guards before any later source mutation. Existing
|
|
212
|
+
`AUTO APPROVE` contracts remain manual-apply unless both opt-ins are present.
|
|
213
|
+
See [Operator-Supervised Automatic
|
|
214
|
+
Apply](supervised-automatic-apply.md).
|
|
215
|
+
|
|
216
|
+
## Guarded Development Writeback Setup
|
|
217
|
+
|
|
218
|
+
Preview is always the default:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
npx -y @synapsor/runner@latest writeback setup \
|
|
222
|
+
--profile staging \
|
|
223
|
+
--json
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The response contains one immutable plan digest and exact setup SQL, objects,
|
|
227
|
+
grants, receipt mode, and next action.
|
|
228
|
+
|
|
229
|
+
- `runner_ledger`: no Synapsor receipt table or receipt grant is added to the
|
|
230
|
+
source database.
|
|
231
|
+
- `source_db` + `auto_migrate`: verifies the configured idempotent first-use
|
|
232
|
+
path using the trusted writer.
|
|
233
|
+
- `source_db` + `precreated`: requires a separate setup/admin URL environment
|
|
234
|
+
name and exact steady-state writer role.
|
|
235
|
+
- app-owned handlers receive no unnecessary source receipt setup.
|
|
236
|
+
|
|
237
|
+
DDL apply is refused for missing, unknown, or production profiles. In
|
|
238
|
+
development/staging, it requires the exact displayed confirmation:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
npx -y @synapsor/runner@latest writeback setup \
|
|
242
|
+
--profile staging \
|
|
243
|
+
--writer-role '<steady-state-writer-role>' \
|
|
244
|
+
--setup-url-env SYNAPSOR_SETUP_DATABASE_URL \
|
|
245
|
+
--apply \
|
|
246
|
+
--confirm 'APPLY WRITEBACK SETUP sha256:<reviewed-plan-digest>'
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Never reuse the model/read credential as the elevated setup connection.
|
|
250
|
+
|
|
251
|
+
## Connect An MCP Client
|
|
252
|
+
|
|
253
|
+
Workbench generates ready-to-copy Cursor, Claude, Codex, and generic stdio
|
|
254
|
+
snippets for the same reviewed authority. Stdio opens no network listener and
|
|
255
|
+
needs no HTTP bearer credential.
|
|
256
|
+
|
|
257
|
+
Render the generic snippet:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
npx -y @synapsor/runner@latest mcp config --absolute-paths
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Preview a project-scoped Cursor change:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
npx -y @synapsor/runner@latest mcp install cursor --project --dry-run
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
No client config contains database URLs, trusted scope values, approval,
|
|
270
|
+
activation, apply, or revert authority.
|
|
271
|
+
|
|
272
|
+
## Machine-Readable Output
|
|
273
|
+
|
|
274
|
+
Commands that support machine output write exactly one JSON value to stdout.
|
|
275
|
+
Operational logs and diagnostics use stderr:
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
npx -y @synapsor/runner@latest try explore --suggested --json | jq -e .
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
On a process-level failure, JSON output uses:
|
|
282
|
+
|
|
283
|
+
```json
|
|
284
|
+
{
|
|
285
|
+
"ok": false,
|
|
286
|
+
"error": {
|
|
287
|
+
"code": "COMMAND_REJECTED",
|
|
288
|
+
"message": "Safe redacted explanation"
|
|
289
|
+
},
|
|
290
|
+
"recovery": {
|
|
291
|
+
"state_preserved": "What remains intact",
|
|
292
|
+
"source_database_changed": null,
|
|
293
|
+
"next_action": "One exact next action"
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
`null` means Runner cannot honestly infer the source mutation outcome from a
|
|
299
|
+
generic command exception. Operation-specific receipts and reconciliation
|
|
300
|
+
states remain authoritative.
|
|
301
|
+
|
|
302
|
+
## Before And After
|
|
303
|
+
|
|
304
|
+
The earlier manual path could require roughly thirteen disconnected steps:
|
|
305
|
+
inspect, compile, create config, validate, doctor, initialize a store, start
|
|
306
|
+
Workbench, generate MCP config, create receipt DDL/grants, run a smoke call,
|
|
307
|
+
approve, and apply.
|
|
308
|
+
|
|
309
|
+
The guided path through the first proposal uses:
|
|
310
|
+
|
|
311
|
+
- one public shell command;
|
|
312
|
+
- deliberate Workbench review and activation;
|
|
313
|
+
- no manual file edits;
|
|
314
|
+
- no external documentation search;
|
|
315
|
+
- no Cursor requirement.
|
|
316
|
+
|
|
317
|
+
The packed FitFlow gate records the exact command count, human decisions, and
|
|
318
|
+
timings in `development/runner-1.6.3-fitflow-results.json`.
|
|
319
|
+
|
|
320
|
+
## Visual Reference
|
|
321
|
+
|
|
322
|
+
Repository screenshots cover desktop, mobile, light, dark, loading, blocked,
|
|
323
|
+
stale, failure, empty Protect, and unavailable-action states:
|
|
324
|
+
|
|
325
|
+
- [Desktop overview](https://github.com/Synapsor/Synapsor-Runner/blob/main/development/runner-1.6.3-visual/workbench-overview-desktop-light.png)
|
|
326
|
+
- [Mobile overview](https://github.com/Synapsor/Synapsor-Runner/blob/main/development/runner-1.6.3-visual/workbench-overview-mobile-light.png)
|
|
327
|
+
- [Blocked identity review](https://github.com/Synapsor/Synapsor-Runner/blob/main/development/runner-1.6.3-visual/workbench-blocked-identity.png)
|
|
328
|
+
- [Stale/failure recovery](https://github.com/Synapsor/Synapsor-Runner/blob/main/development/runner-1.6.3-visual/workbench-keyboard-stale-failure.png)
|
|
329
|
+
|
|
330
|
+
For common blocked states and one-action recovery commands, use
|
|
331
|
+
[Troubleshooting First Run](troubleshooting-first-run.md).
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# Human Attention And Notifications
|
|
2
|
+
|
|
3
|
+
Runner keeps the full lifecycle in its ledger and secured Workbench. External
|
|
4
|
+
notifications are a quiet interruption channel for meaningful human attention,
|
|
5
|
+
not a copy of every state transition.
|
|
6
|
+
|
|
7
|
+
Notifications are additive, local-first, and disabled by default. Runner needs
|
|
8
|
+
no webhook, network connection, Cloud account, or continuously running
|
|
9
|
+
dispatcher for its existing proposal, approval, apply, receipt, and replay
|
|
10
|
+
flows.
|
|
11
|
+
|
|
12
|
+
## Architecture
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
authoritative ledger transition
|
|
16
|
+
-> immutable attention event in the same store transaction
|
|
17
|
+
-> coalesced Human Attention item
|
|
18
|
+
-> separately operated dispatcher
|
|
19
|
+
-> configured JSONL or signed webhook sink
|
|
20
|
+
-> authenticated human opens Workbench
|
|
21
|
+
-> existing approval/recovery control handles the item
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
An event informs. It never authorizes. A webhook response, delivery receipt,
|
|
25
|
+
inbox acknowledgement, or notification replay cannot activate a capability,
|
|
26
|
+
approve a proposal, apply a write, cancel work, or resolve an unknown database
|
|
27
|
+
outcome.
|
|
28
|
+
|
|
29
|
+
SQLite and shared PostgreSQL runtime stores retain the same event identities,
|
|
30
|
+
attention projection, acknowledgement, and delivery state.
|
|
31
|
+
|
|
32
|
+
## Quiet Default
|
|
33
|
+
|
|
34
|
+
Under the default route, successful activity remains visible in the ledger,
|
|
35
|
+
Workbench timeline, CLI, metrics, and optional digest, but does not interrupt a
|
|
36
|
+
human.
|
|
37
|
+
|
|
38
|
+
No immediate external notification is sent by default for:
|
|
39
|
+
|
|
40
|
+
- proposal creation, policy auto-approval, approval, or queueing;
|
|
41
|
+
- a worker lease or individual retry;
|
|
42
|
+
- successful manual or automatic apply;
|
|
43
|
+
- an isolated conflict that Runner handled safely;
|
|
44
|
+
- planned worker start, pause, resume, drain, or stop;
|
|
45
|
+
- capability activation by the same operator.
|
|
46
|
+
|
|
47
|
+
Immediate delivery defaults to states where timely attention matters:
|
|
48
|
+
|
|
49
|
+
- human approval required;
|
|
50
|
+
- UNKNOWN transaction outcome or reconciliation required;
|
|
51
|
+
- dead-letter work;
|
|
52
|
+
- sustained worker-health failure;
|
|
53
|
+
- queue depth/age crossing an operator threshold;
|
|
54
|
+
- schema drift or stale digest blocking authority;
|
|
55
|
+
- invalidated writer credential posture;
|
|
56
|
+
- critical policy/limit failure;
|
|
57
|
+
- an approved proposal approaching expiry while action is still possible.
|
|
58
|
+
|
|
59
|
+
Informational success events can be sent only through an explicit all-events
|
|
60
|
+
route or periodic digest.
|
|
61
|
+
|
|
62
|
+
## Events And Attention Items
|
|
63
|
+
|
|
64
|
+
Events are immutable audit facts. Attention items are durable incident
|
|
65
|
+
projections over related events.
|
|
66
|
+
|
|
67
|
+
For example, ten proposals requiring the same role can produce ten immutable
|
|
68
|
+
events but one item saying that ten proposals need review. Repeated worker
|
|
69
|
+
failures, queue backlog, or the same schema-drift condition update one item and
|
|
70
|
+
its occurrence count. Retry attempts stay internal until the configured
|
|
71
|
+
attempt/duration threshold is crossed.
|
|
72
|
+
|
|
73
|
+
Attention states are:
|
|
74
|
+
|
|
75
|
+
- `open`;
|
|
76
|
+
- `acknowledged`;
|
|
77
|
+
- `resolved`;
|
|
78
|
+
- `expired`.
|
|
79
|
+
|
|
80
|
+
Acknowledgement means only that a verified operator saw the item. It is not
|
|
81
|
+
approval or resolution.
|
|
82
|
+
|
|
83
|
+
## Configure Sinks
|
|
84
|
+
|
|
85
|
+
This operator-owned config enables one signed webhook and one optional
|
|
86
|
+
development JSONL stream:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"notifications": {
|
|
91
|
+
"enabled": true,
|
|
92
|
+
"workbench_url_env": "SYNAPSOR_WORKBENCH_URL",
|
|
93
|
+
"sinks": [
|
|
94
|
+
{
|
|
95
|
+
"id": "operations",
|
|
96
|
+
"type": "webhook",
|
|
97
|
+
"enabled": true,
|
|
98
|
+
"url_env": "SYNAPSOR_NOTIFY_WEBHOOK_URL",
|
|
99
|
+
"signing_secret_env": "SYNAPSOR_NOTIFY_SIGNING_SECRET",
|
|
100
|
+
"minimum_severity": "warning",
|
|
101
|
+
"events": [
|
|
102
|
+
"proposal.review_required",
|
|
103
|
+
"worker.dead_lettered",
|
|
104
|
+
"worker.unknown_outcome",
|
|
105
|
+
"worker.reconciliation_required",
|
|
106
|
+
"schema.drift_detected"
|
|
107
|
+
],
|
|
108
|
+
"capabilities": ["billing.propose_small_credit"],
|
|
109
|
+
"environments": ["staging", "production"],
|
|
110
|
+
"delivery": "immediate",
|
|
111
|
+
"max_attempts": 5,
|
|
112
|
+
"timeout_ms": 3000,
|
|
113
|
+
"max_response_bytes": 1024,
|
|
114
|
+
"replay_window_seconds": 300,
|
|
115
|
+
"allow_private_destinations": false,
|
|
116
|
+
"recovery_notifications": false,
|
|
117
|
+
"budgets": {
|
|
118
|
+
"per_minute": 10,
|
|
119
|
+
"per_hour": 100,
|
|
120
|
+
"immediate_informational_per_hour": 0,
|
|
121
|
+
"aggregation_window_seconds": 300,
|
|
122
|
+
"cooldown_seconds": 600,
|
|
123
|
+
"max_unresolved_reminders": 3,
|
|
124
|
+
"digest_cadence_minutes": 1440,
|
|
125
|
+
"escalation_delay_seconds": 60,
|
|
126
|
+
"retry_attempt_threshold": 3,
|
|
127
|
+
"degraded_duration_seconds": 120,
|
|
128
|
+
"queue_depth_threshold": 100,
|
|
129
|
+
"queue_age_seconds": 300
|
|
130
|
+
},
|
|
131
|
+
"quiet_hours": {
|
|
132
|
+
"start_utc_hour": 22,
|
|
133
|
+
"end_utc_hour": 7
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"id": "development",
|
|
138
|
+
"type": "jsonl",
|
|
139
|
+
"enabled": false,
|
|
140
|
+
"destination": "stdout",
|
|
141
|
+
"minimum_severity": "informational",
|
|
142
|
+
"delivery": "all"
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Set values only in the operator environment:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
export SYNAPSOR_WORKBENCH_URL=https://runner-ops.example.internal
|
|
153
|
+
export SYNAPSOR_NOTIFY_WEBHOOK_URL=https://events.example.com/synapsor
|
|
154
|
+
export SYNAPSOR_NOTIFY_SIGNING_SECRET='at-least-32-random-bytes'
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The config stores environment-variable names, not URLs or secrets. Events never
|
|
158
|
+
contain credentials, database URLs, SQL, source rows, aggregate group results,
|
|
159
|
+
kept-out fields, trusted tenant/principal values, sensitive arguments, or
|
|
160
|
+
authority tokens.
|
|
161
|
+
|
|
162
|
+
## Route By Destination
|
|
163
|
+
|
|
164
|
+
Every sink has its own event, severity, capability, environment, and delivery
|
|
165
|
+
route. A practical split is:
|
|
166
|
+
|
|
167
|
+
| Sink | Route |
|
|
168
|
+
| --- | --- |
|
|
169
|
+
| Workbench | Complete event and attention history |
|
|
170
|
+
| Team chat webhook | Review-required and operational warnings |
|
|
171
|
+
| Incident webhook | UNKNOWN, reconciliation, dead letter, sustained worker failure |
|
|
172
|
+
| Email-provider webhook | Daily digest |
|
|
173
|
+
| Audit webhook | Explicit all-events redacted stream |
|
|
174
|
+
|
|
175
|
+
The ledger and Workbench remain the source of truth.
|
|
176
|
+
|
|
177
|
+
## Operate Without Copying IDs
|
|
178
|
+
|
|
179
|
+
Inspect the highest-priority item:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
synapsor-runner attention list --config ./synapsor.runner.json
|
|
183
|
+
synapsor-runner attention show --config ./synapsor.runner.json
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Use `attention show <attention_id>` only when selecting a specific item. JSON
|
|
187
|
+
automation is available with `--json`.
|
|
188
|
+
|
|
189
|
+
Acknowledgement is explicit:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
synapsor-runner attention acknowledge --actor alice \
|
|
193
|
+
--config ./synapsor.runner.json
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Production acknowledgement follows configured verified operator identity and is
|
|
197
|
+
cryptographically bound to the exact item digest and occurrence.
|
|
198
|
+
|
|
199
|
+
Inspect and test delivery:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
synapsor-runner notifications status --config ./synapsor.runner.json
|
|
203
|
+
synapsor-runner notifications test --sink operations \
|
|
204
|
+
--config ./synapsor.runner.json
|
|
205
|
+
synapsor-runner notifications dispatch --sink operations --limit 20 \
|
|
206
|
+
--config ./synapsor.runner.json
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`notifications test` sends an unmistakable synthetic event containing no
|
|
210
|
+
database or proposal data. Opening Workbench, installing Runner, and starting
|
|
211
|
+
MCP never start outbound delivery.
|
|
212
|
+
|
|
213
|
+
Requeue one dead-letter delivery:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
synapsor-runner notifications replay latest --yes \
|
|
217
|
+
--reason "Webhook repaired and synthetic test passed" \
|
|
218
|
+
--config ./synapsor.runner.json
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
This replays only the immutable redacted notification. It does not replay an
|
|
222
|
+
approval or database mutation. Replay is an operator recovery action and
|
|
223
|
+
requires the configured `signed_key` or `jwt_oidc` identity. Runner binds the
|
|
224
|
+
verified decision to the exact delivery revision and records a redacted
|
|
225
|
+
immutable `notification.replayed` audit event.
|
|
226
|
+
|
|
227
|
+
## Webhook Contract
|
|
228
|
+
|
|
229
|
+
Runner sends a CloudEvents 1.0 structured JSON body with content type
|
|
230
|
+
`application/cloudevents+json`. Important headers are:
|
|
231
|
+
|
|
232
|
+
```text
|
|
233
|
+
X-Synapsor-Event-Id
|
|
234
|
+
X-Synapsor-Timestamp
|
|
235
|
+
X-Synapsor-Signature-Version: v1
|
|
236
|
+
X-Synapsor-Signature: sha256=<hex HMAC>
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
The HMAC-SHA-256 input is the UTF-8 string
|
|
240
|
+
`v1.<timestamp>.<event-id>.<exact-body>`. Receivers must:
|
|
241
|
+
|
|
242
|
+
1. Require signature version `v1`.
|
|
243
|
+
2. Reject timestamps outside the configured replay window.
|
|
244
|
+
3. Verify the HMAC with a timing-safe comparison.
|
|
245
|
+
4. Deduplicate the stable event ID.
|
|
246
|
+
5. Return only a bounded transport acknowledgement.
|
|
247
|
+
|
|
248
|
+
Runner ignores response content. A receiver cannot return a command.
|
|
249
|
+
|
|
250
|
+
A minimal Node receiver can verify the exact bytes before parsing JSON:
|
|
251
|
+
|
|
252
|
+
```js
|
|
253
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
254
|
+
|
|
255
|
+
export function verifySynapsorWebhook(rawBody, headers, secret, now = Date.now()) {
|
|
256
|
+
const eventId = headers["x-synapsor-event-id"];
|
|
257
|
+
const timestamp = headers["x-synapsor-timestamp"];
|
|
258
|
+
const version = headers["x-synapsor-signature-version"];
|
|
259
|
+
const suppliedHeader = headers["x-synapsor-signature"];
|
|
260
|
+
if (!eventId || !timestamp || version !== "v1" || !suppliedHeader?.startsWith("sha256=")) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const timestampSeconds = Number(timestamp);
|
|
265
|
+
const ageMs = Math.abs(now - timestampSeconds * 1000);
|
|
266
|
+
if (!Number.isSafeInteger(timestampSeconds) || ageMs > 300_000) return false;
|
|
267
|
+
|
|
268
|
+
const expected = Buffer.from(
|
|
269
|
+
createHmac("sha256", secret)
|
|
270
|
+
.update(`v1.${timestamp}.${eventId}.${rawBody}`, "utf8")
|
|
271
|
+
.digest("hex"),
|
|
272
|
+
"hex",
|
|
273
|
+
);
|
|
274
|
+
const supplied = Buffer.from(suppliedHeader.slice("sha256=".length), "hex");
|
|
275
|
+
return expected.length === supplied.length && timingSafeEqual(expected, supplied);
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Read the request as raw UTF-8 bytes for verification; reserializing parsed JSON
|
|
280
|
+
changes the signed body. Store the event ID for at least the replay window and
|
|
281
|
+
reject a duplicate before processing it.
|
|
282
|
+
|
|
283
|
+
The structured body resembles:
|
|
284
|
+
|
|
285
|
+
```json
|
|
286
|
+
{
|
|
287
|
+
"specversion": "1.0",
|
|
288
|
+
"id": "aev_...",
|
|
289
|
+
"type": "ai.synapsor.worker.reconciliation_required",
|
|
290
|
+
"source": "urn:synapsor:runner:production",
|
|
291
|
+
"time": "2026-07-25T00:00:00.000Z",
|
|
292
|
+
"datacontenttype": "application/json",
|
|
293
|
+
"subject": "billing.propose_small_credit",
|
|
294
|
+
"data": {
|
|
295
|
+
"schema_version": "synapsor.notification.v1",
|
|
296
|
+
"severity": "critical",
|
|
297
|
+
"environment": "production",
|
|
298
|
+
"capability": "billing.propose_small_credit",
|
|
299
|
+
"contract_digest": "sha256:...",
|
|
300
|
+
"summary": "A guarded apply needs operator reconciliation.",
|
|
301
|
+
"workbench_url": "https://runner-ops.example.internal/attention/att_..."
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The Workbench URL identifies an item but carries no approval or recovery
|
|
307
|
+
authority. The operator must authenticate normally.
|
|
308
|
+
|
|
309
|
+
HTTPS is required by default. Redirects and unsafe loopback, link-local,
|
|
310
|
+
metadata, multicast, unspecified, private, and rebinding destinations are
|
|
311
|
+
refused. Intentional private destinations require both
|
|
312
|
+
`allow_private_destinations: true` and an exact `private_host_allowlist`.
|
|
313
|
+
|
|
314
|
+
## Delivery And Noise Guarantees
|
|
315
|
+
|
|
316
|
+
Delivery is at least once. Sink-specific records use atomic leases, fencing,
|
|
317
|
+
bounded retries, jittered backoff, timeout, maximum attempts, and dead-letter
|
|
318
|
+
state. Repeated delivery cannot repeat proposal authority or source mutation.
|
|
319
|
+
|
|
320
|
+
When a non-critical budget is exhausted, Runner preserves every event, updates
|
|
321
|
+
the Workbench item, and suppresses or batches external messages. Critical events
|
|
322
|
+
bypass ordinary quiet hours and budgets but still coalesce under a separate
|
|
323
|
+
bounded emergency ceiling.
|
|
324
|
+
|
|
325
|
+
Optional digests report redacted counts for proposals, auto-approval, apply,
|
|
326
|
+
review-required work, conflicts, retries, dead letters, limits, queue age, and
|
|
327
|
+
unresolved critical items. They link to authenticated Workbench detail.
|
|
328
|
+
|
|
329
|
+
Recovery messages are off by default. When an incident resolves, Workbench is
|
|
330
|
+
updated; an external recovery message is sent only when that sink explicitly
|
|
331
|
+
enables `recovery_notifications` or the critical-route policy requires it.
|
|
332
|
+
|
|
333
|
+
## Retry And Dead-Letter Runbook
|
|
334
|
+
|
|
335
|
+
1. Run `notifications status --json` and inspect sink health, pending delivery,
|
|
336
|
+
and dead-letter counts.
|
|
337
|
+
2. Open the corresponding Workbench attention item. Confirm the authoritative
|
|
338
|
+
proposal/worker state before changing delivery.
|
|
339
|
+
3. Repair the operator-owned destination, DNS policy, TLS, or secret rotation.
|
|
340
|
+
4. Send `notifications test --sink <id>`; it contains no database data.
|
|
341
|
+
5. Run `notifications dispatch --sink <id> --limit 20` to drain eligible work.
|
|
342
|
+
6. Replay a dead-letter delivery only with
|
|
343
|
+
`notifications replay <delivery_id> --yes --reason "<why replay is safe>"`.
|
|
344
|
+
|
|
345
|
+
Replay preserves the original immutable event ID. It cannot re-run the database
|
|
346
|
+
operation. The exact recovery decision must be verified through the configured
|
|
347
|
+
operator identity. A still-failing sink remains observable and does not rewrite
|
|
348
|
+
the underlying proposal outcome.
|
|
349
|
+
|
|
350
|
+
## Supervised Worker Health Gate
|
|
351
|
+
|
|
352
|
+
An exact supervised-worker capability may list:
|
|
353
|
+
|
|
354
|
+
```json
|
|
355
|
+
{
|
|
356
|
+
"required_attention_sinks": ["operations"]
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
If every required sink is unhealthy or untested, an otherwise eligible
|
|
361
|
+
policy-approved production write remains queued. Workbench explains that
|
|
362
|
+
operator visibility is unavailable. A healthy sink does not approve anything;
|
|
363
|
+
after recovery, the worker still repeats every ordinary digest, policy, scope,
|
|
364
|
+
freshness, limit, credential, lease, and guarded-write check.
|
|
365
|
+
|
|
366
|
+
See [Operator-Supervised Automatic
|
|
367
|
+
Apply](supervised-automatic-apply.md) for the execution boundary.
|