@simbimbo/brainstem 0.0.3 → 0.0.5

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/docs/api.md CHANGED
@@ -1,380 +1,360 @@
1
- # API Spec
1
+ # Runtime API (Current Implementation)
2
+
3
+ This document reflects the runtime and API surfaces that are implemented today.
4
+
5
+ ## What is exposed today
6
+
7
+ - ingest endpoints
8
+ - `POST /ingest/event`
9
+ - `POST /ingest/batch`
10
+ - `POST /ingest/logicmonitor`
11
+ - `POST /replay/raw`
12
+ - inspection endpoints
13
+ - `GET /interesting`
14
+ - `GET /candidates`
15
+ - `GET /signatures`
16
+ - `GET /canonical_events`
17
+ - `GET /stats`
18
+ - `GET /failures`
19
+ - `GET /failures/{raw_envelope_id}`
20
+ - `GET /raw_envelopes`
21
+ - `GET /ingest/recent`
22
+ - `GET /sources`
23
+ - `GET /sources/status`
24
+ - runtime state endpoints
25
+ - `GET /runtime`
26
+ - `GET /status`
27
+ - `GET /healthz`
28
+
29
+ ## Runtime auth
30
+
31
+ `BRAINSTEM_API_TOKEN` controls API auth.
32
+ - if unset: write and read endpoints are open
33
+ - if set: endpoints above require one of:
34
+ - `X-API-Token: <token>`
35
+ - `Authorization: Bearer <token>`
36
+
37
+ `/healthz`, `/runtime`, and `/status` are currently unauthenticated.
38
+
39
+ ## Source surface used by the API
40
+
41
+ Registered source types in this milestone are:
42
+ - `syslog`
43
+ - `file`
44
+ - `logicmonitor`
45
+
46
+ Use `source_type` to select `syslog` or `file` payload drivers for raw envelope ingestion.
47
+ `logicmonitor` payloads are expected on `/ingest/logicmonitor`.
48
+
49
+ ## Shared ingest request fields
50
+
51
+ `POST /ingest/event` payload:
2
52
 
3
- ## Goal
4
-
5
- Provide a small, explainable API for ingesting events, scoring patterns, promoting memory, and retrieving relevant operational history.
6
-
7
- The API should support both machine-driven ingestion and operator-driven investigation.
8
-
9
- ---
10
-
11
- ## 1. Ingest Event
12
-
13
- ### `POST /events/ingest`
14
-
15
- Ingest one normalized or semi-normalized event.
16
-
17
- Request:
18
53
  ```json
19
54
  {
20
- "tenant_id": "client-a",
21
- "asset_id": "fw-01",
55
+ "tenant_id": "demo-tenant",
22
56
  "source_type": "syslog",
57
+ "source_id": "fw-01",
58
+ "source_name": "edge-fw-01",
23
59
  "source_path": "/var/log/syslog",
60
+ "timestamp": "2026-03-22T03:00:00Z",
24
61
  "host": "fw-01",
25
62
  "service": "charon",
26
- "timestamp": "2026-03-22T03:30:00Z",
27
- "severity": "warning",
63
+ "severity": "info",
64
+ "asset_id": "fw-01",
28
65
  "facility": "daemon",
29
66
  "message_raw": "IPsec SA rekey failed; retrying",
30
- "structured_fields": {
31
- "peer": "203.0.113.10"
32
- }
33
- }
34
- ```
35
-
36
- Response:
37
- ```json
38
- {
39
- "ok": true,
40
- "event_id": "evt_123",
41
- "signature_id": "sig_456",
42
- "candidate_ids": ["cand_789"]
67
+ "structured_fields": { "peer": "203.0.113.10" },
68
+ "correlation_keys": {},
69
+ "metadata": {}
43
70
  }
44
71
  ```
45
72
 
46
- ---
73
+ Optional query params:
74
+ - `threshold` (default: `2`)
75
+ - `db_path` (default: `BRAINSTEM_DB_PATH` or `.brainstem-state/brainstem.sqlite3`)
47
76
 
48
- ## 2. Bulk Ingest
77
+ The response includes ingest accounting for this request:
49
78
 
50
- ### `POST /events/ingest_bulk`
51
-
52
- For log file batches, webhook batches, or replay.
53
-
54
- Request:
55
- ```json
56
- {
57
- "events": [ ... ],
58
- "source_label": "syslog-replay"
59
- }
60
- ```
61
-
62
- Response:
63
79
  ```json
64
80
  {
65
81
  "ok": true,
66
- "ingested": 500,
67
- "signatures_created": 12,
68
- "candidates_created": 6
82
+ "tenant_id": "demo-tenant",
83
+ "event_count": 1,
84
+ "signature_count": 1,
85
+ "candidate_count": 0,
86
+ "parse_failed": 0,
87
+ "interesting_items": []
69
88
  }
70
89
  ```
71
90
 
72
- ---
73
-
74
- ## 3. Search Events
91
+ `POST /ingest/batch` accepts:
75
92
 
76
- ### `POST /events/search`
77
-
78
- Search raw/normalized events.
79
-
80
- Request:
81
93
  ```json
82
94
  {
83
- "tenant_id": "client-a",
84
- "query": "rekey failed",
85
- "host": "fw-01",
86
- "service": "charon",
87
- "since": "2026-03-22T00:00:00Z",
88
- "limit": 50
89
- }
90
- ```
91
-
92
- Response:
93
- ```json
94
- {
95
- "ok": true,
96
- "results": [ ... ]
95
+ "threshold": 2,
96
+ "db_path": "/tmp/brainstem.sqlite3",
97
+ "events": [
98
+ {
99
+ "tenant_id": "demo-tenant",
100
+ "source_type": "syslog",
101
+ "message_raw": "Mar 22 03:00:00 fw-01 charon: VPN tunnel recovered",
102
+ "host": "fw-01",
103
+ "service": "charon"
104
+ },
105
+ {
106
+ "tenant_id": "demo-tenant",
107
+ "source_type": "syslog",
108
+ "message_raw": "Mar 22 03:00:10 fw-01 charon: VPN tunnel recovered",
109
+ "host": "fw-01",
110
+ "service": "charon"
111
+ }
112
+ ]
97
113
  }
98
114
  ```
99
115
 
100
- ---
101
-
102
- ## 4. Search Candidates
116
+ Response shape is the same as single-event ingest, with counts based on all batch rows.
103
117
 
104
- ### `POST /candidates/search`
118
+ `POST /ingest/logicmonitor` accepts:
105
119
 
106
- Search meaningful derived patterns.
107
-
108
- Request:
109
120
  ```json
110
121
  {
111
- "tenant_id": "client-a",
112
- "decision_band": ["review", "urgent_human_review"],
113
- "candidate_type": ["recurrence", "self_heal"],
114
- "limit": 20
115
- }
116
- ```
117
-
118
- Response:
119
- ```json
120
- {
121
- "ok": true,
122
- "results": [ ... ]
122
+ "tenant_id": "demo-tenant",
123
+ "source_path": "/logicmonitor/ingest",
124
+ "threshold": 2,
125
+ "db_path": "/tmp/brainstem.sqlite3",
126
+ "events": [
127
+ {
128
+ "resource_id": 123,
129
+ "resource_name": "edge-fw-01",
130
+ "message_raw": "VPN tunnel dropped and recovered",
131
+ "severity": "warning",
132
+ "metadata": {
133
+ "datasource": "IPSec Tunnel"
134
+ }
135
+ }
136
+ ]
123
137
  }
124
138
  ```
125
139
 
126
- ---
140
+ Response shape is the same as other ingest endpoints.
127
141
 
128
- ## 5. Get Candidate Explanation
142
+ ## Replay raw envelopes
129
143
 
130
- ### `POST /candidates/explain`
144
+ `POST /replay/raw` requires a DB-backed set of raw envelope IDs:
131
145
 
132
- Request:
133
146
  ```json
134
147
  {
135
- "candidate_id": "cand_789"
148
+ "db_path": "/tmp/brainstem.sqlite3",
149
+ "raw_envelope_ids": [1, 2, 5],
150
+ "threshold": 2,
151
+ "force": false,
152
+ "allowed_statuses": ["received", "parse_failed"]
136
153
  }
137
154
  ```
138
155
 
139
- Response:
156
+ Valid status values are:
157
+ - `received`
158
+ - `canonicalized`
159
+ - `parse_failed`
160
+ - `unsupported`
161
+
162
+ Response example:
163
+
140
164
  ```json
141
165
  {
142
166
  "ok": true,
143
- "candidate": { ... },
144
- "score_breakdown": {
145
- "recurrence": 0.8,
146
- "recovery": 0.6,
147
- "precursor": 0.7
148
- },
149
- "evidence": {
150
- "event_count": 12,
151
- "signature_ids": ["sig_456"],
152
- "related_incidents": ["inc_100"]
153
- },
154
- "explanation": "This recurring self-healing VPN issue has increased in frequency and matched a prior promoted incident."
167
+ "db_path": "/tmp/brainstem.sqlite3",
168
+ "requested_raw_envelope_ids": [1,2,5],
169
+ "attempted_raw_envelope_ids": [1,2],
170
+ "skipped": [
171
+ {"raw_envelope_id": 5, "reason": "not_replayable", "status": "canonicalized"}
172
+ ],
173
+ "event_count": 2,
174
+ "signature_count": 1,
175
+ "candidate_count": 1,
176
+ "parse_failed": 0
155
177
  }
156
178
  ```
157
179
 
158
- ---
159
-
160
- ## 6. Promote Candidate
180
+ ## Inspection endpoints
161
181
 
162
- ### `POST /candidates/promote`
182
+ ### `GET /interesting`
163
183
 
164
- Promote a candidate into durable incident memory.
184
+ Query:
185
+ - `limit` (default: `5`)
186
+ - `db_path` (required for non-empty output)
165
187
 
166
- Request:
188
+ Returns:
167
189
  ```json
168
- {
169
- "candidate_id": "cand_789",
170
- "title": "Recurring VPN rekey instability for client-a",
171
- "summary": "Observed 12 self-resolving rekey failures over 7 days.",
172
- "incident_type": "vpn_instability"
173
- }
190
+ {"ok": true, "items": []}
174
191
  ```
192
+ If `db_path` is omitted, items are empty by design.
175
193
 
176
- Response:
177
- ```json
178
- {
179
- "ok": true,
180
- "incident_memory_id": "inc_100"
181
- }
182
- ```
183
-
184
- ---
185
-
186
- ## 7. Search Incident Memory
194
+ ### `GET /candidates`
187
195
 
188
- ### `POST /incidents/search`
196
+ Query:
197
+ - `limit` (default: `5`)
198
+ - `candidate_type` (optional)
199
+ - `decision_band` (optional)
200
+ - `min_score_total` (optional)
201
+ - `db_path` (optional)
189
202
 
190
- Request:
203
+ Returns:
191
204
  ```json
192
- {
193
- "tenant_id": "client-a",
194
- "query": "vpn flaps",
195
- "limit": 10
196
- }
205
+ {"ok": true, "count": 0, "items": []}
197
206
  ```
198
207
 
199
- Response:
200
- ```json
201
- {
202
- "ok": true,
203
- "results": [ ... ]
204
- }
205
- ```
208
+ ### `GET /signatures`
206
209
 
207
- ---
210
+ Query:
211
+ - `limit` (default: `5`)
212
+ - `event_family` (optional)
213
+ - `service` (optional)
214
+ - `min_occurrence_count` (optional)
215
+ - `db_path` (optional)
208
216
 
209
- ## 8. Related History
217
+ Returns:
218
+ ```json
219
+ {"ok": true, "count": 0, "items": []}
220
+ ```
210
221
 
211
- ### `POST /history/related`
222
+ ### `GET /canonical_events`
212
223
 
213
- Given an event, signature, or candidate, return related past operational memory.
224
+ Query:
225
+ - `limit` (default: `20`)
226
+ - `tenant_id` (optional)
227
+ - `source` (optional)
228
+ - `host` (optional)
229
+ - `service` (optional)
230
+ - `severity` (optional)
231
+ - `db_path` (optional)
214
232
 
215
- Request:
233
+ Returns:
216
234
  ```json
217
- {
218
- "subject_type": "signature",
219
- "subject_id": "sig_456",
220
- "limit": 10
221
- }
235
+ {"ok": true, "count": 0, "items": []}
222
236
  ```
223
237
 
224
- Response:
238
+ ### `GET /stats`
239
+
240
+ Query:
241
+ - `db_path` (optional)
242
+
243
+ Returns:
225
244
  ```json
226
245
  {
227
246
  "ok": true,
228
- "related_candidates": [ ... ],
229
- "related_incidents": [ ... ],
230
- "related_lessons": [ ... ]
247
+ "received": 10,
248
+ "canonicalized": 9,
249
+ "parse_failed": 1,
250
+ "unsupported": 0,
251
+ "candidates_generated": 4,
252
+ "source_summaries": {
253
+ "source_type": [{"value":"syslog","count":9}],
254
+ "...": []
255
+ }
231
256
  }
232
257
  ```
233
258
 
234
- ---
235
-
236
- ## 9. Daily Digest
259
+ ### `GET /failures`
237
260
 
238
- ### `POST /digest/daily`
261
+ Query:
262
+ - `limit` (default: `20`)
263
+ - `status` (optional; one of received/canonicalized/parse_failed/unsupported)
264
+ - `db_path` (optional)
239
265
 
240
- Generate the operator digest for a tenant or environment.
241
-
242
- Request:
266
+ Returns:
243
267
  ```json
244
- {
245
- "tenant_id": "client-a",
246
- "since": "2026-03-21T00:00:00Z",
247
- "limit": 10
248
- }
268
+ {"ok": true, "items": [], "count": 0, "status": null}
249
269
  ```
250
270
 
251
- Response:
252
- ```json
253
- {
254
- "ok": true,
255
- "items": [
256
- {
257
- "candidate_id": "cand_789",
258
- "title": "Recurring VPN rekey instability",
259
- "decision_band": "review",
260
- "why_it_matters": "Repeated 12 times this week, self-resolved each time, and matches a prior incident.",
261
- "score_total": 0.84
262
- }
263
- ]
264
- }
265
- ```
271
+ ### `GET /ingest/recent`
266
272
 
267
- ---
273
+ Query:
274
+ - `limit` (default: `20`)
275
+ - `status` (optional filter)
276
+ - `db_path` (optional)
268
277
 
269
- ## 10. Review Decision
278
+ Returns latest raw envelopes ordered newest-first.
270
279
 
271
- ### `POST /review/record`
280
+ ### `GET /failures/{raw_envelope_id}`
272
281
 
273
- Capture operator judgment.
282
+ Returns one raw envelope row or 404 if missing.
274
283
 
275
- Request:
276
- ```json
277
- {
278
- "tenant_id": "client-a",
279
- "subject_type": "candidate",
280
- "subject_id": "cand_789",
281
- "decision": "useful",
282
- "notes": "This pattern caused tickets last month.",
283
- "reviewer": "steven"
284
- }
285
- ```
284
+ ### `GET /raw_envelopes`
286
285
 
287
- Response:
288
- ```json
289
- {
290
- "ok": true,
291
- "review_id": "rev_101"
292
- }
293
- ```
286
+ Query:
287
+ - `limit` (default: `20`)
288
+ - `status` (optional; one of received/canonicalized/parse_failed/unsupported)
289
+ - `tenant_id` (optional)
290
+ - `source_type` (optional)
291
+ - `source_id` (optional)
292
+ - `source_path` (optional)
293
+ - `db_path` (optional)
294
294
 
295
- ---
295
+ Returns matching raw envelopes ordered newest-first with compact inspection fields (tenant/source metadata, message, status, parsing metadata).
296
296
 
297
- ## 11. LogicMonitor ingest
297
+ ### `GET /sources`
298
298
 
299
- ### `POST /connectors/logicmonitor/events`
299
+ Query:
300
+ - `limit` (default: `10`)
301
+ - `db_path` (optional)
300
302
 
301
- Accept LogicMonitor-originated alert/event payloads.
303
+ Returns per-dimension counts for source metadata.
302
304
 
303
- Request:
304
- ```json
305
- {
306
- "tenant_id": "client-a",
307
- "resource_id": 12345,
308
- "host": "edge-fw-01",
309
- "service": "vpn",
310
- "severity": "warning",
311
- "alert_id": 998877,
312
- "message_raw": "VPN tunnel dropped and recovered",
313
- "timestamp": "2026-03-22T00:00:00Z",
314
- "metadata": {
315
- "datasource": "IPSec Tunnel",
316
- "instance_name": "site-b",
317
- "acknowledged": false,
318
- "cleared_at": "2026-03-22T00:00:32Z"
319
- }
320
- }
321
- ```
305
+ ### `GET /sources/status`
322
306
 
323
- Response:
324
- ```json
325
- {
326
- "ok": true,
327
- "event_id": "evt_123",
328
- "signature_id": "sig_456",
329
- "candidate_ids": ["cand_789"]
330
- }
331
- ```
307
+ Query:
308
+ - `limit` (default: `20`)
309
+ - `tenant_id` (optional filter)
310
+ - `source_type` (optional filter)
311
+ - `source_id` (optional filter)
312
+ - `source_path` (optional filter)
313
+ - `db_path` (optional)
332
314
 
333
- ### `POST /connectors/logicmonitor/sync`
315
+ Returns one row per source dimension with raw/failed/canonicalized counts.
334
316
 
335
- Poll/sync LogicMonitor data in batches.
317
+ ### `GET /healthz`
336
318
 
337
- Request:
338
- ```json
339
- {
340
- "tenant_id": "client-a",
341
- "since": "2026-03-21T00:00:00Z",
342
- "mode": "alerts"
343
- }
344
- ```
319
+ Always available; useful for liveness.
345
320
 
346
- Response:
347
- ```json
348
- {
349
- "ok": true,
350
- "ingested": 250,
351
- "signatures_created": 9,
352
- "candidates_created": 4
353
- }
354
- ```
321
+ Response is identical to `GET /status` and includes:
322
+ - `ok`
323
+ - `status`
324
+ - runtime summary including auth/capability/config state
355
325
 
356
- ## 12. Health / Readiness
326
+ ### `GET /status`
357
327
 
358
- ### `GET /healthz`
359
- Simple liveness/readiness.
328
+ Operator-oriented runtime summary with the same payload as `/healthz`.
329
+ - `ok`
330
+ - `status`
331
+ - `runtime`: version, auth_state, defaults, limits, capabilities
332
+ - `api_token_enabled`
360
333
 
361
- ### `GET /status`
362
- Compact runtime and ingest status.
334
+ ### `GET /runtime`
335
+
336
+ Returns canonical runtime snapshot:
337
+ - `version`
338
+ - `api_token_env`
339
+ - `capability_flags.source_capabilities` (`source_types` + per-source ingest-mode matrix)
340
+ - runtime defaults and limits
341
+ - listener defaults
342
+ - endpoint capability flags
343
+
344
+ Current `capability_flags.source_capabilities` includes:
345
+ - `source_types`: `file`, `logicmonitor`, `syslog`
346
+ - `ingest_modes_by_source_type`:
347
+ - `syslog`: `single_event_api`, `batch_api`, `udp_listener`
348
+ - `file`: `single_event_api`, `batch_api`
349
+ - `logicmonitor`: `logicmonitor_webhook`
363
350
 
364
- ### `GET /metrics`
365
- Bounded metrics and counts. Must not block on deep correlation or full integrity scans.
351
+ `GET /runtime` is a fuller diagnostic snapshot with the same runtime summary object used by `/status`/`/healthz`.
366
352
 
367
- ---
353
+ ## Listener + file/syslog foundation alignment
368
354
 
369
- ## MVP API subset
355
+ The runtime path is deliberately split:
356
+ - API path (`/ingest/*`) for HTTP ingestion, replay, and inspection
357
+ - UDP syslog path (`brainstem.listener`) for direct syslog intake and optional stdout event emission
358
+ - file intake for helper ingestion flow through `source_type: file` payloads
370
359
 
371
- For MVP, implement first:
372
- - `/events/ingest`
373
- - `/events/ingest_bulk`
374
- - `/candidates/search`
375
- - `/candidates/explain`
376
- - `/candidates/promote`
377
- - `/digest/daily`
378
- - `/history/related`
379
- - `/healthz`
380
- - `/metrics`
360
+ For listener operator notes and full step-by-step startup, see [docs/README.md](README.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simbimbo/brainstem",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "brAInstem — operational memory for weak signals.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "brainstem"
7
- version = "0.0.3"
7
+ version = "0.0.5"
8
8
  description = "brAInstem — operational memory for weak signals."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"