@simbimbo/brainstem 0.0.2 → 0.0.4

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