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.
Files changed (181) hide show
  1. hmctl/__init__.py +7 -0
  2. hmctl/cli.py +947 -0
  3. hypermind/__init__.py +208 -0
  4. hypermind/_deprecations.py +108 -0
  5. hypermind/agent.py +2896 -0
  6. hypermind/agent_card.py +127 -0
  7. hypermind/api/__init__.py +32 -0
  8. hypermind/api/app.py +686 -0
  9. hypermind/capability.py +923 -0
  10. hypermind/consult.py +493 -0
  11. hypermind/consult_bias.py +112 -0
  12. hypermind/contract_net.py +219 -0
  13. hypermind/crypto/__init__.py +21 -0
  14. hypermind/crypto/cose_encrypt.py +126 -0
  15. hypermind/crypto/dkg.py +246 -0
  16. hypermind/crypto/domain.py +59 -0
  17. hypermind/crypto/frost.py +771 -0
  18. hypermind/crypto/hashing.py +38 -0
  19. hypermind/crypto/hlc.py +163 -0
  20. hypermind/crypto/hpke.py +277 -0
  21. hypermind/crypto/kms.py +196 -0
  22. hypermind/crypto/kms_backends/__init__.py +56 -0
  23. hypermind/crypto/kms_backends/aws.py +128 -0
  24. hypermind/crypto/kms_backends/azure.py +114 -0
  25. hypermind/crypto/kms_backends/gcp.py +97 -0
  26. hypermind/crypto/kms_backends/vault.py +120 -0
  27. hypermind/crypto/namespace_root.py +204 -0
  28. hypermind/crypto/pq.py +152 -0
  29. hypermind/crypto/room_epoch.py +97 -0
  30. hypermind/crypto/signing.py +245 -0
  31. hypermind/deliberation.py +355 -0
  32. hypermind/did_web.py +256 -0
  33. hypermind/dispute.py +858 -0
  34. hypermind/errors.py +218 -0
  35. hypermind/eval/__init__.py +49 -0
  36. hypermind/eval/calibration.py +307 -0
  37. hypermind/eval/comparison.py +251 -0
  38. hypermind/eval/replay.py +202 -0
  39. hypermind/eval/swarm_iq.py +718 -0
  40. hypermind/knowledge/__init__.py +17 -0
  41. hypermind/knowledge/citation_graph.py +141 -0
  42. hypermind/knowledge/correlated_agreement.py +167 -0
  43. hypermind/knowledge/embedding_registry.py +115 -0
  44. hypermind/knowledge/kernel_store.py +197 -0
  45. hypermind/knowledge/rekor.py +173 -0
  46. hypermind/knowledge/reputation.py +364 -0
  47. hypermind/knowledge/swarm_memory.py +523 -0
  48. hypermind/knowledge/transparency.py +409 -0
  49. hypermind/mcp/__init__.py +60 -0
  50. hypermind/mcp/bridge.py +151 -0
  51. hypermind/mcp/client.py +142 -0
  52. hypermind/mcp/provenance.py +214 -0
  53. hypermind/mcp/relata_tools.py +152 -0
  54. hypermind/mcp/server.py +248 -0
  55. hypermind/mcp/transport.py +206 -0
  56. hypermind/mind/__init__.py +150 -0
  57. hypermind/mind/active.py +234 -0
  58. hypermind/mind/belief.py +369 -0
  59. hypermind/mind/deliberator.py +625 -0
  60. hypermind/mind/diversity.py +144 -0
  61. hypermind/mind/evolve.py +184 -0
  62. hypermind/mind/federation.py +154 -0
  63. hypermind/mind/goals.py +117 -0
  64. hypermind/mind/integration.py +190 -0
  65. hypermind/mind/mediator.py +74 -0
  66. hypermind/mind/memory_store.py +129 -0
  67. hypermind/mind/policy.py +307 -0
  68. hypermind/mind/records.py +260 -0
  69. hypermind/mind/roles.py +190 -0
  70. hypermind/mind/sybil_guard.py +41 -0
  71. hypermind/mind/world_model.py +166 -0
  72. hypermind/namespace.py +515 -0
  73. hypermind/namespace_policy.py +254 -0
  74. hypermind/observability/__init__.py +25 -0
  75. hypermind/observability/asgi.py +214 -0
  76. hypermind/observability/cost.py +277 -0
  77. hypermind/observability/otel_export.py +200 -0
  78. hypermind/observability/recorder.py +344 -0
  79. hypermind/observability/swarm_trace.py +438 -0
  80. hypermind/pin_policy.py +116 -0
  81. hypermind/py.typed +0 -0
  82. hypermind/responders/__init__.py +87 -0
  83. hypermind/responders/anthropic.py +159 -0
  84. hypermind/responders/base.py +162 -0
  85. hypermind/responders/openai.py +199 -0
  86. hypermind/responders/router.py +254 -0
  87. hypermind/responders/structured.py +287 -0
  88. hypermind/responders/tools.py +444 -0
  89. hypermind/revocation.py +270 -0
  90. hypermind/rule_ids.py +135 -0
  91. hypermind/schemas/encrypted_statement.cddl +28 -0
  92. hypermind/schemas/namespace_accept.cddl +20 -0
  93. hypermind/schemas/namespace_create.cddl +25 -0
  94. hypermind/schemas/namespace_founding_attest.cddl +30 -0
  95. hypermind/schemas/namespace_invite.cddl +22 -0
  96. hypermind/schemas/wire-v0.1.cddl +73 -0
  97. hypermind/simlab/__init__.py +15 -0
  98. hypermind/simlab/_persona_registry.py +93 -0
  99. hypermind/simlab/_scenario_impl.py +2778 -0
  100. hypermind/simlab/app.py +2538 -0
  101. hypermind/simlab/backends/__init__.py +27 -0
  102. hypermind/simlab/backends/anchor.py +88 -0
  103. hypermind/simlab/backends/local.py +32 -0
  104. hypermind/simlab/backends/server.py +79 -0
  105. hypermind/simlab/cli_command.py +108 -0
  106. hypermind/simlab/config.py +106 -0
  107. hypermind/simlab/deployment.py +26 -0
  108. hypermind/simlab/knowledge.py +716 -0
  109. hypermind/simlab/learning.py +295 -0
  110. hypermind/simlab/modes/__init__.py +115 -0
  111. hypermind/simlab/modes/_eval_real_llm.py +373 -0
  112. hypermind/simlab/modes/aar.py +611 -0
  113. hypermind/simlab/modes/backtest.py +610 -0
  114. hypermind/simlab/modes/binary_forecast.py +69 -0
  115. hypermind/simlab/modes/conformance.py +1869 -0
  116. hypermind/simlab/modes/evaluation.py +2271 -0
  117. hypermind/simlab/modes/governance.py +648 -0
  118. hypermind/simlab/modes/redteam.py +534 -0
  119. hypermind/simlab/modes/tabletop.py +797 -0
  120. hypermind/simlab/modes/tournament.py +448 -0
  121. hypermind/simlab/modes/whatif.py +523 -0
  122. hypermind/simlab/namespace.py +125 -0
  123. hypermind/simlab/personas.py +477 -0
  124. hypermind/simlab/prompt_generator.py +172 -0
  125. hypermind/simlab/question_sets/geopolitics_q20.json +122 -0
  126. hypermind/simlab/question_sets/governance_q5.json +67 -0
  127. hypermind/simlab/question_sets/msft_outlook_q10.json +62 -0
  128. hypermind/simlab/question_sets/whatif_q3.json +38 -0
  129. hypermind/simlab/registry.py +325 -0
  130. hypermind/simlab/runner.py +239 -0
  131. hypermind/simlab/scenario.py +147 -0
  132. hypermind/simlab/swarms.py +180 -0
  133. hypermind/simlab/tool_handlers/__init__.py +4 -0
  134. hypermind/simlab/tool_handlers/alpha_vantage.py +39 -0
  135. hypermind/simlab/tool_handlers/brave.py +46 -0
  136. hypermind/simlab/tool_handlers/newsapi.py +50 -0
  137. hypermind/simlab/tool_handlers/openweather.py +46 -0
  138. hypermind/simlab/tool_handlers/pinecone.py +52 -0
  139. hypermind/simlab/tool_handlers/qdrant.py +57 -0
  140. hypermind/simlab/tool_handlers/query_knowledge_base.py +21 -0
  141. hypermind/simlab/tool_handlers/relata_recall.py +61 -0
  142. hypermind/simlab/tool_handlers/retrieve_document.py +21 -0
  143. hypermind/simlab/tool_handlers/serper.py +46 -0
  144. hypermind/simlab/tool_handlers/tavily.py +53 -0
  145. hypermind/simlab/tool_handlers/weaviate.py +65 -0
  146. hypermind/simlab/tool_handlers/wikipedia.py +64 -0
  147. hypermind/simlab/tool_handlers/wolfram.py +48 -0
  148. hypermind/simlab/tool_handlers/yahoo_finance.py +49 -0
  149. hypermind/simlab/tool_registry.py +568 -0
  150. hypermind/simlab/topic_adapter.py +338 -0
  151. hypermind/simlab/topic_questions.py +259 -0
  152. hypermind/storage/__init__.py +69 -0
  153. hypermind/storage/backend.py +71 -0
  154. hypermind/storage/relata.py +245 -0
  155. hypermind/storage/sqlite.py +258 -0
  156. hypermind/sync.py +191 -0
  157. hypermind/tal.py +175 -0
  158. hypermind/tasks.py +334 -0
  159. hypermind/testing.py +16 -0
  160. hypermind/tools/__init__.py +32 -0
  161. hypermind/tools/registry.py +61 -0
  162. hypermind/transport/__init__.py +22 -0
  163. hypermind/transport/anti_entropy.py +708 -0
  164. hypermind/transport/bus.py +206 -0
  165. hypermind/transport/knows_delta.py +204 -0
  166. hypermind/transport/libp2p.py +298 -0
  167. hypermind/transport/placement.py +58 -0
  168. hypermind/transport/tcp.py +797 -0
  169. hypermind/transport/tls_profile.py +417 -0
  170. hypermind/types.py +59 -0
  171. hypermind/uncertainty.py +222 -0
  172. hypermind/wire.py +897 -0
  173. hypermind/workflow/__init__.py +72 -0
  174. hypermind/workflow/engine.py +655 -0
  175. hypermind/workflow/plan.py +182 -0
  176. hypermind/workflow/repair.py +203 -0
  177. hypermind-0.11.0.dist-info/METADATA +524 -0
  178. hypermind-0.11.0.dist-info/RECORD +181 -0
  179. hypermind-0.11.0.dist-info/WHEEL +4 -0
  180. hypermind-0.11.0.dist-info/entry_points.txt +2 -0
  181. 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)