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,797 @@
1
+ """Crisis Tabletop mode — auto-swarm composition + step-by-step branching.
2
+
3
+ The user submits a *scenario seed* — a one-line description of an
4
+ unfolding crisis (cyber breach, supplier collapse, regulatory action,
5
+ market shock, public-safety incident). The mode:
6
+
7
+ 1. **Auto-composes a swarm** appropriate to the scenario type. Each
8
+ scenario family pulls a different set of role bundles — a cyber
9
+ breach pulls CISO/IR/Legal/Comms/CustomerSuccess; a supplier
10
+ collapse pulls COO/Procurement/Finance/Legal/Engineering. This is
11
+ the only mode that visibly exercises ``mind/roles.py``-style role
12
+ composition at runtime.
13
+
14
+ 2. **Runs a multi-step deliberation.** At each step, the swarm
15
+ enumerates the top-N plausible *next events* with probabilities,
16
+ leading indicators, and a per-step asset-impact register
17
+ (data, customer trust, regulatory standing, vendor relationships,
18
+ operational throughput, brand). Some steps are *decision points*
19
+ where a human leader must choose; sub-branches descend from
20
+ each choice.
21
+
22
+ 3. **Surfaces a recommended playbook** — the canonical sequence of
23
+ actions if you follow the most-likely branch.
24
+
25
+ This is a *defensive deliberation* tool. The asset register is
26
+ intentionally generic (asset *classes*, not real-world named
27
+ targets) so the mode never enumerates external attack surface.
28
+
29
+ Spec hooks:
30
+ * §0.3.3 InfoGainRouter — picks the highest-leverage next question
31
+ * §19 Autonomy Contract — each branch is a deliberation goal
32
+ * §20 Swarm IQ — auto-swarm composition templated from role bundles
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import hashlib
38
+ import json
39
+ import time
40
+ from dataclasses import asdict, dataclass
41
+ from pathlib import Path
42
+ from typing import Any
43
+
44
+ from . import ModeSpec, register
45
+
46
+ # --- Scenario taxonomy ----------------------------------------------------
47
+
48
+ # Each scenario family has:
49
+ # - a label
50
+ # - the swarm composition (ordered list of role bundles)
51
+ # - the asset classes to track (generic; never real-world named targets)
52
+ # - a small library of canonical next-event templates per step
53
+ SCENARIOS: dict[str, dict[str, Any]] = {
54
+ "cyber-breach": {
55
+ "label": "Cyber breach / data exposure",
56
+ "swarm": [
57
+ {"slug": "ciso", "label": "CISO"},
58
+ {"slug": "ir-lead", "label": "Incident Response Lead"},
59
+ {"slug": "legal", "label": "Legal Counsel"},
60
+ {"slug": "comms", "label": "Communications"},
61
+ {"slug": "customer-success", "label": "Customer Success"},
62
+ {"slug": "engineering", "label": "Engineering Lead"},
63
+ ],
64
+ "asset_classes": [
65
+ "customer-pii",
66
+ "internal-credentials",
67
+ "service-uptime",
68
+ "regulatory-standing",
69
+ "customer-trust",
70
+ "brand",
71
+ ],
72
+ "step_templates": [
73
+ "containment",
74
+ "scope-discovery",
75
+ "notification",
76
+ "remediation",
77
+ "post-mortem",
78
+ ],
79
+ },
80
+ "supply-chain": {
81
+ "label": "Supplier collapse / supply-chain break",
82
+ "swarm": [
83
+ {"slug": "coo", "label": "COO"},
84
+ {"slug": "procurement", "label": "Procurement Lead"},
85
+ {"slug": "finance", "label": "Finance"},
86
+ {"slug": "legal", "label": "Legal Counsel"},
87
+ {"slug": "engineering", "label": "Engineering"},
88
+ {"slug": "customer-success", "label": "Customer Success"},
89
+ ],
90
+ "asset_classes": [
91
+ "operational-throughput",
92
+ "vendor-redundancy",
93
+ "cash-runway",
94
+ "contractual-obligations",
95
+ "customer-deliveries",
96
+ "brand",
97
+ ],
98
+ "step_templates": [
99
+ "exposure-assessment",
100
+ "alternative-sourcing",
101
+ "customer-communication",
102
+ "contract-review",
103
+ "stabilisation",
104
+ ],
105
+ },
106
+ "regulatory": {
107
+ "label": "Regulatory action / compliance crisis",
108
+ "swarm": [
109
+ {"slug": "ceo", "label": "CEO"},
110
+ {"slug": "legal", "label": "Legal Counsel"},
111
+ {"slug": "compliance", "label": "Compliance Officer"},
112
+ {"slug": "comms", "label": "Communications"},
113
+ {"slug": "product", "label": "Head of Product"},
114
+ {"slug": "finance", "label": "Finance"},
115
+ ],
116
+ "asset_classes": [
117
+ "regulatory-standing",
118
+ "market-access",
119
+ "operating-license",
120
+ "customer-trust",
121
+ "competitive-position",
122
+ "brand",
123
+ ],
124
+ "step_templates": [
125
+ "engagement-with-regulator",
126
+ "scope-of-action",
127
+ "remediation-plan",
128
+ "communication",
129
+ "monitoring",
130
+ ],
131
+ },
132
+ "market-shock": {
133
+ "label": "Market shock / strategic stress",
134
+ "swarm": [
135
+ {"slug": "cfo", "label": "CFO"},
136
+ {"slug": "strategy", "label": "Head of Strategy"},
137
+ {"slug": "ir", "label": "Investor Relations"},
138
+ {"slug": "operations", "label": "COO"},
139
+ {"slug": "product", "label": "Head of Product"},
140
+ {"slug": "people", "label": "Head of People"},
141
+ ],
142
+ "asset_classes": [
143
+ "cash-runway",
144
+ "growth-rate",
145
+ "investor-confidence",
146
+ "competitive-position",
147
+ "talent-retention",
148
+ "brand",
149
+ ],
150
+ "step_templates": [
151
+ "exposure-mapping",
152
+ "scenario-planning",
153
+ "stakeholder-communication",
154
+ "tactical-adjustment",
155
+ "monitoring",
156
+ ],
157
+ },
158
+ "public-safety": {
159
+ "label": "Public-safety incident (defensive coordination)",
160
+ "swarm": [
161
+ {"slug": "ic", "label": "Incident Commander"},
162
+ {"slug": "operations", "label": "Operations Section Chief"},
163
+ {"slug": "logistics", "label": "Logistics"},
164
+ {"slug": "planning", "label": "Planning"},
165
+ {"slug": "comms", "label": "Public Information Officer"},
166
+ {"slug": "safety", "label": "Safety Officer"},
167
+ ],
168
+ "asset_classes": [
169
+ "life-safety",
170
+ "responder-safety",
171
+ "infrastructure",
172
+ "communications",
173
+ "public-trust",
174
+ "evidentiary-record",
175
+ ],
176
+ "step_templates": [
177
+ "size-up",
178
+ "containment",
179
+ "stabilisation",
180
+ "coordination",
181
+ "transition-to-recovery",
182
+ ],
183
+ },
184
+ }
185
+
186
+
187
+ # --- Result types ---------------------------------------------------------
188
+
189
+
190
+ @dataclass
191
+ class AssetImpact:
192
+ asset_class: str
193
+ severity: str # info|low|medium|high|critical
194
+ rationale: str
195
+
196
+
197
+ @dataclass
198
+ class LeadingIndicator:
199
+ name: str
200
+ if_observed: str # short statement of what the indicator means
201
+
202
+
203
+ @dataclass
204
+ class Branch:
205
+ branch_id: str
206
+ label: str
207
+ probability: float
208
+ rationale: str
209
+ leading_indicators: list[LeadingIndicator]
210
+ asset_impacts: list[AssetImpact]
211
+ is_decision_point: bool # if True, leader must choose
212
+ children: list[Branch] # sub-branches when this is a decision point
213
+
214
+
215
+ @dataclass
216
+ class Step:
217
+ step_id: str
218
+ step_index: int
219
+ template: str # one of the family's step_templates
220
+ title: str
221
+ summary: str
222
+ branches: list[Branch]
223
+
224
+
225
+ @dataclass
226
+ class SwarmMember:
227
+ slug: str
228
+ label: str
229
+ kid: str
230
+ framework: str # the lens this role brings
231
+
232
+
233
+ @dataclass
234
+ class CrisisTabletopReport:
235
+ scenario_family: str
236
+ scenario_label: str
237
+ scenario_seed: str
238
+ swarm: list[SwarmMember]
239
+ steps: list[Step]
240
+ recommended_playbook: list[dict[str, Any]] # canonical actions
241
+ headline_assets_at_risk: list[AssetImpact]
242
+ n_decision_points: int
243
+
244
+
245
+ # --- Input schema ---------------------------------------------------------
246
+
247
+ INPUT_SCHEMA: dict[str, Any] = {
248
+ "type": "object",
249
+ "title": "Crisis Tabletop",
250
+ "required": ["scenario_seed"],
251
+ "properties": {
252
+ "scenario_seed": {
253
+ "type": "string",
254
+ "title": "Scenario seed (one paragraph)",
255
+ "format": "textarea",
256
+ "description": (
257
+ "Describe the unfolding crisis. e.g. 'Suspected ransomware "
258
+ "deployment in primary EU region; ~30% of customer "
259
+ "instances unreachable.' The mode picks an appropriate "
260
+ "swarm and step taxonomy from the description."
261
+ ),
262
+ },
263
+ "scenario_family": {
264
+ "type": "string",
265
+ "title": "Scenario family (auto-detected if blank)",
266
+ "enum": [
267
+ "auto",
268
+ "cyber-breach",
269
+ "supply-chain",
270
+ "regulatory",
271
+ "market-shock",
272
+ "public-safety",
273
+ ],
274
+ "default": "auto",
275
+ "description": (
276
+ "Override the auto-detected family. 'auto' inspects the seed text for keywords."
277
+ ),
278
+ },
279
+ "n_steps": {
280
+ "type": "integer",
281
+ "title": "Steps to simulate",
282
+ "minimum": 2,
283
+ "maximum": 8,
284
+ "default": 5,
285
+ },
286
+ "branches_per_step": {
287
+ "type": "integer",
288
+ "title": "Branches per step",
289
+ "minimum": 2,
290
+ "maximum": 4,
291
+ "default": 3,
292
+ },
293
+ },
294
+ }
295
+
296
+
297
+ RESULT_SCHEMA: dict[str, Any] = {
298
+ "type": "object",
299
+ "properties": {
300
+ "mode": {"type": "string", "const": "crisis-tabletop"},
301
+ "scenario_family": {"type": "string"},
302
+ "swarm": {"type": "array"},
303
+ "steps": {"type": "array"},
304
+ "recommended_playbook": {"type": "array"},
305
+ },
306
+ }
307
+
308
+
309
+ # --- Auto-detection -------------------------------------------------------
310
+
311
+
312
+ def _detect_family(seed: str, override: str) -> str:
313
+ if override and override != "auto":
314
+ return override if override in SCENARIOS else "cyber-breach"
315
+ s = seed.lower()
316
+ if any(
317
+ k in s
318
+ for k in ["ransomware", "breach", "exfil", "credential", "auth", "intrusion", "cyber"]
319
+ ):
320
+ return "cyber-breach"
321
+ if any(
322
+ k in s
323
+ for k in ["supplier", "vendor", "supply chain", "shortage", "bankruptcy", "chapter 11"]
324
+ ):
325
+ return "supply-chain"
326
+ if any(
327
+ k in s
328
+ for k in ["regulator", "fine", "investigation", "compliance", "subpoena", "enforcement"]
329
+ ):
330
+ return "regulatory"
331
+ if any(
332
+ k in s
333
+ for k in [
334
+ "market",
335
+ "stock",
336
+ "earnings",
337
+ "currency",
338
+ "rate",
339
+ "fed",
340
+ "inflation",
341
+ "valuation",
342
+ ]
343
+ ):
344
+ return "market-shock"
345
+ if any(k in s for k in ["safety", "evacuation", "responder", "fire", "flood", "ems"]):
346
+ return "public-safety"
347
+ # Default — most generic
348
+ return "cyber-breach"
349
+
350
+
351
+ # --- Synthesis helpers ----------------------------------------------------
352
+
353
+
354
+ def _kid(prefix: str, salt: str) -> str:
355
+ return hashlib.sha256(f"{prefix}::{salt}".encode()).hexdigest()[:32]
356
+
357
+
358
+ def _hashf(*parts: str) -> int:
359
+ return int(hashlib.sha256("::".join(parts).encode("utf-8")).hexdigest(), 16)
360
+
361
+
362
+ def _build_swarm(family: dict[str, Any], salt: str) -> list[SwarmMember]:
363
+ return [
364
+ SwarmMember(
365
+ slug=role["slug"],
366
+ label=role["label"],
367
+ kid=_kid(f"role::{role['slug']}", salt),
368
+ framework="incident-management"
369
+ if role["slug"] in {"ir-lead", "ic", "operations", "ciso", "safety"}
370
+ else "governance"
371
+ if role["slug"] in {"ceo", "cfo", "coo", "compliance", "legal", "ir"}
372
+ else "operations",
373
+ )
374
+ for role in family["swarm"]
375
+ ]
376
+
377
+
378
+ # Branch label templates per (family, step_template). Kept small and
379
+ # legible — the demo audience reads these verbatim.
380
+ BRANCH_LIBRARY: dict[str, list[str]] = {
381
+ "containment": [
382
+ "Isolate affected segments; pause inbound traffic from compromised path.",
383
+ "Maintain operations behind tighter monitoring; gather more telemetry.",
384
+ "Full lockdown — disable broader surface area until scope is known.",
385
+ "Selective failover to known-clean secondary environment.",
386
+ ],
387
+ "scope-discovery": [
388
+ "Forensic triage on affected hosts; chain-of-custody preserved.",
389
+ "Customer-facing scope narrows after telemetry review.",
390
+ "Scope expands beyond initial estimate; activate broader response.",
391
+ ],
392
+ "notification": [
393
+ "Notify regulators within statutory window; queue customer comms.",
394
+ "Hold customer comms until scope is firm; pre-brief key accounts.",
395
+ "Public statement first; regulator notification per legal calendar.",
396
+ ],
397
+ "remediation": [
398
+ "Patch + rotate credentials + invalidate sessions across blast radius.",
399
+ "Targeted remediation on confirmed compromised assets only.",
400
+ "Full rebuild of affected tier from known-clean baseline.",
401
+ ],
402
+ "post-mortem": [
403
+ "Cross-functional AAR with publishable findings.",
404
+ "Internal-only post-mortem; legal-privileged document.",
405
+ ],
406
+ "exposure-assessment": [
407
+ "Map dependencies on the failed supplier; identify single-source items.",
408
+ "Escalate exposure if any single-source item is on critical path.",
409
+ ],
410
+ "alternative-sourcing": [
411
+ "Activate pre-qualified secondary suppliers.",
412
+ "Spot-buy from open market at premium pricing.",
413
+ "Internalise the affected component temporarily.",
414
+ ],
415
+ "customer-communication": [
416
+ "Proactive disclosure to top-N enterprise customers.",
417
+ "Hold communication until impact is concrete; prepare playbook.",
418
+ "Public statement; coordinate with sales for hand-on retention.",
419
+ ],
420
+ "contract-review": [
421
+ "Review force-majeure clauses; preserve litigation optionality.",
422
+ "Negotiate amendments preserving long-term relationship.",
423
+ ],
424
+ "stabilisation": [
425
+ "Lock in alternative-source contracts; resume normal operations.",
426
+ "Maintain elevated monitoring posture for 90 days.",
427
+ ],
428
+ "engagement-with-regulator": [
429
+ "Cooperative posture: voluntary disclosure + remediation plan.",
430
+ "Defensive posture: respond only to specific requests.",
431
+ ],
432
+ "scope-of-action": [
433
+ "Narrow scope: address the specific complaint.",
434
+ "Broaden scope: address class of issues to demonstrate good faith.",
435
+ ],
436
+ "remediation-plan": [
437
+ "Aggressive timeline; signals seriousness to regulator.",
438
+ "Realistic timeline; protects against further enforcement on missed milestones.",
439
+ ],
440
+ "communication": [
441
+ "Coordinated statement: regulator + investors + customers same day.",
442
+ "Staggered comms: internal first, then regulator, then public.",
443
+ ],
444
+ "monitoring": [
445
+ "Public dashboard of remediation milestones; visible to regulator.",
446
+ "Internal tracking only; report at regulator-mandated cadence.",
447
+ ],
448
+ "exposure-mapping": [
449
+ "Direct exposure: revenue + cost lines hit by the shock.",
450
+ "Indirect exposure: partner / customer / vendor second-order effects.",
451
+ ],
452
+ "scenario-planning": [
453
+ "Bull case persists 1–2 quarters; protect growth investments.",
454
+ "Stress case persists 4+ quarters; defend cash runway aggressively.",
455
+ ],
456
+ "stakeholder-communication": [
457
+ "Investor preview before earnings; pre-empt the surprise.",
458
+ "Hold until scheduled disclosure window.",
459
+ ],
460
+ "tactical-adjustment": [
461
+ "Pause hiring + non-essential spend; reforecast quarterly.",
462
+ "Maintain plan; absorb shock from balance sheet.",
463
+ ],
464
+ "size-up": [
465
+ "Establish unified command; declare incident class.",
466
+ "Secure perimeter; protect responder safety as priority one.",
467
+ ],
468
+ "coordination": [
469
+ "Multi-agency unified command structure activated.",
470
+ "Single-agency lead with mutual-aid as needed.",
471
+ ],
472
+ "transition-to-recovery": [
473
+ "Stand down responders; transition to recovery agency.",
474
+ "Maintain elevated posture during recovery operations.",
475
+ ],
476
+ }
477
+
478
+
479
+ INDICATORS_LIBRARY: dict[str, list[tuple[str, str]]] = {
480
+ "cyber-breach": [
481
+ ("Outbound C2 beacon volume", "If rises, escalate to active intrusion."),
482
+ ("Authentication failure rate", "If anomalous, credential compromise likely."),
483
+ ("Telemetry coverage gaps", "If observed, attacker may be evading detection."),
484
+ ("Customer support ticket volume", "If spikes, customer-facing impact."),
485
+ ],
486
+ "supply-chain": [
487
+ ("Inbound shipment cadence", "If slows below threshold, escalate exposure."),
488
+ ("Alternative-source pricing", "If premium >2x, plan for margin compression."),
489
+ ("Customer escalation count", "If rises, communication gap widening."),
490
+ ],
491
+ "regulatory": [
492
+ ("Regulator communication tone", "If shifts, anticipate enforcement step."),
493
+ ("Press coverage volume", "If rises, public-comms must outpace narrative."),
494
+ ("Internal compliance metrics", "If trend negative, remediation insufficient."),
495
+ ],
496
+ "market-shock": [
497
+ ("Lead-volume / pipeline", "If drops >15%, demand-side impact confirmed."),
498
+ ("Cash burn rate", "If exceeds plan, runway re-forecast required."),
499
+ ("Talent attrition rate", "If rises, retention plan needed."),
500
+ ],
501
+ "public-safety": [
502
+ ("Affected population estimate", "If grows, escalate ICS level."),
503
+ ("Resource utilisation", "If exceeds 80%, request mutual aid."),
504
+ ("Communication channel saturation", "If saturated, deploy redundant channels."),
505
+ ],
506
+ }
507
+
508
+
509
+ def _choose_branches(
510
+ family_key: str,
511
+ step_template: str,
512
+ n: int,
513
+ salt: str,
514
+ ) -> list[str]:
515
+ pool = BRANCH_LIBRARY.get(
516
+ step_template,
517
+ [
518
+ "Continue current posture; monitor for new signals.",
519
+ "Escalate response; activate next-tier playbook.",
520
+ "De-escalate; situation has stabilised.",
521
+ ],
522
+ )
523
+ h = _hashf(family_key, step_template, salt)
524
+ out: list[str] = []
525
+ for i in range(min(n, len(pool))):
526
+ idx = (h + i * 7) % len(pool)
527
+ cand = pool[idx]
528
+ if cand not in out:
529
+ out.append(cand)
530
+ while len(out) < n:
531
+ out.append(pool[len(out) % len(pool)])
532
+ return out[:n]
533
+
534
+
535
+ def _branch_probabilities(n: int, salt: str) -> list[float]:
536
+ """Generate n probabilities that sum to 1, with one dominant branch."""
537
+ h = _hashf(salt)
538
+ weights = [1.0 + (((h >> (i * 5)) % 50) / 100.0) for i in range(n)]
539
+ # Bias the first branch up so there's a visible "most likely" path.
540
+ weights[0] += 0.6
541
+ total = sum(weights)
542
+ return [round(w / total, 3) for w in weights]
543
+
544
+
545
+ def _asset_impact_for_branch(
546
+ family: dict[str, Any],
547
+ branch_label: str,
548
+ salt: str,
549
+ ) -> list[AssetImpact]:
550
+ text = branch_label.lower()
551
+ out: list[AssetImpact] = []
552
+ for asset in family["asset_classes"]:
553
+ # Each asset gets a synthetic severity. A few keyword heuristics
554
+ # so the demo reads naturally.
555
+ sev = "low"
556
+ rationale = f"{asset} is monitored under this branch."
557
+ h = _hashf(asset, branch_label, salt)
558
+ if "lockdown" in text or "full" in text or "rebuild" in text:
559
+ sev = "high"
560
+ rationale = f"{asset} pressured by aggressive containment."
561
+ elif "isolate" in text or "patch" in text or "narrow" in text:
562
+ sev = "medium"
563
+ rationale = f"{asset} partially affected; mitigation in progress."
564
+ elif "monitor" in text or "maintain" in text or "hold" in text:
565
+ sev = "low"
566
+ rationale = f"{asset} stable under continued monitoring."
567
+ elif "notify" in text or "public" in text or "regulator" in text:
568
+ if asset in {"regulatory-standing", "customer-trust", "brand"}:
569
+ sev = "medium"
570
+ rationale = f"{asset} exposed to communication outcome."
571
+ elif "alternative" in text or "spot-buy" in text:
572
+ if asset in {"cash-runway", "operational-throughput"}:
573
+ sev = "medium"
574
+ rationale = f"{asset} short-term hit; long-term resilient."
575
+ # 30% of slots get bumped one severity tier, deterministically
576
+ if (h % 100) < 25:
577
+ sev_order = ["info", "low", "medium", "high", "critical"]
578
+ idx = min(len(sev_order) - 1, sev_order.index(sev) + 1)
579
+ sev = sev_order[idx]
580
+ out.append(
581
+ AssetImpact(
582
+ asset_class=asset,
583
+ severity=sev,
584
+ rationale=rationale,
585
+ )
586
+ )
587
+ return out
588
+
589
+
590
+ def _decision_point_for(template: str) -> bool:
591
+ return template in {
592
+ "containment",
593
+ "notification",
594
+ "remediation",
595
+ "alternative-sourcing",
596
+ "engagement-with-regulator",
597
+ "scenario-planning",
598
+ "size-up",
599
+ "coordination",
600
+ }
601
+
602
+
603
+ # --- Run function ---------------------------------------------------------
604
+
605
+
606
+ async def _run_tabletop(args: Any, recorder: Any | None = None) -> None:
607
+ payload = getattr(args, "mode_payload", None) or {}
608
+ seed = (payload.get("scenario_seed") or "").strip()
609
+ if not seed:
610
+ seed = (
611
+ "Suspected ransomware deployment in primary EU region; "
612
+ "~30% of customer instances unreachable."
613
+ )
614
+ family_override = payload.get("scenario_family") or "auto"
615
+ n_steps = int(payload.get("n_steps") or 5)
616
+ n_branches = int(payload.get("branches_per_step") or 3)
617
+ n_steps = max(2, min(8, n_steps))
618
+ n_branches = max(2, min(4, n_branches))
619
+
620
+ family_key = _detect_family(seed, family_override)
621
+ family = SCENARIOS[family_key]
622
+ salt = hashlib.sha256(seed.encode("utf-8")).hexdigest()[:16]
623
+
624
+ started_at = time.time()
625
+
626
+ swarm = _build_swarm(family, salt)
627
+ indicator_pool = INDICATORS_LIBRARY.get(family_key, [])
628
+
629
+ steps: list[Step] = []
630
+ headline_assets: list[AssetImpact] = []
631
+ n_decision_points = 0
632
+ templates = list(family["step_templates"])
633
+ # Cycle through templates if user asks for more steps than canonical.
634
+ templates_for_run = [templates[i % len(templates)] for i in range(n_steps)]
635
+
636
+ for i, tmpl in enumerate(templates_for_run):
637
+ step_id = f"S-{i + 1:02d}"
638
+ is_dp = _decision_point_for(tmpl)
639
+ if is_dp:
640
+ n_decision_points += 1
641
+ labels = _choose_branches(family_key, tmpl, n_branches, salt + step_id)
642
+ probs = _branch_probabilities(len(labels), salt + step_id)
643
+ branches: list[Branch] = []
644
+ for j, label in enumerate(labels):
645
+ branch_id = f"{step_id}-B{j + 1}"
646
+ asset_impacts = _asset_impact_for_branch(family, label, salt + branch_id)
647
+ # Capture the highest-severity impact for the headline list
648
+ for ai in asset_impacts:
649
+ if ai.severity in {"high", "critical"}:
650
+ headline_assets.append(ai)
651
+ # Pick 2 leading indicators for this branch (rotate through pool)
652
+ inds: list[LeadingIndicator] = []
653
+ for k in range(min(2, len(indicator_pool))):
654
+ idx = (_hashf(branch_id, str(k)) + k) % len(indicator_pool)
655
+ name, what = indicator_pool[idx]
656
+ inds.append(LeadingIndicator(name=name, if_observed=what))
657
+ # For decision-point branches, optionally emit 2 sub-children
658
+ children: list[Branch] = []
659
+ if (
660
+ is_dp and j == 0
661
+ ): # Only the most-likely option gets sub-branching to keep the tree legible
662
+ for k in range(2):
663
+ sub_label = (
664
+ "Sub-decision: aggressive posture"
665
+ if k == 0
666
+ else "Sub-decision: conservative posture"
667
+ )
668
+ children.append(
669
+ Branch(
670
+ branch_id=f"{branch_id}-C{k + 1}",
671
+ label=sub_label,
672
+ probability=round(0.6 if k == 0 else 0.4, 3),
673
+ rationale=(
674
+ "Sub-branch under the most-likely choice; "
675
+ "leader's call between aggressive and conservative posture."
676
+ ),
677
+ leading_indicators=[],
678
+ asset_impacts=[],
679
+ is_decision_point=False,
680
+ children=[],
681
+ )
682
+ )
683
+ branches.append(
684
+ Branch(
685
+ branch_id=branch_id,
686
+ label=label,
687
+ probability=probs[j],
688
+ rationale=(
689
+ f"Branch {branch_id}: {tmpl} — under this option, "
690
+ f"the swarm posterior weights this path at "
691
+ f"{probs[j] * 100:.0f}% based on the seed and the "
692
+ f"{family_key} response pattern."
693
+ ),
694
+ leading_indicators=inds,
695
+ asset_impacts=asset_impacts,
696
+ is_decision_point=is_dp and j == 0,
697
+ children=children,
698
+ )
699
+ )
700
+ # Sort by probability descending
701
+ branches.sort(key=lambda b: -b.probability)
702
+ steps.append(
703
+ Step(
704
+ step_id=step_id,
705
+ step_index=i,
706
+ template=tmpl,
707
+ title=tmpl.replace("-", " ").title(),
708
+ summary=(
709
+ f"Step {i + 1} ({tmpl}): the swarm enumerates "
710
+ f"{len(branches)} plausible paths; "
711
+ f"{'a leader decision is required' if is_dp else 'continuation is straightforward'}."
712
+ ),
713
+ branches=branches,
714
+ )
715
+ )
716
+
717
+ # Recommended playbook = most-likely branch from each step
718
+ playbook: list[dict[str, Any]] = []
719
+ for s in steps:
720
+ top = s.branches[0]
721
+ playbook.append(
722
+ {
723
+ "step_id": s.step_id,
724
+ "step_title": s.title,
725
+ "action": top.label,
726
+ "probability": top.probability,
727
+ "is_decision_point": top.is_decision_point,
728
+ }
729
+ )
730
+
731
+ # Deduplicate headline assets (keep worst severity per asset_class)
732
+ sev_rank = {"info": 0, "low": 1, "medium": 2, "high": 3, "critical": 4}
733
+ by_class: dict[str, AssetImpact] = {}
734
+ for ai in headline_assets:
735
+ if (
736
+ ai.asset_class not in by_class
737
+ or sev_rank[ai.severity] > sev_rank[by_class[ai.asset_class].severity]
738
+ ):
739
+ by_class[ai.asset_class] = ai
740
+ headline_assets_dedup = list(by_class.values())
741
+ headline_assets_dedup.sort(key=lambda a: -sev_rank[a.severity])
742
+
743
+ report = CrisisTabletopReport(
744
+ scenario_family=family_key,
745
+ scenario_label=family["label"],
746
+ scenario_seed=seed,
747
+ swarm=swarm,
748
+ steps=steps,
749
+ recommended_playbook=playbook,
750
+ headline_assets_at_risk=headline_assets_dedup,
751
+ n_decision_points=n_decision_points,
752
+ )
753
+
754
+ elapsed = time.time() - started_at
755
+ trace = {
756
+ "mode": "crisis-tabletop",
757
+ "started_at": started_at,
758
+ "elapsed_s": round(elapsed, 3),
759
+ "n_agents": len(swarm),
760
+ "n_questions": len(steps),
761
+ "scenario_seed": seed,
762
+ "scenario_family": family_key,
763
+ "report": asdict(report),
764
+ "collective_iq": {
765
+ # Confidence proxy: top-branch probability averaged across steps
766
+ "score": round(
767
+ sum(s.branches[0].probability for s in steps) / max(len(steps), 1),
768
+ 4,
769
+ ),
770
+ },
771
+ }
772
+
773
+ Path(args.trace).write_text(json.dumps(trace, indent=2, default=str))
774
+
775
+
776
+ # --- Registration ---------------------------------------------------------
777
+
778
+ SPEC = ModeSpec(
779
+ slug="crisis-tabletop",
780
+ label="Crisis Tabletop",
781
+ description=(
782
+ "Auto-composes a swarm based on the scenario type (cyber breach, "
783
+ "supplier collapse, regulatory action, market shock, public-safety "
784
+ "incident) and runs a step-by-step branching simulation. Each step "
785
+ "enumerates plausible next events with leading indicators, asset-"
786
+ "impact register, and decision-point flags. Output: a recommended "
787
+ "playbook + branching tree."
788
+ ),
789
+ input_schema=INPUT_SCHEMA,
790
+ result_schema=RESULT_SCHEMA,
791
+ run_fn=_run_tabletop,
792
+ uses_legacy_fields=False,
793
+ expected_roles=["scout", "sceptic", "synthesist", "mediator"],
794
+ )
795
+
796
+
797
+ register(SPEC)