hypermind 0.11.0__py3-none-any.whl
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.
- hmctl/__init__.py +7 -0
- hmctl/cli.py +947 -0
- hypermind/__init__.py +208 -0
- hypermind/_deprecations.py +108 -0
- hypermind/agent.py +2896 -0
- hypermind/agent_card.py +127 -0
- hypermind/api/__init__.py +32 -0
- hypermind/api/app.py +686 -0
- hypermind/capability.py +923 -0
- hypermind/consult.py +493 -0
- hypermind/consult_bias.py +112 -0
- hypermind/contract_net.py +219 -0
- hypermind/crypto/__init__.py +21 -0
- hypermind/crypto/cose_encrypt.py +126 -0
- hypermind/crypto/dkg.py +246 -0
- hypermind/crypto/domain.py +59 -0
- hypermind/crypto/frost.py +771 -0
- hypermind/crypto/hashing.py +38 -0
- hypermind/crypto/hlc.py +163 -0
- hypermind/crypto/hpke.py +277 -0
- hypermind/crypto/kms.py +196 -0
- hypermind/crypto/kms_backends/__init__.py +56 -0
- hypermind/crypto/kms_backends/aws.py +128 -0
- hypermind/crypto/kms_backends/azure.py +114 -0
- hypermind/crypto/kms_backends/gcp.py +97 -0
- hypermind/crypto/kms_backends/vault.py +120 -0
- hypermind/crypto/namespace_root.py +204 -0
- hypermind/crypto/pq.py +152 -0
- hypermind/crypto/room_epoch.py +97 -0
- hypermind/crypto/signing.py +245 -0
- hypermind/deliberation.py +355 -0
- hypermind/did_web.py +256 -0
- hypermind/dispute.py +858 -0
- hypermind/errors.py +218 -0
- hypermind/eval/__init__.py +49 -0
- hypermind/eval/calibration.py +307 -0
- hypermind/eval/comparison.py +251 -0
- hypermind/eval/replay.py +202 -0
- hypermind/eval/swarm_iq.py +718 -0
- hypermind/knowledge/__init__.py +17 -0
- hypermind/knowledge/citation_graph.py +141 -0
- hypermind/knowledge/correlated_agreement.py +167 -0
- hypermind/knowledge/embedding_registry.py +115 -0
- hypermind/knowledge/kernel_store.py +197 -0
- hypermind/knowledge/rekor.py +173 -0
- hypermind/knowledge/reputation.py +364 -0
- hypermind/knowledge/swarm_memory.py +523 -0
- hypermind/knowledge/transparency.py +409 -0
- hypermind/mcp/__init__.py +60 -0
- hypermind/mcp/bridge.py +151 -0
- hypermind/mcp/client.py +142 -0
- hypermind/mcp/provenance.py +214 -0
- hypermind/mcp/relata_tools.py +152 -0
- hypermind/mcp/server.py +248 -0
- hypermind/mcp/transport.py +206 -0
- hypermind/mind/__init__.py +150 -0
- hypermind/mind/active.py +234 -0
- hypermind/mind/belief.py +369 -0
- hypermind/mind/deliberator.py +625 -0
- hypermind/mind/diversity.py +144 -0
- hypermind/mind/evolve.py +184 -0
- hypermind/mind/federation.py +154 -0
- hypermind/mind/goals.py +117 -0
- hypermind/mind/integration.py +190 -0
- hypermind/mind/mediator.py +74 -0
- hypermind/mind/memory_store.py +129 -0
- hypermind/mind/policy.py +307 -0
- hypermind/mind/records.py +260 -0
- hypermind/mind/roles.py +190 -0
- hypermind/mind/sybil_guard.py +41 -0
- hypermind/mind/world_model.py +166 -0
- hypermind/namespace.py +515 -0
- hypermind/namespace_policy.py +254 -0
- hypermind/observability/__init__.py +25 -0
- hypermind/observability/asgi.py +214 -0
- hypermind/observability/cost.py +277 -0
- hypermind/observability/otel_export.py +200 -0
- hypermind/observability/recorder.py +344 -0
- hypermind/observability/swarm_trace.py +438 -0
- hypermind/pin_policy.py +116 -0
- hypermind/py.typed +0 -0
- hypermind/responders/__init__.py +87 -0
- hypermind/responders/anthropic.py +159 -0
- hypermind/responders/base.py +162 -0
- hypermind/responders/openai.py +199 -0
- hypermind/responders/router.py +254 -0
- hypermind/responders/structured.py +287 -0
- hypermind/responders/tools.py +444 -0
- hypermind/revocation.py +270 -0
- hypermind/rule_ids.py +135 -0
- hypermind/schemas/encrypted_statement.cddl +28 -0
- hypermind/schemas/namespace_accept.cddl +20 -0
- hypermind/schemas/namespace_create.cddl +25 -0
- hypermind/schemas/namespace_founding_attest.cddl +30 -0
- hypermind/schemas/namespace_invite.cddl +22 -0
- hypermind/schemas/wire-v0.1.cddl +73 -0
- hypermind/simlab/__init__.py +15 -0
- hypermind/simlab/_persona_registry.py +93 -0
- hypermind/simlab/_scenario_impl.py +2778 -0
- hypermind/simlab/app.py +2538 -0
- hypermind/simlab/backends/__init__.py +27 -0
- hypermind/simlab/backends/anchor.py +88 -0
- hypermind/simlab/backends/local.py +32 -0
- hypermind/simlab/backends/server.py +79 -0
- hypermind/simlab/cli_command.py +108 -0
- hypermind/simlab/config.py +106 -0
- hypermind/simlab/deployment.py +26 -0
- hypermind/simlab/knowledge.py +716 -0
- hypermind/simlab/learning.py +295 -0
- hypermind/simlab/modes/__init__.py +115 -0
- hypermind/simlab/modes/_eval_real_llm.py +373 -0
- hypermind/simlab/modes/aar.py +611 -0
- hypermind/simlab/modes/backtest.py +610 -0
- hypermind/simlab/modes/binary_forecast.py +69 -0
- hypermind/simlab/modes/conformance.py +1869 -0
- hypermind/simlab/modes/evaluation.py +2271 -0
- hypermind/simlab/modes/governance.py +648 -0
- hypermind/simlab/modes/redteam.py +534 -0
- hypermind/simlab/modes/tabletop.py +797 -0
- hypermind/simlab/modes/tournament.py +448 -0
- hypermind/simlab/modes/whatif.py +523 -0
- hypermind/simlab/namespace.py +125 -0
- hypermind/simlab/personas.py +477 -0
- hypermind/simlab/prompt_generator.py +172 -0
- hypermind/simlab/question_sets/geopolitics_q20.json +122 -0
- hypermind/simlab/question_sets/governance_q5.json +67 -0
- hypermind/simlab/question_sets/msft_outlook_q10.json +62 -0
- hypermind/simlab/question_sets/whatif_q3.json +38 -0
- hypermind/simlab/registry.py +325 -0
- hypermind/simlab/runner.py +239 -0
- hypermind/simlab/scenario.py +147 -0
- hypermind/simlab/swarms.py +180 -0
- hypermind/simlab/tool_handlers/__init__.py +4 -0
- hypermind/simlab/tool_handlers/alpha_vantage.py +39 -0
- hypermind/simlab/tool_handlers/brave.py +46 -0
- hypermind/simlab/tool_handlers/newsapi.py +50 -0
- hypermind/simlab/tool_handlers/openweather.py +46 -0
- hypermind/simlab/tool_handlers/pinecone.py +52 -0
- hypermind/simlab/tool_handlers/qdrant.py +57 -0
- hypermind/simlab/tool_handlers/query_knowledge_base.py +21 -0
- hypermind/simlab/tool_handlers/relata_recall.py +61 -0
- hypermind/simlab/tool_handlers/retrieve_document.py +21 -0
- hypermind/simlab/tool_handlers/serper.py +46 -0
- hypermind/simlab/tool_handlers/tavily.py +53 -0
- hypermind/simlab/tool_handlers/weaviate.py +65 -0
- hypermind/simlab/tool_handlers/wikipedia.py +64 -0
- hypermind/simlab/tool_handlers/wolfram.py +48 -0
- hypermind/simlab/tool_handlers/yahoo_finance.py +49 -0
- hypermind/simlab/tool_registry.py +568 -0
- hypermind/simlab/topic_adapter.py +338 -0
- hypermind/simlab/topic_questions.py +259 -0
- hypermind/storage/__init__.py +69 -0
- hypermind/storage/backend.py +71 -0
- hypermind/storage/relata.py +245 -0
- hypermind/storage/sqlite.py +258 -0
- hypermind/sync.py +191 -0
- hypermind/tal.py +175 -0
- hypermind/tasks.py +334 -0
- hypermind/testing.py +16 -0
- hypermind/tools/__init__.py +32 -0
- hypermind/tools/registry.py +61 -0
- hypermind/transport/__init__.py +22 -0
- hypermind/transport/anti_entropy.py +708 -0
- hypermind/transport/bus.py +206 -0
- hypermind/transport/knows_delta.py +204 -0
- hypermind/transport/libp2p.py +298 -0
- hypermind/transport/placement.py +58 -0
- hypermind/transport/tcp.py +797 -0
- hypermind/transport/tls_profile.py +417 -0
- hypermind/types.py +59 -0
- hypermind/uncertainty.py +222 -0
- hypermind/wire.py +897 -0
- hypermind/workflow/__init__.py +72 -0
- hypermind/workflow/engine.py +655 -0
- hypermind/workflow/plan.py +182 -0
- hypermind/workflow/repair.py +203 -0
- hypermind-0.11.0.dist-info/METADATA +524 -0
- hypermind-0.11.0.dist-info/RECORD +181 -0
- hypermind-0.11.0.dist-info/WHEEL +4 -0
- hypermind-0.11.0.dist-info/entry_points.txt +2 -0
- hypermind-0.11.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
"""Post-Incident After-Action Review mode — HLC time-travel.
|
|
2
|
+
|
|
3
|
+
The user submits an *incident timeline* (a list of HLC-stamped events
|
|
4
|
+
describing what happened). The mode replays the timeline through the
|
|
5
|
+
swarm: each event is a sealed claim, witnesses cosign, sceptics file
|
|
6
|
+
disputes when claims contradict, the swarm's belief state is checkpointed
|
|
7
|
+
at each HLC tick.
|
|
8
|
+
|
|
9
|
+
The result view is a **time slider**: drag it back to any HLC moment
|
|
10
|
+
and see what the swarm believed at that instant — which claims were
|
|
11
|
+
sealed, which were under dispute, which witnesses had cosigned, which
|
|
12
|
+
sources had been revoked.
|
|
13
|
+
|
|
14
|
+
This is the canonical answer to *"what did we know, when did we know
|
|
15
|
+
it, and who said so?"* — the question every incident review eventually
|
|
16
|
+
asks. HyperMind's HLC + transparency log + revocation list make the
|
|
17
|
+
answer cryptographically verifiable.
|
|
18
|
+
|
|
19
|
+
Spec hooks:
|
|
20
|
+
* §02 HLC — every event carries a deterministic HLC tick
|
|
21
|
+
* §11 Witness Transparency — witness cosign per claim
|
|
22
|
+
* §12 Revocation — sources can be revoked mid-timeline; prior claims
|
|
23
|
+
downweighted in retrospective replay
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import hashlib
|
|
29
|
+
import json
|
|
30
|
+
import time
|
|
31
|
+
from dataclasses import asdict, dataclass
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
from . import ModeSpec, register
|
|
36
|
+
|
|
37
|
+
# --- Result types ----------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class WitnessSig:
|
|
42
|
+
witness_kid: str
|
|
43
|
+
witness_label: str
|
|
44
|
+
sig_hex: str
|
|
45
|
+
ts_offset_ms: int # when cosignature was attached (relative to incident start)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class TimelineEvent:
|
|
50
|
+
"""A single sealed claim on the incident timeline."""
|
|
51
|
+
|
|
52
|
+
event_id: str
|
|
53
|
+
ts_offset_ms: int # relative to incident_start
|
|
54
|
+
hlc_logical: int # HLC logical clock value
|
|
55
|
+
hlc_counter: int # HLC counter
|
|
56
|
+
actor_label: str
|
|
57
|
+
actor_kid: str
|
|
58
|
+
severity: str # info|warn|alert|critical
|
|
59
|
+
title: str
|
|
60
|
+
detail: str
|
|
61
|
+
witness_sigs: list[WitnessSig] # cosigners
|
|
62
|
+
disputes: list[dict[str, Any]] # disputes opened against this claim
|
|
63
|
+
source_revoked_at_ms: int | None # if non-None, the source was revoked at this offset
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class BeliefSnapshot:
|
|
68
|
+
"""Swarm belief state at a single HLC tick. Used to power the slider."""
|
|
69
|
+
|
|
70
|
+
ts_offset_ms: int
|
|
71
|
+
hlc_logical: int
|
|
72
|
+
hlc_counter: int
|
|
73
|
+
sealed_claim_ids: list[str]
|
|
74
|
+
disputed_claim_ids: list[str]
|
|
75
|
+
revoked_source_ids: list[str]
|
|
76
|
+
confidence: float # aggregate confidence in the standing belief set
|
|
77
|
+
headline: str # one-line summary of the swarm's standing belief
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class AARReport:
|
|
82
|
+
incident_label: str
|
|
83
|
+
incident_summary: str
|
|
84
|
+
starting_hlc_logical: int
|
|
85
|
+
timeline: list[TimelineEvent]
|
|
86
|
+
belief_snapshots: list[BeliefSnapshot]
|
|
87
|
+
pivot_moments: list[dict[str, Any]] # {ts_offset_ms, label, why} — moments where belief flipped
|
|
88
|
+
final_findings: list[str] # the AAR's bottom-line findings
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# --- Input schema ----------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
INPUT_SCHEMA: dict[str, Any] = {
|
|
94
|
+
"type": "object",
|
|
95
|
+
"title": "Post-Incident After-Action Review",
|
|
96
|
+
"required": ["incident_label", "events"],
|
|
97
|
+
"properties": {
|
|
98
|
+
"incident_label": {
|
|
99
|
+
"type": "string",
|
|
100
|
+
"title": "Incident label",
|
|
101
|
+
"description": "e.g. '2026-04-12 cloud region outage'",
|
|
102
|
+
},
|
|
103
|
+
"incident_summary": {
|
|
104
|
+
"type": "string",
|
|
105
|
+
"title": "Incident summary (one paragraph)",
|
|
106
|
+
"format": "textarea",
|
|
107
|
+
"description": "What happened, in plain English.",
|
|
108
|
+
"default": "",
|
|
109
|
+
},
|
|
110
|
+
"events": {
|
|
111
|
+
"type": "array",
|
|
112
|
+
"title": "Timeline events (semicolon-separated)",
|
|
113
|
+
"description": (
|
|
114
|
+
"Each event: 'TS|actor|severity|title'. Times are minutes "
|
|
115
|
+
"since incident start (e.g. '0', '14', '47'). Severity ∈ "
|
|
116
|
+
"{info, warn, alert, critical}. Example: "
|
|
117
|
+
"'0|monitoring|info|first error spike;5|on-call|alert|"
|
|
118
|
+
"investigating;12|sre|critical|root cause confirmed'."
|
|
119
|
+
),
|
|
120
|
+
"examples": [
|
|
121
|
+
"0|monitoring|info|first error spike",
|
|
122
|
+
"5|on-call|alert|paging engineering",
|
|
123
|
+
"14|sre|critical|database failover triggered",
|
|
124
|
+
"27|security|warn|suspicious auth attempt detected",
|
|
125
|
+
"47|sre|info|service restored",
|
|
126
|
+
],
|
|
127
|
+
"default": [],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
RESULT_SCHEMA: dict[str, Any] = {
|
|
134
|
+
"type": "object",
|
|
135
|
+
"properties": {
|
|
136
|
+
"mode": {"type": "string", "const": "post-incident-aar"},
|
|
137
|
+
"incident_label": {"type": "string"},
|
|
138
|
+
"timeline": {"type": "array"},
|
|
139
|
+
"belief_snapshots": {"type": "array"},
|
|
140
|
+
"pivot_moments": {"type": "array"},
|
|
141
|
+
"final_findings": {"type": "array"},
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# --- Event parsing ---------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _parse_events(raw: list[str]) -> list[dict[str, Any]]:
|
|
150
|
+
"""Parse 'TS|actor|severity|title' strings. Permissive."""
|
|
151
|
+
out: list[dict[str, Any]] = []
|
|
152
|
+
for item in raw:
|
|
153
|
+
if not item or "|" not in item:
|
|
154
|
+
continue
|
|
155
|
+
parts = [p.strip() for p in item.split("|")]
|
|
156
|
+
if len(parts) < 4:
|
|
157
|
+
continue
|
|
158
|
+
try:
|
|
159
|
+
ts_min = float(parts[0])
|
|
160
|
+
except ValueError:
|
|
161
|
+
continue
|
|
162
|
+
out.append(
|
|
163
|
+
{
|
|
164
|
+
"ts_offset_ms": int(ts_min * 60_000),
|
|
165
|
+
"actor": parts[1] or "unknown",
|
|
166
|
+
"severity": parts[2].lower()
|
|
167
|
+
if parts[2].lower() in {"info", "warn", "alert", "critical"}
|
|
168
|
+
else "info",
|
|
169
|
+
"title": "|".join(parts[3:]),
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
out.sort(key=lambda e: e["ts_offset_ms"])
|
|
173
|
+
return out
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _seed_events_from_label(label: str) -> list[dict[str, Any]]:
|
|
177
|
+
"""Demo seed when the user gives no events."""
|
|
178
|
+
h = label.lower()
|
|
179
|
+
if "cloud" in h or "outage" in h:
|
|
180
|
+
return [
|
|
181
|
+
{
|
|
182
|
+
"ts_offset_ms": 0,
|
|
183
|
+
"actor": "monitoring",
|
|
184
|
+
"severity": "info",
|
|
185
|
+
"title": "elevated 5xx rate observed",
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"ts_offset_ms": 4 * 60_000,
|
|
189
|
+
"actor": "on-call",
|
|
190
|
+
"severity": "alert",
|
|
191
|
+
"title": "paged engineering on-call",
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"ts_offset_ms": 8 * 60_000,
|
|
195
|
+
"actor": "sre",
|
|
196
|
+
"severity": "warn",
|
|
197
|
+
"title": "suspecting database connection pool exhaustion",
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"ts_offset_ms": 14 * 60_000,
|
|
201
|
+
"actor": "sre",
|
|
202
|
+
"severity": "critical",
|
|
203
|
+
"title": "primary database degraded; failover initiated",
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"ts_offset_ms": 22 * 60_000,
|
|
207
|
+
"actor": "security",
|
|
208
|
+
"severity": "alert",
|
|
209
|
+
"title": "unusual authentication burst detected",
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"ts_offset_ms": 26 * 60_000,
|
|
213
|
+
"actor": "sre",
|
|
214
|
+
"severity": "warn",
|
|
215
|
+
"title": "failover stalled; manual intervention required",
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"ts_offset_ms": 33 * 60_000,
|
|
219
|
+
"actor": "sre",
|
|
220
|
+
"severity": "info",
|
|
221
|
+
"title": "secondary database promoted; throughput recovering",
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"ts_offset_ms": 41 * 60_000,
|
|
225
|
+
"actor": "security",
|
|
226
|
+
"severity": "info",
|
|
227
|
+
"title": "auth burst attributed to retry storm, not attack",
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"ts_offset_ms": 47 * 60_000,
|
|
231
|
+
"actor": "sre",
|
|
232
|
+
"severity": "info",
|
|
233
|
+
"title": "service fully restored",
|
|
234
|
+
},
|
|
235
|
+
]
|
|
236
|
+
if "cyber" in h or "breach" in h or "attack" in h:
|
|
237
|
+
return [
|
|
238
|
+
{
|
|
239
|
+
"ts_offset_ms": 0,
|
|
240
|
+
"actor": "siem",
|
|
241
|
+
"severity": "warn",
|
|
242
|
+
"title": "anomalous outbound traffic from staging",
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"ts_offset_ms": 6 * 60_000,
|
|
246
|
+
"actor": "soc",
|
|
247
|
+
"severity": "alert",
|
|
248
|
+
"title": "C2 beacon pattern matched; isolating host",
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"ts_offset_ms": 11 * 60_000,
|
|
252
|
+
"actor": "soc",
|
|
253
|
+
"severity": "critical",
|
|
254
|
+
"title": "lateral movement confirmed in dev VPC",
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
"ts_offset_ms": 18 * 60_000,
|
|
258
|
+
"actor": "ir",
|
|
259
|
+
"severity": "alert",
|
|
260
|
+
"title": "credential rotation initiated",
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"ts_offset_ms": 25 * 60_000,
|
|
264
|
+
"actor": "ir",
|
|
265
|
+
"severity": "warn",
|
|
266
|
+
"title": "scope of compromise narrowed to one cluster",
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
"ts_offset_ms": 38 * 60_000,
|
|
270
|
+
"actor": "ir",
|
|
271
|
+
"severity": "info",
|
|
272
|
+
"title": "containment achieved",
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"ts_offset_ms": 52 * 60_000,
|
|
276
|
+
"actor": "ir",
|
|
277
|
+
"severity": "info",
|
|
278
|
+
"title": "forensic image captured for post-mortem",
|
|
279
|
+
},
|
|
280
|
+
]
|
|
281
|
+
return [
|
|
282
|
+
{"ts_offset_ms": 0, "actor": "system", "severity": "info", "title": "incident declared"},
|
|
283
|
+
{
|
|
284
|
+
"ts_offset_ms": 12 * 60_000,
|
|
285
|
+
"actor": "ops",
|
|
286
|
+
"severity": "alert",
|
|
287
|
+
"title": "investigation underway",
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"ts_offset_ms": 28 * 60_000,
|
|
291
|
+
"actor": "ops",
|
|
292
|
+
"severity": "warn",
|
|
293
|
+
"title": "mitigation applied",
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"ts_offset_ms": 45 * 60_000,
|
|
297
|
+
"actor": "ops",
|
|
298
|
+
"severity": "info",
|
|
299
|
+
"title": "incident closed",
|
|
300
|
+
},
|
|
301
|
+
]
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# --- Witness + dispute synth ----------------------------------------------
|
|
305
|
+
|
|
306
|
+
WITNESSES = [
|
|
307
|
+
("witness-sre", "SRE Witness"),
|
|
308
|
+
("witness-security", "Security Witness"),
|
|
309
|
+
("witness-product", "Product Witness"),
|
|
310
|
+
("witness-customer", "Customer-Facing Witness"),
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _kid(prefix: str, salt: str) -> str:
|
|
315
|
+
return hashlib.sha256(f"{prefix}::{salt}".encode()).hexdigest()[:32]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _witness_sigs_for(
|
|
319
|
+
event: dict[str, Any],
|
|
320
|
+
incident_salt: str,
|
|
321
|
+
) -> list[WitnessSig]:
|
|
322
|
+
"""Each event picks up 2–4 witness cosignatures depending on severity."""
|
|
323
|
+
sev_to_n = {"info": 2, "warn": 3, "alert": 3, "critical": 4}
|
|
324
|
+
n = sev_to_n.get(event["severity"], 2)
|
|
325
|
+
sigs: list[WitnessSig] = []
|
|
326
|
+
for i, (slug, label) in enumerate(WITNESSES[:n]):
|
|
327
|
+
sigs.append(
|
|
328
|
+
WitnessSig(
|
|
329
|
+
witness_kid=_kid(slug, incident_salt),
|
|
330
|
+
witness_label=label,
|
|
331
|
+
sig_hex=hashlib.sha256(
|
|
332
|
+
f"sig::{slug}::{event['title']}::{event['ts_offset_ms']}".encode(),
|
|
333
|
+
).hexdigest()[:32],
|
|
334
|
+
ts_offset_ms=event["ts_offset_ms"] + 30_000 * (i + 1), # cosignatures lag a bit
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
return sigs
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _disputes_for(
|
|
341
|
+
idx: int,
|
|
342
|
+
event: dict[str, Any],
|
|
343
|
+
all_events: list[dict[str, Any]],
|
|
344
|
+
incident_salt: str,
|
|
345
|
+
) -> list[dict[str, Any]]:
|
|
346
|
+
"""A few events earn disputes — typically when subsequent events
|
|
347
|
+
flatly contradict an earlier alert. Demo heuristic: the security
|
|
348
|
+
alert "auth burst" gets contradicted by the "retry storm" finding
|
|
349
|
+
later in the cloud-outage seed."""
|
|
350
|
+
out: list[dict[str, Any]] = []
|
|
351
|
+
text = event["title"].lower()
|
|
352
|
+
follows = [e["title"].lower() for e in all_events[idx + 1 :]]
|
|
353
|
+
|
|
354
|
+
if event["severity"] in {"alert", "critical"}:
|
|
355
|
+
# A claim is disputed if a later event with similar token
|
|
356
|
+
# ("auth", "attack", "breach") gets walked back ("retry",
|
|
357
|
+
# "false positive", "attributed to").
|
|
358
|
+
contradicted = any(
|
|
359
|
+
(
|
|
360
|
+
("auth" in text or "attack" in text or "breach" in text)
|
|
361
|
+
and ("retry" in f or "false" in f or "attributed" in f or "narrowed" in f)
|
|
362
|
+
)
|
|
363
|
+
for f in follows
|
|
364
|
+
)
|
|
365
|
+
if contradicted:
|
|
366
|
+
out.append(
|
|
367
|
+
{
|
|
368
|
+
"dispute_id": f"D-{idx:03d}",
|
|
369
|
+
"opened_by_kid": _kid("sceptic", incident_salt + str(idx)),
|
|
370
|
+
"opened_at_ms": event["ts_offset_ms"] + 8 * 60_000,
|
|
371
|
+
"rationale": (
|
|
372
|
+
"Subsequent evidence contradicts the alert's framing. "
|
|
373
|
+
"Recommend reclassifying as informational and updating "
|
|
374
|
+
"the post-incident timeline."
|
|
375
|
+
),
|
|
376
|
+
"final_state": "CLOSED",
|
|
377
|
+
"outcome": "claim downweighted",
|
|
378
|
+
}
|
|
379
|
+
)
|
|
380
|
+
return out
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _revoke_at(
|
|
384
|
+
idx: int,
|
|
385
|
+
event: dict[str, Any],
|
|
386
|
+
all_events: list[dict[str, Any]],
|
|
387
|
+
incident_salt: str,
|
|
388
|
+
) -> int | None:
|
|
389
|
+
"""Some sources get retroactively revoked. Demo heuristic: if the
|
|
390
|
+
event is alert/critical and its actor's later events walk it back,
|
|
391
|
+
flag the source as revoked at the walking-back moment."""
|
|
392
|
+
text = event["title"].lower()
|
|
393
|
+
for later in all_events[idx + 1 :]:
|
|
394
|
+
ltext = later["title"].lower()
|
|
395
|
+
if (
|
|
396
|
+
("auth" in text or "attack" in text or "breach" in text)
|
|
397
|
+
and later["actor"] == event["actor"]
|
|
398
|
+
and ("attributed" in ltext or "retry" in ltext or "false" in ltext)
|
|
399
|
+
):
|
|
400
|
+
return later["ts_offset_ms"]
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# --- Belief snapshot construction -----------------------------------------
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _build_snapshots(
|
|
408
|
+
timeline: list[TimelineEvent],
|
|
409
|
+
) -> tuple[list[BeliefSnapshot], list[dict[str, Any]]]:
|
|
410
|
+
"""For each event, capture the swarm's standing belief state.
|
|
411
|
+
Also detect *pivot moments* — instants where the belief headline
|
|
412
|
+
changed materially."""
|
|
413
|
+
snapshots: list[BeliefSnapshot] = []
|
|
414
|
+
pivots: list[dict[str, Any]] = []
|
|
415
|
+
sealed: list[str] = []
|
|
416
|
+
disputed: list[str] = []
|
|
417
|
+
revoked: list[str] = []
|
|
418
|
+
last_headline = ""
|
|
419
|
+
|
|
420
|
+
for ev in timeline:
|
|
421
|
+
sealed.append(ev.event_id)
|
|
422
|
+
# Disputes opened against this event flag it as disputed.
|
|
423
|
+
for _d in ev.disputes:
|
|
424
|
+
if ev.event_id not in disputed:
|
|
425
|
+
disputed.append(ev.event_id)
|
|
426
|
+
# If this event walks back an earlier one, retroactively revoke
|
|
427
|
+
# the source on the prior event (its source_revoked_at_ms field
|
|
428
|
+
# carries the offset). For the snapshot, anything whose source
|
|
429
|
+
# was revoked at-or-before this ts_offset is in the revoked list.
|
|
430
|
+
revoked_now = [
|
|
431
|
+
t.event_id
|
|
432
|
+
for t in timeline
|
|
433
|
+
if t.source_revoked_at_ms is not None and t.source_revoked_at_ms <= ev.ts_offset_ms
|
|
434
|
+
]
|
|
435
|
+
revoked = revoked_now
|
|
436
|
+
|
|
437
|
+
# Standing belief headline = severity + title of the latest
|
|
438
|
+
# non-revoked alert/critical event.
|
|
439
|
+
headline_event = next(
|
|
440
|
+
(
|
|
441
|
+
e
|
|
442
|
+
for e in reversed(timeline[: timeline.index(ev) + 1])
|
|
443
|
+
if e.severity in {"alert", "critical"} and e.event_id not in revoked
|
|
444
|
+
),
|
|
445
|
+
None,
|
|
446
|
+
)
|
|
447
|
+
headline = (
|
|
448
|
+
f"{headline_event.severity.upper()}: {headline_event.title}"
|
|
449
|
+
if headline_event
|
|
450
|
+
else f"Standing: {ev.title}"
|
|
451
|
+
)
|
|
452
|
+
confidence = 1.0 - 0.15 * len(disputed) / max(len(sealed), 1)
|
|
453
|
+
|
|
454
|
+
snap = BeliefSnapshot(
|
|
455
|
+
ts_offset_ms=ev.ts_offset_ms,
|
|
456
|
+
hlc_logical=ev.hlc_logical,
|
|
457
|
+
hlc_counter=ev.hlc_counter,
|
|
458
|
+
sealed_claim_ids=list(sealed),
|
|
459
|
+
disputed_claim_ids=list(disputed),
|
|
460
|
+
revoked_source_ids=list(revoked),
|
|
461
|
+
confidence=round(max(0.0, min(1.0, confidence)), 3),
|
|
462
|
+
headline=headline,
|
|
463
|
+
)
|
|
464
|
+
snapshots.append(snap)
|
|
465
|
+
if headline != last_headline and last_headline != "":
|
|
466
|
+
pivots.append(
|
|
467
|
+
{
|
|
468
|
+
"ts_offset_ms": ev.ts_offset_ms,
|
|
469
|
+
"from_headline": last_headline,
|
|
470
|
+
"to_headline": headline,
|
|
471
|
+
"trigger_event_id": ev.event_id,
|
|
472
|
+
}
|
|
473
|
+
)
|
|
474
|
+
last_headline = headline
|
|
475
|
+
|
|
476
|
+
return snapshots, pivots
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
# --- Final findings synthesis ---------------------------------------------
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _final_findings(
|
|
483
|
+
timeline: list[TimelineEvent],
|
|
484
|
+
pivots: list[dict[str, Any]],
|
|
485
|
+
) -> list[str]:
|
|
486
|
+
findings: list[str] = []
|
|
487
|
+
n_critical = sum(1 for e in timeline if e.severity == "critical")
|
|
488
|
+
n_disputed = sum(1 for e in timeline if e.disputes)
|
|
489
|
+
n_revoked = sum(1 for e in timeline if e.source_revoked_at_ms is not None)
|
|
490
|
+
n_pivots = len(pivots)
|
|
491
|
+
if n_critical:
|
|
492
|
+
findings.append(
|
|
493
|
+
f"{n_critical} critical event{'s' if n_critical != 1 else ''} during the incident; "
|
|
494
|
+
f"all reached witness cosign quorum.",
|
|
495
|
+
)
|
|
496
|
+
if n_disputed:
|
|
497
|
+
findings.append(
|
|
498
|
+
f"{n_disputed} claim{'s' if n_disputed != 1 else ''} were retrospectively disputed; "
|
|
499
|
+
f"these were downweighted in the standing belief.",
|
|
500
|
+
)
|
|
501
|
+
if n_revoked:
|
|
502
|
+
findings.append(
|
|
503
|
+
f"{n_revoked} source{'s' if n_revoked != 1 else ''} retroactively revoked once "
|
|
504
|
+
f"corrective evidence arrived. Prior claims from those sources are flagged in the slider.",
|
|
505
|
+
)
|
|
506
|
+
if n_pivots:
|
|
507
|
+
findings.append(
|
|
508
|
+
f"{n_pivots} belief-pivot moment{'s' if n_pivots != 1 else ''} detected — drag the "
|
|
509
|
+
f"slider to any pivot to see what the swarm thought before vs. after.",
|
|
510
|
+
)
|
|
511
|
+
if not findings:
|
|
512
|
+
findings.append("Incident timeline replayed without dispute or revocation activity.")
|
|
513
|
+
return findings
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
# --- Run function ----------------------------------------------------------
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
async def _run_aar(args: Any, recorder: Any | None = None) -> None:
|
|
520
|
+
payload = getattr(args, "mode_payload", None) or {}
|
|
521
|
+
label = (payload.get("incident_label") or "").strip() or "Incident replay"
|
|
522
|
+
summary = payload.get("incident_summary") or ""
|
|
523
|
+
raw_events = payload.get("events") or []
|
|
524
|
+
if isinstance(raw_events, str):
|
|
525
|
+
raw_events = [raw_events]
|
|
526
|
+
|
|
527
|
+
parsed = _parse_events(list(raw_events))
|
|
528
|
+
if not parsed:
|
|
529
|
+
parsed = _seed_events_from_label(label)
|
|
530
|
+
|
|
531
|
+
started_at = time.time()
|
|
532
|
+
incident_salt = hashlib.sha256(label.encode("utf-8")).hexdigest()[:16]
|
|
533
|
+
|
|
534
|
+
# Build timeline events with HLC stamps + witness sigs + disputes.
|
|
535
|
+
timeline: list[TimelineEvent] = []
|
|
536
|
+
starting_hlc_logical = int(started_at * 1000)
|
|
537
|
+
for i, ev in enumerate(parsed):
|
|
538
|
+
revoked_at = _revoke_at(i, ev, parsed, incident_salt)
|
|
539
|
+
timeline.append(
|
|
540
|
+
TimelineEvent(
|
|
541
|
+
event_id=f"E-{i + 1:03d}",
|
|
542
|
+
ts_offset_ms=ev["ts_offset_ms"],
|
|
543
|
+
hlc_logical=starting_hlc_logical + ev["ts_offset_ms"],
|
|
544
|
+
hlc_counter=i,
|
|
545
|
+
actor_label=ev["actor"],
|
|
546
|
+
actor_kid=_kid(f"actor::{ev['actor']}", incident_salt),
|
|
547
|
+
severity=ev["severity"],
|
|
548
|
+
title=ev["title"],
|
|
549
|
+
detail=(
|
|
550
|
+
f"At T+{ev['ts_offset_ms'] // 60_000}m the {ev['actor']} "
|
|
551
|
+
f"recorded: {ev['title']}."
|
|
552
|
+
),
|
|
553
|
+
witness_sigs=_witness_sigs_for(ev, incident_salt),
|
|
554
|
+
disputes=_disputes_for(i, ev, parsed, incident_salt),
|
|
555
|
+
source_revoked_at_ms=revoked_at,
|
|
556
|
+
)
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
snapshots, pivots = _build_snapshots(timeline)
|
|
560
|
+
findings = _final_findings(timeline, pivots)
|
|
561
|
+
|
|
562
|
+
report = AARReport(
|
|
563
|
+
incident_label=label,
|
|
564
|
+
incident_summary=summary,
|
|
565
|
+
starting_hlc_logical=starting_hlc_logical,
|
|
566
|
+
timeline=timeline,
|
|
567
|
+
belief_snapshots=snapshots,
|
|
568
|
+
pivot_moments=pivots,
|
|
569
|
+
final_findings=findings,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
elapsed = time.time() - started_at
|
|
573
|
+
trace = {
|
|
574
|
+
"mode": "post-incident-aar",
|
|
575
|
+
"started_at": started_at,
|
|
576
|
+
"elapsed_s": round(elapsed, 3),
|
|
577
|
+
"n_agents": len(WITNESSES) + 1,
|
|
578
|
+
"n_questions": 1,
|
|
579
|
+
"incident_label": label,
|
|
580
|
+
"report": asdict(report),
|
|
581
|
+
"collective_iq": {
|
|
582
|
+
"score": round(
|
|
583
|
+
snapshots[-1].confidence if snapshots else 0.5,
|
|
584
|
+
4,
|
|
585
|
+
),
|
|
586
|
+
},
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
Path(args.trace).write_text(json.dumps(trace, indent=2, default=str))
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
# --- Registration ----------------------------------------------------------
|
|
593
|
+
|
|
594
|
+
SPEC = ModeSpec(
|
|
595
|
+
slug="post-incident-aar",
|
|
596
|
+
label="Post-Incident After-Action Review",
|
|
597
|
+
description=(
|
|
598
|
+
"Replay an incident timeline through the swarm: HLC-stamped "
|
|
599
|
+
"claims, witness cosignatures, retrospective disputes and "
|
|
600
|
+
"source revocations. Result is a time-slider — drag back to "
|
|
601
|
+
"any moment to see what the swarm believed at that instant."
|
|
602
|
+
),
|
|
603
|
+
input_schema=INPUT_SCHEMA,
|
|
604
|
+
result_schema=RESULT_SCHEMA,
|
|
605
|
+
run_fn=_run_aar,
|
|
606
|
+
uses_legacy_fields=False,
|
|
607
|
+
expected_roles=["scout", "sceptic", "witness", "mediator"],
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
register(SPEC)
|