@simbimbo/brainstem 0.0.1 → 0.0.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 (41) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +99 -3
  3. package/brainstem/__init__.py +3 -0
  4. package/brainstem/api.py +257 -0
  5. package/brainstem/connectors/__init__.py +1 -0
  6. package/brainstem/connectors/logicmonitor.py +26 -0
  7. package/brainstem/connectors/types.py +16 -0
  8. package/brainstem/demo.py +64 -0
  9. package/brainstem/fingerprint.py +44 -0
  10. package/brainstem/ingest.py +108 -0
  11. package/brainstem/instrumentation.py +38 -0
  12. package/brainstem/interesting.py +62 -0
  13. package/brainstem/models.py +80 -0
  14. package/brainstem/recurrence.py +112 -0
  15. package/brainstem/scoring.py +38 -0
  16. package/brainstem/storage.py +428 -0
  17. package/docs/adapters.md +435 -0
  18. package/docs/api.md +380 -0
  19. package/docs/architecture.md +333 -0
  20. package/docs/connectors.md +66 -0
  21. package/docs/data-model.md +290 -0
  22. package/docs/design-governance.md +595 -0
  23. package/docs/mvp-flow.md +109 -0
  24. package/docs/roadmap.md +87 -0
  25. package/docs/scoring.md +424 -0
  26. package/docs/v0.0.1.md +277 -0
  27. package/docs/vision.md +85 -0
  28. package/package.json +6 -14
  29. package/pyproject.toml +18 -0
  30. package/tests/fixtures/sample_syslog.log +6 -0
  31. package/tests/test_api.py +319 -0
  32. package/tests/test_canonicalization.py +28 -0
  33. package/tests/test_demo.py +25 -0
  34. package/tests/test_fingerprint.py +22 -0
  35. package/tests/test_ingest.py +15 -0
  36. package/tests/test_instrumentation.py +16 -0
  37. package/tests/test_interesting.py +36 -0
  38. package/tests/test_logicmonitor.py +22 -0
  39. package/tests/test_recurrence.py +16 -0
  40. package/tests/test_scoring.py +21 -0
  41. package/tests/test_storage.py +294 -0
@@ -0,0 +1,319 @@
1
+ from pathlib import Path
2
+
3
+ from fastapi.testclient import TestClient
4
+
5
+ from brainstem.api import app
6
+ from brainstem.models import RawInputEnvelope
7
+ from brainstem.storage import (
8
+ init_db,
9
+ set_raw_envelope_status,
10
+ store_raw_envelopes,
11
+ )
12
+
13
+
14
+ def test_ingest_event_endpoint_round_trip(tmp_path: Path) -> None:
15
+ client = TestClient(app)
16
+ db_path = tmp_path / "brainstem.sqlite3"
17
+ response = client.post(
18
+ "/ingest/event?threshold=2&db_path=" + str(db_path),
19
+ json={
20
+ "tenant_id": "client-a",
21
+ "source_type": "syslog",
22
+ "message_raw": "VPN tunnel dropped and recovered",
23
+ "host": "fw-01",
24
+ "service": "charon",
25
+ "severity": "info",
26
+ },
27
+ )
28
+ assert response.status_code == 200
29
+ payload = response.json()
30
+ assert payload["ok"] is True
31
+ assert payload["event_count"] == 1
32
+ assert payload["signature_count"] == 1
33
+
34
+
35
+ def test_ingest_batch_and_interesting(tmp_path: Path) -> None:
36
+ client = TestClient(app)
37
+ db_path = tmp_path / "brainstem.sqlite3"
38
+ response = client.post(
39
+ "/ingest/batch",
40
+ json={
41
+ "threshold": 2,
42
+ "db_path": str(db_path),
43
+ "events": [
44
+ {
45
+ "tenant_id": "client-a",
46
+ "source_type": "syslog",
47
+ "message_raw": "Failed password for admin from 10.1.2.3",
48
+ "host": "fw-01",
49
+ "service": "sshd",
50
+ },
51
+ {
52
+ "tenant_id": "client-a",
53
+ "source_type": "syslog",
54
+ "message_raw": "Failed password for admin from 10.1.2.3",
55
+ "host": "fw-01",
56
+ "service": "sshd",
57
+ },
58
+ ],
59
+ },
60
+ )
61
+ assert response.status_code == 200
62
+ payload = response.json()
63
+ assert payload["ok"] is True
64
+ assert payload["event_count"] == 2
65
+ assert payload["candidate_count"] >= 1
66
+
67
+ interesting = client.get(f"/interesting?db_path={db_path}&limit=10")
68
+ assert interesting.status_code == 200
69
+ interesting_payload = interesting.json()
70
+ assert interesting_payload["ok"] is True
71
+ assert interesting_payload["items"]
72
+
73
+
74
+ def test_stats_after_successful_and_failed_ingest(tmp_path: Path) -> None:
75
+ client = TestClient(app)
76
+ db_path = tmp_path / "brainstem.sqlite3"
77
+ batch_response = client.post(
78
+ "/ingest/batch",
79
+ json={
80
+ "threshold": 2,
81
+ "db_path": str(db_path),
82
+ "events": [
83
+ {
84
+ "tenant_id": "client-a",
85
+ "source_type": "syslog",
86
+ "message_raw": "Failed password for admin from 10.1.2.3",
87
+ "host": "fw-01",
88
+ "service": "sshd",
89
+ },
90
+ {
91
+ "tenant_id": "client-a",
92
+ "source_type": "syslog",
93
+ "message_raw": "Failed password for admin from 10.1.2.3",
94
+ "host": "fw-01",
95
+ "service": "sshd",
96
+ },
97
+ {
98
+ "tenant_id": "client-a",
99
+ "source_type": "syslog",
100
+ "message_raw": "",
101
+ "host": "fw-01",
102
+ "service": "sshd",
103
+ },
104
+ ],
105
+ },
106
+ )
107
+ assert batch_response.status_code == 200
108
+ batch_payload = batch_response.json()
109
+ assert batch_payload["ok"] is True
110
+ assert batch_payload["event_count"] == 2
111
+ assert batch_payload["parse_failed"] == 1
112
+
113
+ stats = client.get(f"/stats?db_path={db_path}")
114
+ assert stats.status_code == 200
115
+ stats_payload = stats.json()
116
+ assert stats_payload["ok"] is True
117
+ assert stats_payload["received"] == 3
118
+ assert stats_payload["canonicalized"] == 2
119
+ assert stats_payload["parse_failed"] == 1
120
+ assert stats_payload["candidates_generated"] >= 1
121
+
122
+
123
+ def test_healthz_is_ready() -> None:
124
+ client = TestClient(app)
125
+ response = client.get("/healthz")
126
+ assert response.status_code == 200
127
+ assert response.json()["ok"] is True
128
+
129
+
130
+ def test_failures_endpoint_lists_recent_parse_failures(tmp_path: Path) -> None:
131
+ client = TestClient(app)
132
+ db_path = tmp_path / "brainstem.sqlite3"
133
+ client.post(
134
+ "/ingest/batch",
135
+ json={
136
+ "threshold": 2,
137
+ "db_path": str(db_path),
138
+ "events": [
139
+ {
140
+ "tenant_id": "client-a",
141
+ "source_type": "syslog",
142
+ "message_raw": "",
143
+ "host": "fw-01",
144
+ "service": "sshd",
145
+ },
146
+ {
147
+ "tenant_id": "client-a",
148
+ "source_type": "syslog",
149
+ "message_raw": "VPN tunnel dropped and recovered",
150
+ "host": "fw-01",
151
+ "service": "charon",
152
+ },
153
+ ],
154
+ },
155
+ )
156
+
157
+ response = client.get(f"/failures?db_path={db_path}&limit=10")
158
+ assert response.status_code == 200
159
+ payload = response.json()
160
+ assert payload["ok"] is True
161
+ assert payload["count"] == 1
162
+ assert payload["items"][0]["canonicalization_status"] == "parse_failed"
163
+
164
+
165
+ def test_failures_endpoint_filters_by_status_and_fetches_single_record(tmp_path: Path) -> None:
166
+ client = TestClient(app)
167
+ db_path = tmp_path / "brainstem.sqlite3"
168
+ init_db(str(db_path))
169
+ raw_ids = store_raw_envelopes(
170
+ [
171
+ RawInputEnvelope(
172
+ tenant_id="client-a",
173
+ source_type="syslog",
174
+ timestamp="2026-03-22T00:00:01Z",
175
+ message_raw="first",
176
+ host="fw-01",
177
+ service="sshd",
178
+ ),
179
+ RawInputEnvelope(
180
+ tenant_id="client-a",
181
+ source_type="syslog",
182
+ timestamp="2026-03-22T00:00:02Z",
183
+ message_raw="second",
184
+ host="fw-01",
185
+ service="sshd",
186
+ ),
187
+ ],
188
+ db_path=str(db_path),
189
+ )
190
+ set_raw_envelope_status(raw_ids[0], "parse_failed", db_path=str(db_path), failure_reason="bad parse")
191
+ set_raw_envelope_status(raw_ids[1], "unsupported", db_path=str(db_path), failure_reason="unsupported source")
192
+
193
+ failed_only = client.get(f"/failures?db_path={db_path}&status=parse_failed&limit=10")
194
+ assert failed_only.status_code == 200
195
+ failed_payload = failed_only.json()
196
+ assert failed_payload["count"] == 1
197
+ assert failed_payload["items"][0]["id"] == raw_ids[0]
198
+
199
+ unsupported = client.get(f"/failures?db_path={db_path}&status=unsupported&limit=10")
200
+ assert unsupported.status_code == 200
201
+ unsupported_payload = unsupported.json()
202
+ assert unsupported_payload["count"] == 1
203
+ assert unsupported_payload["items"][0]["id"] == raw_ids[1]
204
+
205
+ single = client.get(f"/failures/{raw_ids[1]}?db_path={db_path}")
206
+ assert single.status_code == 200
207
+ single_payload = single.json()
208
+ assert single_payload["ok"] is True
209
+ assert single_payload["item"]["id"] == raw_ids[1]
210
+ assert single_payload["item"]["failure_reason"] == "unsupported source"
211
+
212
+ invalid = client.get(f"/failures?db_path={db_path}&status=bogus")
213
+ assert invalid.status_code == 422
214
+
215
+
216
+ def test_sources_endpoint_summarizes_ingest_dimensions(tmp_path: Path) -> None:
217
+ client = TestClient(app)
218
+ db_path = tmp_path / "brainstem.sqlite3"
219
+ batch_response = client.post(
220
+ "/ingest/batch",
221
+ json={
222
+ "threshold": 1,
223
+ "db_path": str(db_path),
224
+ "events": [
225
+ {
226
+ "tenant_id": "client-a",
227
+ "source_type": "syslog",
228
+ "source_id": "fw-01",
229
+ "source_name": "edge-fw-01",
230
+ "source_path": "/var/log/syslog",
231
+ "message_raw": "Failed password for admin from 10.1.2.3",
232
+ "host": "fw-01",
233
+ "service": "sshd",
234
+ "severity": "info",
235
+ },
236
+ {
237
+ "tenant_id": "client-a",
238
+ "source_type": "syslog",
239
+ "source_id": "fw-01",
240
+ "source_name": "edge-fw-01",
241
+ "source_path": "/var/log/syslog",
242
+ "message_raw": "Failed password for admin from 10.1.2.3",
243
+ "host": "fw-01",
244
+ "service": "sshd",
245
+ "severity": "info",
246
+ },
247
+ {
248
+ "tenant_id": "client-a",
249
+ "source_type": "logicmonitor",
250
+ "source_id": "lm-01",
251
+ "source_name": "edge-lm-01",
252
+ "source_path": "/alerts",
253
+ "message_raw": "Disk space low",
254
+ "host": "lm-01",
255
+ "service": "logicmonitor",
256
+ "severity": "warning",
257
+ },
258
+ ],
259
+ },
260
+ )
261
+ assert batch_response.status_code == 200
262
+
263
+ response = client.get(f"/sources?db_path={db_path}&limit=10")
264
+ assert response.status_code == 200
265
+ payload = response.json()
266
+ assert payload["ok"] is True
267
+ assert payload["items"]["source_type"] == [
268
+ {"value": "syslog", "count": 2},
269
+ {"value": "logicmonitor", "count": 1},
270
+ ]
271
+ assert dict((entry["value"], entry["count"]) for entry in payload["items"]["source_name"]) == {
272
+ "edge-fw-01": 2,
273
+ "edge-lm-01": 1,
274
+ }
275
+
276
+
277
+ def test_ingest_recent_endpoint_returns_recent_intake_and_allows_status_filter(tmp_path: Path) -> None:
278
+ client = TestClient(app)
279
+ db_path = tmp_path / "brainstem.sqlite3"
280
+ client.post(
281
+ "/ingest/batch",
282
+ json={
283
+ "threshold": 1,
284
+ "db_path": str(db_path),
285
+ "events": [
286
+ {
287
+ "tenant_id": "client-a",
288
+ "source_type": "syslog",
289
+ "source_id": "fw-01",
290
+ "source_name": "edge-fw-01",
291
+ "message_raw": "service restarted",
292
+ "host": "fw-01",
293
+ "service": "systemd",
294
+ },
295
+ {
296
+ "tenant_id": "client-a",
297
+ "source_type": "syslog",
298
+ "source_id": "fw-01",
299
+ "source_name": "edge-fw-01",
300
+ "message_raw": "",
301
+ "host": "fw-01",
302
+ "service": "systemd",
303
+ },
304
+ ],
305
+ },
306
+ )
307
+
308
+ response = client.get(f"/ingest/recent?db_path={db_path}&limit=10")
309
+ assert response.status_code == 200
310
+ payload = response.json()
311
+ assert payload["ok"] is True
312
+ assert payload["count"] == 2
313
+ assert len({item["canonicalization_status"] for item in payload["items"]}) == 2
314
+
315
+ failed = client.get(f"/ingest/recent?db_path={db_path}&status=parse_failed&limit=10")
316
+ assert failed.status_code == 200
317
+ failed_payload = failed.json()
318
+ assert failed_payload["count"] == 1
319
+ assert failed_payload["items"][0]["canonicalization_status"] == "parse_failed"
@@ -0,0 +1,28 @@
1
+ from brainstem.ingest import canonicalize_raw_input_envelope, parse_syslog_envelope, parse_syslog_line
2
+ from brainstem.models import CanonicalEvent, RawInputEnvelope
3
+
4
+
5
+ def test_parse_syslog_envelope_preserves_raw_fields() -> None:
6
+ raw = parse_syslog_envelope("Mar 22 00:00:01 fw-01 charon: VPN tunnel dropped and recovered", tenant_id="client-a", source_path="/var/log/syslog")
7
+ assert isinstance(raw, RawInputEnvelope)
8
+ assert raw.host == "fw-01"
9
+ assert raw.service == "charon"
10
+ assert raw.metadata["raw_line"]
11
+
12
+
13
+ def test_canonicalization_from_raw_envelope_is_explicit() -> None:
14
+ raw = parse_syslog_envelope("Mar 22 00:00:01 fw-01 charon: VPN tunnel dropped and recovered", tenant_id="client-a", source_path="/var/log/syslog")
15
+ canonical = canonicalize_raw_input_envelope(raw)
16
+ assert isinstance(canonical, CanonicalEvent)
17
+ assert canonical.message_normalized == "vpn tunnel dropped and recovered"
18
+ assert canonical.signature_input == canonical.message_normalized
19
+ assert canonical.ingest_metadata["canonicalization_source"] == "syslog"
20
+ assert canonical.ingest_metadata["raw_input_seen"] is True
21
+ assert canonical.ingest_metadata["raw_line"] == "Mar 22 00:00:01 fw-01 charon: VPN tunnel dropped and recovered"
22
+
23
+
24
+ def test_parse_syslog_line_still_returns_canonical_event() -> None:
25
+ event = parse_syslog_line("Mar 22 00:00:01 fw-01 charon: VPN tunnel dropped and recovered", tenant_id="client-a", source_path="/var/log/syslog")
26
+ assert isinstance(event, CanonicalEvent)
27
+ assert event.message_normalized
28
+ assert event.ingest_metadata["canonicalization_source"] == "syslog"
@@ -0,0 +1,25 @@
1
+ from pathlib import Path
2
+
3
+ from brainstem.demo import run_syslog_demo
4
+
5
+
6
+ FIXTURE = Path(__file__).parent / "fixtures" / "sample_syslog.log"
7
+
8
+
9
+ def test_run_syslog_demo_emits_digest_for_recurrence(tmp_path: Path) -> None:
10
+ payload = run_syslog_demo(str(FIXTURE), tenant_id="client-a", threshold=2, db_path=str(tmp_path / 'brainstem.sqlite3'))
11
+ assert payload["ok"] is True
12
+ assert payload["event_count"] == 6
13
+ assert payload["signature_count"] >= 2
14
+ assert payload["candidate_count"] >= 1
15
+ assert payload["digest"]
16
+ assert payload["interesting_items"]
17
+ assert payload["top_candidate"] is not None
18
+
19
+
20
+ def test_run_syslog_demo_threshold_can_suppress_candidates() -> None:
21
+ payload = run_syslog_demo(str(FIXTURE), tenant_id="client-a", threshold=10)
22
+ assert payload["ok"] is True
23
+ assert payload["candidate_count"] == 0
24
+ assert payload["digest"] == []
25
+ assert payload["top_candidate"] is None
@@ -0,0 +1,22 @@
1
+ from brainstem.fingerprint import normalize_message, fingerprint_event
2
+ from brainstem.models import Event
3
+
4
+
5
+ def test_normalize_message_replaces_ips_and_numbers() -> None:
6
+ text = normalize_message("Auth failure from 10.1.2.3 after 17 retries")
7
+ assert "<ip>" in text
8
+ assert "<n>" in text
9
+
10
+
11
+ def test_fingerprint_event_builds_signature() -> None:
12
+ event = Event(
13
+ tenant_id="client-a",
14
+ source_type="syslog",
15
+ timestamp="2026-03-22T00:00:00Z",
16
+ host="fw-01",
17
+ service="sshd",
18
+ message_raw="Auth failure from 10.1.2.3 after 17 retries",
19
+ )
20
+ sig = fingerprint_event(event)
21
+ assert sig.signature_key.startswith("failure|sshd|") or sig.signature_key.startswith("auth|sshd|")
22
+ assert sig.normalized_pattern
@@ -0,0 +1,15 @@
1
+ from brainstem.ingest import ingest_syslog_lines, signatures_for_events
2
+
3
+
4
+ def test_ingest_syslog_lines_parses_basic_fields() -> None:
5
+ lines = [
6
+ 'Mar 22 00:00:01 fw-01 charon: VPN tunnel dropped and recovered',
7
+ 'Mar 22 00:00:03 fw-01 charon: VPN tunnel dropped and recovered',
8
+ ]
9
+ events = ingest_syslog_lines(lines, tenant_id='client-a', source_path='/var/log/syslog')
10
+ assert len(events) == 2
11
+ assert events[0].host == 'fw-01'
12
+ assert events[0].service == 'charon'
13
+ sigs = signatures_for_events(events)
14
+ assert len(sigs) == 2
15
+ assert sigs[0].signature_key == sigs[1].signature_key
@@ -0,0 +1,16 @@
1
+ from brainstem.instrumentation import emit, span
2
+
3
+
4
+ def test_emit_writes_json_log(capsys) -> None:
5
+ emit("demo_event", count=2)
6
+ captured = capsys.readouterr()
7
+ assert '"event": "demo_event"' in captured.err
8
+ assert '"count": 2' in captured.err
9
+
10
+
11
+ def test_span_emits_start_and_complete(capsys) -> None:
12
+ with span("unit_work", item_count=3):
13
+ pass
14
+ captured = capsys.readouterr()
15
+ assert '"event": "unit_work_start"' in captured.err
16
+ assert '"event": "unit_work_complete"' in captured.err
@@ -0,0 +1,36 @@
1
+ from brainstem.ingest import ingest_syslog_lines, signatures_for_events
2
+ from brainstem.interesting import interesting_items
3
+ from brainstem.recurrence import build_recurrence_candidates
4
+
5
+
6
+ def test_interesting_items_returns_ranked_observations() -> None:
7
+ lines = [
8
+ 'Mar 22 00:00:01 fw-01 charon: VPN tunnel dropped and recovered',
9
+ 'Mar 22 00:00:03 fw-01 charon: VPN tunnel dropped and recovered',
10
+ 'Mar 22 00:00:05 fw-01 charon: VPN tunnel dropped and recovered',
11
+ 'Mar 22 00:01:10 fw-01 sshd: Failed password for admin from 10.1.2.3 port 54422',
12
+ 'Mar 22 00:01:12 fw-01 sshd: Failed password for admin from 10.1.2.3 port 54425',
13
+ ]
14
+ events = ingest_syslog_lines(lines, tenant_id='client-a')
15
+ signatures = signatures_for_events(events)
16
+ candidates = build_recurrence_candidates(events, signatures, threshold=2)
17
+ items = interesting_items(candidates, limit=3)
18
+ assert items
19
+ assert items[0]['title']
20
+ assert 'why_it_matters' in items[0]
21
+ assert items[0]['signals']
22
+
23
+
24
+ def test_interesting_items_respects_limit() -> None:
25
+ lines = [
26
+ 'Mar 22 00:00:01 fw-01 charon: VPN tunnel dropped and recovered',
27
+ 'Mar 22 00:00:03 fw-01 charon: VPN tunnel dropped and recovered',
28
+ 'Mar 22 00:00:05 fw-01 charon: VPN tunnel dropped and recovered',
29
+ 'Mar 22 00:01:10 fw-01 sshd: Failed password for admin from 10.1.2.3 port 54422',
30
+ 'Mar 22 00:01:12 fw-01 sshd: Failed password for admin from 10.1.2.3 port 54425',
31
+ ]
32
+ events = ingest_syslog_lines(lines, tenant_id='client-a')
33
+ signatures = signatures_for_events(events)
34
+ candidates = build_recurrence_candidates(events, signatures, threshold=2)
35
+ items = interesting_items(candidates, limit=1)
36
+ assert len(items) == 1
@@ -0,0 +1,22 @@
1
+ from brainstem.connectors.logicmonitor import map_logicmonitor_event
2
+
3
+
4
+ def test_map_logicmonitor_event_normalizes_payload() -> None:
5
+ event = map_logicmonitor_event(
6
+ {
7
+ 'resource_id': 123,
8
+ 'resource_name': 'edge-fw-01',
9
+ 'severity': 'warning',
10
+ 'timestamp': '2026-03-22T00:00:00Z',
11
+ 'message_raw': 'VPN tunnel dropped and recovered',
12
+ 'metadata': {
13
+ 'datasource': 'IPSec Tunnel',
14
+ 'instance_name': 'site-b',
15
+ 'acknowledged': False,
16
+ },
17
+ },
18
+ tenant_id='client-a',
19
+ )
20
+ assert event.source_type == 'logicmonitor'
21
+ assert event.host == 'edge-fw-01'
22
+ assert event.metadata['resource_id'] == 123
@@ -0,0 +1,16 @@
1
+ from brainstem.ingest import ingest_syslog_lines, signatures_for_events
2
+ from brainstem.recurrence import build_recurrence_candidates, digest_items
3
+
4
+
5
+ def test_build_recurrence_candidates_emits_candidate_for_repeated_signature() -> None:
6
+ lines = [
7
+ 'Mar 22 00:00:01 fw-01 charon: VPN tunnel dropped and recovered',
8
+ 'Mar 22 00:00:03 fw-01 charon: VPN tunnel dropped and recovered',
9
+ 'Mar 22 00:00:05 fw-01 charon: VPN tunnel dropped and recovered',
10
+ ]
11
+ events = ingest_syslog_lines(lines, tenant_id='client-a', source_path='/var/log/syslog')
12
+ signatures = signatures_for_events(events)
13
+ candidates = build_recurrence_candidates(events, signatures, threshold=2)
14
+ assert candidates
15
+ digest = digest_items(candidates)
16
+ assert digest[0]['decision_band'] in {'watch', 'review', 'urgent_human_review', 'promote_to_incident_memory'}
@@ -0,0 +1,21 @@
1
+ from brainstem.scoring import score_candidate
2
+
3
+
4
+ def test_score_candidate_returns_band() -> None:
5
+ candidate = score_candidate(
6
+ recurrence=0.8,
7
+ recovery=0.7,
8
+ spread=0.4,
9
+ novelty=0.5,
10
+ impact=0.8,
11
+ precursor=0.7,
12
+ memory_weight=0.6,
13
+ )
14
+ assert candidate.score_total > 0
15
+ assert candidate.decision_band in {
16
+ "ignore",
17
+ "watch",
18
+ "review",
19
+ "urgent_human_review",
20
+ "promote_to_incident_memory",
21
+ }