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,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)
|