hirann commited on
Commit
0b4e72a
Β·
verified Β·
1 Parent(s): 7343d7f

Upload server/demo_ui.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. server/demo_ui.py +724 -595
server/demo_ui.py CHANGED
@@ -1,595 +1,724 @@
1
- """
2
- Hackathon-judge demo UI for the live HF Space.
3
-
4
- What this gives the judge when they click the Space link:
5
-
6
- - One-screen Gradio panel.
7
- - Pick a scenario family from the 5 elite ones (basic / RAG / executive
8
- alignment / silo-breaker / stealth-adaptive).
9
- - Click "Run episode" -> we play the heuristic policy for up to 30 steps
10
- in the env, then re-run the same scenario seed with the *trained* LLM
11
- policy (if available on the Hub) and show side-by-side results.
12
- - Outputs:
13
- - reward delta (trained vs heuristic) with a clear winner indicator
14
- - the agent's per-step action stream, with reasoning
15
- - a chart of per-step reward
16
- - a status badge for the trained adapter ("ready" / "training in progress")
17
-
18
- The point is for a non-technical reviewer to see
19
- "agent observed X -> agent did Y -> reward Z" on screen in 10 seconds.
20
-
21
- Mounted at ``/demo`` on the FastAPI app via ``gr.mount_gradio_app``.
22
- """
23
-
24
- from __future__ import annotations
25
-
26
- from collections import Counter
27
- from typing import Any
28
-
29
- import gradio as gr
30
-
31
- from immunoorg.agents.defender import (
32
- format_observation_for_llm,
33
- get_defender_prompt,
34
- )
35
- from immunoorg.environment import ImmunoOrgEnvironment
36
- from immunoorg.models import (
37
- ActionType,
38
- DiagnosticAction,
39
- ImmunoAction,
40
- PipelineGate,
41
- StrategicAction,
42
- TacticalAction,
43
- )
44
- from training.dataset_generator import DatasetConfig, DatasetGenerator
45
- from training.scenario_hooks import (
46
- apply_scenario_hooks,
47
- attach_hooks,
48
- training_step_penalty,
49
- )
50
-
51
-
52
- # ─── Scenario catalogue ─────────────────────────────────────────────────────
53
-
54
-
55
- _SCENARIO_LABEL = {
56
- "basic_containment": "1. Basic Containment (warm-up)",
57
- "rag_grounding": "2. RAG-Grounding (use CVE intel, not blunt isolate)",
58
- "executive_alignment": "3. Executive Alignment (uptime directive overrides instinct)",
59
- "silo_breaker": "4. Silo-Breaker (org friction blocks tactical actions)",
60
- "stealth_adaptive": "5. Stealth & Adaptive (multi-step investigation)",
61
- }
62
- _LABEL_TO_FAMILY = {v: k for k, v in _SCENARIO_LABEL.items()}
63
-
64
-
65
- _SCENARIO_CACHE: dict[str, dict[str, Any]] = {}
66
-
67
-
68
- def _scenario_for(family: str) -> dict[str, Any]:
69
- """Generate one balanced elite scenario per family, cached."""
70
- if family in _SCENARIO_CACHE:
71
- return _SCENARIO_CACHE[family]
72
- gen = DatasetGenerator(DatasetConfig(
73
- dataset_type="elite",
74
- output_dir="/tmp/_demo_scenarios",
75
- verbose=False,
76
- compress_output=False,
77
- ))
78
- scenarios = gen.generate_elite_scenario_mix_dataset(total=5)
79
- for sc in scenarios:
80
- if sc["family"] == family and family not in _SCENARIO_CACHE:
81
- _SCENARIO_CACHE[family] = sc
82
- return _SCENARIO_CACHE[family]
83
-
84
-
85
- # ─── Heuristic policy (mirrors scripts/generate_training_evidence.py) ──────
86
-
87
-
88
- def _heuristic_action(env, obs):
89
- phase = obs.current_phase.value
90
- nodes = obs.visible_nodes
91
- compromised = [n for n in nodes if n.compromised and not n.isolated]
92
- hooks = getattr(env, "_active_scenario_hooks", {}) or {}
93
-
94
- if hooks.get("inject_rag_best_mitigation") and phase in ("detection", "containment") and compromised:
95
- return ImmunoAction(action_type=ActionType.TACTICAL,
96
- tactical_action=TacticalAction.SNAPSHOT_FORENSICS,
97
- target=compromised[0].id,
98
- reasoning="RAG: forensic snapshot before patching the rootkit.")
99
- if hooks.get("board_uptime_no_isolate") and phase == "containment":
100
- target = compromised[0].id if compromised else (nodes[0].id if nodes else "")
101
- return ImmunoAction(action_type=ActionType.TACTICAL,
102
- tactical_action=TacticalAction.DEPLOY_PATCH,
103
- target=target,
104
- reasoning="Board directive: patch instead of isolating.")
105
- if hooks.get("force_denials_on_isolate") and phase in ("containment", "rca", "refactor"):
106
- return ImmunoAction(action_type=ActionType.STRATEGIC,
107
- strategic_action=StrategicAction.ESTABLISH_DEVSECOPS,
108
- target="dept-security", secondary_target="dept-engineering",
109
- reasoning="Approver keeps denying; restructure the org.")
110
- if hooks.get("stealthy_initial_attack") and phase == "detection":
111
- return ImmunoAction(action_type=ActionType.DIAGNOSTIC,
112
- diagnostic_action=DiagnosticAction.VULNERABILITY_SCAN,
113
- reasoning="Stealth attack: deeper scan first.")
114
-
115
- if phase == "detection":
116
- target = compromised[0].id if compromised else (nodes[0].id if nodes else "")
117
- return ImmunoAction(action_type=ActionType.TACTICAL,
118
- tactical_action=TacticalAction.SCAN_LOGS,
119
- target=target, reasoning="Detection: scan for indicators.")
120
- if phase == "containment":
121
- if compromised:
122
- return ImmunoAction(action_type=ActionType.TACTICAL,
123
- tactical_action=TacticalAction.ISOLATE_NODE,
124
- target=compromised[0].id, reasoning="Isolate compromised node.")
125
- return ImmunoAction(action_type=ActionType.DIAGNOSTIC,
126
- diagnostic_action=DiagnosticAction.TIMELINE_RECONSTRUCT,
127
- reasoning="Reconstruct timeline.")
128
- if phase == "rca":
129
- return ImmunoAction(action_type=ActionType.DIAGNOSTIC,
130
- diagnostic_action=DiagnosticAction.IDENTIFY_SILO,
131
- reasoning="Find the silo behind the failure.")
132
- if phase == "refactor":
133
- return ImmunoAction(action_type=ActionType.STRATEGIC,
134
- strategic_action=StrategicAction.REDUCE_BUREAUCRACY,
135
- target="dept-management", reasoning="Reduce approval latency.")
136
- return ImmunoAction(action_type=ActionType.DIAGNOSTIC,
137
- diagnostic_action=DiagnosticAction.MEASURE_ORG_LATENCY,
138
- reasoning="Validate org improvements.")
139
-
140
-
141
- # ─── Episode runners ───────────────────────────────────────────────────────
142
-
143
-
144
- def _mesh_gate_label(env: ImmunoOrgEnvironment) -> str:
145
- gate = getattr(env, "_last_pipeline_gate", None)
146
- if gate is None:
147
- return "β€”"
148
- if isinstance(gate, PipelineGate):
149
- return gate.value
150
- return str(gate)
151
-
152
-
153
- def _telemetry_row(env: ImmunoOrgEnvironment, obs) -> dict[str, str | int | float]:
154
- """Surface War Room, 4-gate mesh, migration/honeypots, MITRE-ish vector."""
155
- mig = {}
156
- try:
157
- mig = env.migration_engine.get_progress() or {}
158
- except Exception:
159
- pass
160
- honeys = mig.get("active_honeypots") or []
161
- if not isinstance(honeys, list):
162
- honeys = []
163
- att = "β€”"
164
- if obs.detected_attacks:
165
- att = obs.detected_attacks[0].vector.value
166
- d0 = obs.directives[0] if obs.directives else "β€”"
167
- if isinstance(d0, str) and len(d0) > 36:
168
- d0 = d0[:33] + "…"
169
- return {
170
- "mesh_ok": round(float(getattr(env, "_last_pipeline_integrity", 1.0) or 1.0), 2),
171
- "gate": _mesh_gate_label(env)[:28],
172
- "war_room": int(getattr(env, "_last_war_room_turns", 0) or 0),
173
- "honeypots": len(honeys),
174
- "migr": str(mig.get("current_phase", "β€”"))[:14] if mig.get("active") else "off",
175
- "migr_pct": int(100 * float(mig.get("progress_pct", 0) or 0)) if mig.get("active") else 0,
176
- "honeytokens": int(mig.get("honeytoken_activations", 0) or 0) if mig.get("active") else 0,
177
- "attack_vec": str(att)[:22],
178
- "directive": str(d0),
179
- }
180
-
181
-
182
- def _pick_demo_action(env: ImmunoOrgEnvironment, obs, policy_fn, step_index: int, showcase_migration: bool):
183
- """Optional injected steps so judges see migration + honeypots (not honeycomb UI β€” decoy nodes)."""
184
- if not showcase_migration:
185
- return policy_fn(env, obs)
186
- phase = obs.current_phase.value
187
- if env.migration_engine.state is None and step_index == 1 and phase in (
188
- "detection", "containment", "rca", "refactor",
189
- ):
190
- return ImmunoAction(
191
- action_type=ActionType.TACTICAL,
192
- tactical_action=TacticalAction.START_MIGRATION,
193
- target="core-backbone",
194
- reasoning="[Demo] Start 50-step polymorphic migration (decoys, honeypots, honeytokens).",
195
- parameters={"compliance": "SOC2"},
196
- )
197
- if env.migration_engine.state and step_index >= 3 and step_index % 4 == 0:
198
- return ImmunoAction(
199
- action_type=ActionType.TACTICAL,
200
- tactical_action=TacticalAction.DEPLOY_HONEYPOT,
201
- target="edge-pool",
202
- reasoning="[Demo] Deploy honeypot node on the migration track.",
203
- )
204
- return policy_fn(env, obs)
205
-
206
-
207
- def _run_episode(scenario, policy_fn, max_steps=30, *, showcase_migration: bool = False):
208
- """Roll out a policy on a scenario, return (frames, total_reward).
209
-
210
- `policy_fn(env, obs)` -> ImmunoAction
211
- """
212
- env = ImmunoOrgEnvironment(
213
- difficulty=int(scenario["difficulty"]),
214
- seed=int(scenario["seed"]),
215
- )
216
- hooks = scenario.get("hooks") or {}
217
- attach_hooks(env, hooks)
218
- obs = env.reset()
219
- apply_scenario_hooks(env, hooks)
220
-
221
- frames = []
222
- total = 0.0
223
- for step in range(min(max_steps, env.state.max_steps)):
224
- action = _pick_demo_action(env, obs, policy_fn, step, showcase_migration)
225
- obs, reward, done = env.step(action)
226
- shaped = float(reward) + float(training_step_penalty(env, action))
227
- total += shaped
228
- tel = _telemetry_row(env, obs)
229
- frames.append({
230
- "step": step + 1,
231
- "phase": obs.current_phase.value,
232
- "action_type": action.action_type.value,
233
- "action": (
234
- (action.tactical_action and action.tactical_action.value)
235
- or (action.strategic_action and action.strategic_action.value)
236
- or (action.diagnostic_action and action.diagnostic_action.value)
237
- or "?"
238
- ),
239
- "target": action.target or "-",
240
- "reasoning": action.reasoning,
241
- "reward": round(shaped, 3),
242
- "threats_left": len(env.attacks.get_active_attacks()),
243
- **tel,
244
- })
245
- if done:
246
- break
247
- return frames, total
248
-
249
-
250
- def _trained_policy(env, obs):
251
- """Trained-LLM policy. Falls back to heuristic if the adapter isn't on
252
- the Hub yet (e.g. the HPC run hasn't pushed)."""
253
- from immunoorg.trained_agent import TrainedAgentUnavailable, TrainedDefender
254
-
255
- try:
256
- td = TrainedDefender.get()
257
- obs_text = format_observation_for_llm(obs.model_dump())
258
- sys_prompt = get_defender_prompt()
259
- data = td.predict_action(obs_text, sys_prompt)
260
- except TrainedAgentUnavailable:
261
- return _heuristic_action(env, obs)
262
-
263
- try:
264
- atype = ActionType(data.get("action_type", "diagnostic"))
265
- except Exception:
266
- atype = ActionType.DIAGNOSTIC
267
-
268
- kwargs = dict(
269
- action_type=atype,
270
- target=data.get("target") or "",
271
- secondary_target=data.get("secondary_target"),
272
- parameters=data.get("parameters") or {},
273
- reasoning=data.get("reasoning") or "",
274
- )
275
- try:
276
- if atype == ActionType.TACTICAL and data.get("tactical_action"):
277
- kwargs["tactical_action"] = TacticalAction(data["tactical_action"])
278
- elif atype == ActionType.STRATEGIC and data.get("strategic_action"):
279
- kwargs["strategic_action"] = StrategicAction(data["strategic_action"])
280
- elif atype == ActionType.DIAGNOSTIC and data.get("diagnostic_action"):
281
- kwargs["diagnostic_action"] = DiagnosticAction(data["diagnostic_action"])
282
- except Exception:
283
- kwargs["diagnostic_action"] = DiagnosticAction.QUERY_BELIEF_MAP
284
- kwargs["action_type"] = ActionType.DIAGNOSTIC
285
-
286
- return ImmunoAction(**kwargs)
287
-
288
-
289
- # ─── Gradio handler ────────────────────────────────────────────────────────
290
-
291
-
292
- def _frames_to_table(frames):
293
- out = []
294
- for f in frames:
295
- out.append([
296
- f["step"],
297
- f["phase"],
298
- f["action_type"],
299
- f["action"],
300
- f["target"],
301
- f["reward"],
302
- f["threats_left"],
303
- f["mesh_ok"],
304
- f["gate"],
305
- f["war_room"],
306
- f["honeypots"],
307
- f["migr"],
308
- f["migr_pct"],
309
- f["honeytokens"],
310
- f["attack_vec"],
311
- f["directive"],
312
- f["reasoning"][:72],
313
- ])
314
- return out
315
-
316
-
317
- def _feature_dashboard_figure(heur_frames: list, trained_frames: list):
318
- """Plotly: pipeline integrity, honeypots, honeytokens, War Room turns."""
319
- import plotly.graph_objects as go
320
- from plotly.subplots import make_subplots
321
-
322
- fig = make_subplots(
323
- rows=4,
324
- cols=1,
325
- shared_xaxes=True,
326
- vertical_spacing=0.05,
327
- subplot_titles=(
328
- "4-gate DevSecOps mesh β€” pipeline integrity (1.0 = clean)",
329
- "Honeypots deployed (moving-target / decoy layer)",
330
- "Honeytoken activations (trap callbacks)",
331
- "War Room β€” consensus rounds (CISO / DevOps / Architect)",
332
- ),
333
- )
334
- specs = [
335
- ("mesh_ok", "Heuristic", "#ff7f0e", "Trained", "#1f77b4"),
336
- ("honeypots", "Heuristic honeypots", "#ff7f0e", "Trained honeypots", "#1f77b4"),
337
- ("honeytokens", "Heuristic honeytokens", "#ff7f0e", "Trained honeytokens", "#1f77b4"),
338
- ("war_room", "Heuristic WR turns", "#ff7f0e", "Trained WR turns", "#1f77b4"),
339
- ]
340
- for row, (key, n1, c1, n2, c2) in enumerate(specs, start=1):
341
- xh = [f["step"] for f in heur_frames]
342
- yh = [f[key] for f in heur_frames]
343
- xt = [f["step"] for f in trained_frames]
344
- yt = [f[key] for f in trained_frames]
345
- fig.add_trace(
346
- go.Scatter(
347
- x=xh, y=yh, mode="lines+markers", name=n1,
348
- line=dict(color=c1, shape="hv"), legendgroup="h", showlegend=(row == 1),
349
- ),
350
- row=row, col=1,
351
- )
352
- fig.add_trace(
353
- go.Scatter(
354
- x=xt, y=yt, mode="lines+markers", name=n2,
355
- line=dict(color=c2, shape="hv", dash="dash"), legendgroup="t", showlegend=(row == 1),
356
- ),
357
- row=row, col=1,
358
- )
359
- fig.update_layout(
360
- height=780,
361
- margin=dict(t=36, b=24, l=48, r=24),
362
- legend=dict(orientation="h", yanchor="bottom", y=1.02, x=0),
363
- font=dict(size=11),
364
- )
365
- fig.update_xaxes(title_text="step", row=4, col=1)
366
- return fig
367
-
368
-
369
- def _mesh_gate_bar_figure(heur_frames: list, trained_frames: list):
370
- """Grouped bar: how often each mesh gate fired (per episode)."""
371
- import plotly.graph_objects as go
372
-
373
- def counts(frames: list) -> dict[str, int]:
374
- c: Counter[str] = Counter()
375
- for f in frames:
376
- g = str(f.get("gate") or "").strip()
377
- if g and g != "β€”":
378
- # Short labels for x axis
379
- short = g.replace("gate", "").replace("_", " ")[:22]
380
- c[short] += 1
381
- return dict(c)
382
-
383
- ch, ct = counts(heur_frames), counts(trained_frames)
384
- if not ch and not ct:
385
- fig = go.Figure()
386
- fig.add_annotation(
387
- text="No mesh gate catches this episode (pipeline stayed clean).",
388
- xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False,
389
- )
390
- fig.update_layout(height=280, margin=dict(t=40, b=20))
391
- return fig
392
-
393
- keys = sorted(set(ch) | set(ct), key=lambda k: (ch.get(k, 0) + ct.get(k, 0)), reverse=True)[:12]
394
- fig = go.Figure(
395
- data=[
396
- go.Bar(name="Heuristic", x=keys, y=[ch.get(k, 0) for k in keys], marker_color="#ff7f0e"),
397
- go.Bar(name="Trained", x=keys, y=[ct.get(k, 0) for k in keys], marker_color="#1f77b4"),
398
- ]
399
- )
400
- fig.update_layout(
401
- title="Mesh gate catches (count of steps where each gate flagged)",
402
- barmode="group",
403
- height=320,
404
- margin=dict(t=50, b=80, l=48, r=24),
405
- xaxis_tickangle=-28,
406
- font=dict(size=11),
407
- )
408
- return fig
409
-
410
-
411
- def _trained_status_text() -> str:
412
- try:
413
- from immunoorg.trained_agent import TrainedDefender
414
-
415
- s = TrainedDefender.get().status()
416
- except Exception as e:
417
- return f"⚠️ trained adapter status check failed: {e}"
418
-
419
- if s.get("repo_exists"):
420
- if s.get("loaded"):
421
- return f"βœ… Trained adapter LOADED from `{s['repo_id']}` (sha {s.get('sha','?')[:7]})"
422
- return (f"🟒 Trained adapter found on the Hub at `{s['repo_id']}` β€” "
423
- f"will load on first 'Run trained agent' click.")
424
- return (f"⏳ Trained adapter not on the Hub yet at `{s['repo_id']}`. "
425
- f"HPC pipeline run-in-progress β€” heuristic policy will be used "
426
- f"until the LoRA is pushed.")
427
-
428
-
429
- def run_demo(scenario_label, max_steps, showcase_migration):
430
- family = _LABEL_TO_FAMILY[scenario_label]
431
- scenario = _scenario_for(family)
432
- show_mig = bool(showcase_migration)
433
-
434
- heur_frames, heur_total = _run_episode(
435
- scenario, _heuristic_action, int(max_steps), showcase_migration=show_mig
436
- )
437
- trained_frames, trained_total = _run_episode(
438
- scenario, _trained_policy, int(max_steps), showcase_migration=show_mig
439
- )
440
-
441
- # Per-step reward chart
442
- import numpy as np
443
-
444
- chart_data = {
445
- "step": list(range(1, max(len(heur_frames), len(trained_frames)) + 1)),
446
- }
447
- chart_data["heuristic"] = (
448
- [f["reward"] for f in heur_frames]
449
- + [None] * (len(chart_data["step"]) - len(heur_frames))
450
- )
451
- chart_data["trained"] = (
452
- [f["reward"] for f in trained_frames]
453
- + [None] * (len(chart_data["step"]) - len(trained_frames))
454
- )
455
-
456
- delta = trained_total - heur_total
457
- if delta > 0.5:
458
- verdict = f"πŸ† Trained agent WINS by **{delta:+.2f}** reward over heuristic baseline"
459
- elif delta < -0.5:
460
- verdict = f"πŸ“‰ Trained agent UNDERPERFORMS heuristic by **{delta:+.2f}** (try more training)"
461
- else:
462
- verdict = f"βž– Trained β‰ˆ heuristic this episode (Ξ” = {delta:+.2f})"
463
-
464
- summary_md = f"""
465
- ### Scenario: **{scenario_label}**
466
-
467
- | Policy | total reward (over {len(heur_frames)} step{'' if len(heur_frames)==1 else 's'}) | threats_left at end |
468
- | --- | ---: | ---: |
469
- | Heuristic baseline | {heur_total:+.2f} | {heur_frames[-1]['threats_left'] if heur_frames else '?'} |
470
- | Trained LLM | {trained_total:+.2f} | {trained_frames[-1]['threats_left'] if trained_frames else '?'} |
471
-
472
- {verdict}
473
- """
474
-
475
- dash = _feature_dashboard_figure(heur_frames, trained_frames)
476
- gates = _mesh_gate_bar_figure(heur_frames, trained_frames)
477
-
478
- return (
479
- summary_md,
480
- _frames_to_table(heur_frames),
481
- _frames_to_table(trained_frames),
482
- chart_data,
483
- dash,
484
- gates,
485
- _trained_status_text(),
486
- )
487
-
488
-
489
- # ─── Build the UI ──────────────────────────────────────────────────────────
490
-
491
-
492
- def build_demo() -> gr.Blocks:
493
- table_headers = [
494
- "step", "phase", "type", "action", "target", "reward", "threats",
495
- "pipeline", "mesh gate", "WR turns", "honeypots", "migr phase",
496
- "migr %", "honeytokens", "attack vec", "directive", "reasoning",
497
- ]
498
-
499
- with gr.Blocks(title="ImmunoOrg 2.0 β€” Live Demo", analytics_enabled=False) as demo:
500
- gr.Markdown(
501
- """
502
- # πŸ›‘οΈ ImmunoOrg 2.0 β€” Live Demo
503
-
504
- The agent has to defend an enterprise from a cyber-attack **and**
505
- restructure the organization that lets the attack succeed in the first
506
- place. Pick one of the 5 scenario families and watch the heuristic
507
- baseline play it head-to-head against the GRPO-trained LLM defender.
508
-
509
- **What the extra columns show (backend features, live from the sim):**
510
-
511
- | Column | Feature in codebase |
512
- | --- | --- |
513
- | **pipeline / mesh gate** | 4-gate **DevSecOps Mesh** (`devsecops_mesh.py`): AST β†’ semantic β†’ Terraform β†’ sandbox; gate shows which layer flagged a payload. |
514
- | **WR turns** | **War Room** multi-agent debate rounds toward consensus (`war_room.py`). |
515
- | **honeypots / migr / honeytokens** | **50-step polymorphic migration** (`migration_engine.py`): decoy phase, honeypot nodes, honeytoken activations β€” *not* a separate β€œhoneycomb” UI; honeypots are tactical decoys here. |
516
- | **attack vec** | Active attack vector (feeds **MITRE** / kill-chain context in the full env). |
517
- | **directive** | Board directive text when the scenario injects one. |
518
-
519
- **Charts below:** interactive **Plotly** dashboards β€” pipeline/decoys/War Room time series, plus **mesh gate** catch counts.
520
-
521
- > πŸ“š [Problem statement](https://github.com/Charannoo/immunoorg/blob/master/PROBLEM_STATEMENT.md)
522
- > Β· [Source](https://github.com/Charannoo/immunoorg)
523
- > Β· [Blog](https://github.com/Charannoo/immunoorg/blob/master/BLOG_POST.md)
524
- > Β· [Training notebook](https://github.com/Charannoo/immunoorg/blob/master/ImmunoOrg_Training_Colab.ipynb)
525
- """
526
- )
527
-
528
- status_md = gr.Markdown(_trained_status_text())
529
-
530
- with gr.Row():
531
- scenario_dd = gr.Dropdown(
532
- choices=list(_SCENARIO_LABEL.values()),
533
- value=list(_SCENARIO_LABEL.values())[1],
534
- label="Scenario family",
535
- )
536
- steps_sl = gr.Slider(5, 30, value=15, step=1, label="Max steps per episode")
537
- mig_cb = gr.Checkbox(
538
- value=True,
539
- label="Demo: run START_MIGRATION + honeypot beats (shows decoys/honeytokens)",
540
- )
541
- run_btn = gr.Button("Run episode", variant="primary")
542
-
543
- summary_md = gr.Markdown()
544
-
545
- with gr.Row():
546
- with gr.Column():
547
- gr.Markdown("### Heuristic baseline")
548
- heur_table = gr.Dataframe(headers=table_headers, wrap=True)
549
- with gr.Column():
550
- gr.Markdown("### Trained LLM (or heuristic fallback)")
551
- trained_table = gr.Dataframe(headers=table_headers, wrap=True)
552
-
553
- chart = gr.LinePlot(
554
- x="step", y="heuristic",
555
- title="Per-step reward (heuristic = orange, trained = blue)",
556
- height=260,
557
- )
558
-
559
- gr.Markdown("### Feature dashboards (Plotly β€” zoom/pan/hover)")
560
- with gr.Row():
561
- signals_plot = gr.Plot(label="Pipeline, honeypots, honeytokens, War Room")
562
- gate_plot = gr.Plot(label="Which mesh gate fired (AST / semantic / Terraform / sandbox)")
563
-
564
- gr.Markdown(
565
- """
566
- ---
567
-
568
- ### What the agent is reasoning about
569
-
570
- - 28 actions across 3 categories: **tactical** (block_port, isolate_node, deploy_patch, **deploy_honeypot**, start_migration…),
571
- **strategic** (merge_departments, reduce_bureaucracy, establish_devsecops…),
572
- **diagnostic** (correlate_failure, identify_silo, vulnerability_scan…).
573
- - 5-track composable reward:
574
- uptime (25%) Β· threat neutralization (25%) Β· bureaucracy efficiency (20%) Β·
575
- code-patch quality (20%) Β· pipeline integrity (10%) β€” pipeline ties to **mesh** columns.
576
- - Trained on the elite 20/20/20/20/20 mix of scenario families
577
- (basic / RAG / executive / silo / stealth) with TRL GRPO + Unsloth.
578
-
579
- Uncheck **Demo: migration + honeypot** for a β€œpure” heuristic/LLM comparison without injected migration steps.
580
- """
581
- )
582
-
583
- run_btn.click(
584
- run_demo,
585
- inputs=[scenario_dd, steps_sl, mig_cb],
586
- outputs=[
587
- summary_md, heur_table, trained_table, chart,
588
- signals_plot, gate_plot, status_md,
589
- ],
590
- )
591
-
592
- return demo
593
-
594
-
595
- __all__ = ["build_demo"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hackathon-judge demo UI for the live HF Space.
3
+
4
+ What this gives the judge when they click the Space link:
5
+
6
+ - One-screen Gradio panel (episode demo + **War Room** LLM debate accordion).
7
+ - Pick a scenario family from the 5 elite ones (basic / RAG / executive
8
+ alignment / silo-breaker / stealth-adaptive).
9
+ - Click "Run episode" -> we play the heuristic policy for up to 30 steps
10
+ in the env, then re-run the same scenario seed with the *trained* LLM
11
+ policy (if available on the Hub) and show side-by-side results.
12
+ - Outputs:
13
+ - reward delta (trained vs heuristic) with a clear winner indicator
14
+ - the agent's per-step action stream, with reasoning
15
+ - a chart of per-step reward
16
+ - a status badge for the trained adapter ("ready" / "training in progress")
17
+
18
+ The point is for a non-technical reviewer to see
19
+ "agent observed X -> agent did Y -> reward Z" on screen in 10 seconds.
20
+
21
+ Mounted at ``/demo`` on the FastAPI app via ``gr.mount_gradio_app``.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import asyncio
27
+ from collections import Counter
28
+ from typing import Any
29
+
30
+ import gradio as gr
31
+
32
+ from server.war_room_debate import run_war_room_debate
33
+
34
+ from immunoorg.agents.defender import (
35
+ format_observation_for_llm,
36
+ get_defender_prompt,
37
+ )
38
+ from immunoorg.environment import ImmunoOrgEnvironment
39
+ from immunoorg.models import (
40
+ ActionType,
41
+ DiagnosticAction,
42
+ ImmunoAction,
43
+ PipelineGate,
44
+ StrategicAction,
45
+ TacticalAction,
46
+ )
47
+ from training.dataset_generator import DatasetConfig, DatasetGenerator
48
+ from training.scenario_hooks import (
49
+ apply_scenario_hooks,
50
+ attach_hooks,
51
+ training_step_penalty,
52
+ )
53
+
54
+
55
+ # ─── Scenario catalogue ─────────────────────────────────────────────────────
56
+
57
+
58
+ _SCENARIO_LABEL = {
59
+ "basic_containment": "1. Basic Containment (warm-up)",
60
+ "rag_grounding": "2. RAG-Grounding (use CVE intel, not blunt isolate)",
61
+ "executive_alignment": "3. Executive Alignment (uptime directive overrides instinct)",
62
+ "silo_breaker": "4. Silo-Breaker (org friction blocks tactical actions)",
63
+ "stealth_adaptive": "5. Stealth & Adaptive (multi-step investigation)",
64
+ }
65
+ _LABEL_TO_FAMILY = {v: k for k, v in _SCENARIO_LABEL.items()}
66
+
67
+
68
+ _SCENARIO_CACHE: dict[str, dict[str, Any]] = {}
69
+
70
+
71
+ def _scenario_for(family: str) -> dict[str, Any]:
72
+ """Generate one balanced elite scenario per family, cached."""
73
+ if family in _SCENARIO_CACHE:
74
+ return _SCENARIO_CACHE[family]
75
+ gen = DatasetGenerator(DatasetConfig(
76
+ dataset_type="elite",
77
+ output_dir="/tmp/_demo_scenarios",
78
+ verbose=False,
79
+ compress_output=False,
80
+ ))
81
+ scenarios = gen.generate_elite_scenario_mix_dataset(total=5)
82
+ for sc in scenarios:
83
+ if sc["family"] == family and family not in _SCENARIO_CACHE:
84
+ _SCENARIO_CACHE[family] = sc
85
+ return _SCENARIO_CACHE[family]
86
+
87
+
88
+ # ─── Heuristic policy (mirrors scripts/generate_training_evidence.py) ──────
89
+
90
+
91
+ def _heuristic_action(env, obs):
92
+ phase = obs.current_phase.value
93
+ nodes = obs.visible_nodes
94
+ compromised = [n for n in nodes if n.compromised and not n.isolated]
95
+ hooks = getattr(env, "_active_scenario_hooks", {}) or {}
96
+
97
+ if hooks.get("inject_rag_best_mitigation") and phase in ("detection", "containment") and compromised:
98
+ return ImmunoAction(action_type=ActionType.TACTICAL,
99
+ tactical_action=TacticalAction.SNAPSHOT_FORENSICS,
100
+ target=compromised[0].id,
101
+ reasoning="RAG: forensic snapshot before patching the rootkit.")
102
+ if hooks.get("board_uptime_no_isolate") and phase == "containment":
103
+ target = compromised[0].id if compromised else (nodes[0].id if nodes else "")
104
+ return ImmunoAction(action_type=ActionType.TACTICAL,
105
+ tactical_action=TacticalAction.DEPLOY_PATCH,
106
+ target=target,
107
+ reasoning="Board directive: patch instead of isolating.")
108
+ if hooks.get("force_denials_on_isolate") and phase in ("containment", "rca", "refactor"):
109
+ return ImmunoAction(action_type=ActionType.STRATEGIC,
110
+ strategic_action=StrategicAction.ESTABLISH_DEVSECOPS,
111
+ target="dept-security", secondary_target="dept-engineering",
112
+ reasoning="Approver keeps denying; restructure the org.")
113
+ if hooks.get("stealthy_initial_attack") and phase == "detection":
114
+ return ImmunoAction(action_type=ActionType.DIAGNOSTIC,
115
+ diagnostic_action=DiagnosticAction.VULNERABILITY_SCAN,
116
+ reasoning="Stealth attack: deeper scan first.")
117
+
118
+ if phase == "detection":
119
+ target = compromised[0].id if compromised else (nodes[0].id if nodes else "")
120
+ return ImmunoAction(action_type=ActionType.TACTICAL,
121
+ tactical_action=TacticalAction.SCAN_LOGS,
122
+ target=target, reasoning="Detection: scan for indicators.")
123
+ if phase == "containment":
124
+ if compromised:
125
+ return ImmunoAction(action_type=ActionType.TACTICAL,
126
+ tactical_action=TacticalAction.ISOLATE_NODE,
127
+ target=compromised[0].id, reasoning="Isolate compromised node.")
128
+ return ImmunoAction(action_type=ActionType.DIAGNOSTIC,
129
+ diagnostic_action=DiagnosticAction.TIMELINE_RECONSTRUCT,
130
+ reasoning="Reconstruct timeline.")
131
+ if phase == "rca":
132
+ return ImmunoAction(action_type=ActionType.DIAGNOSTIC,
133
+ diagnostic_action=DiagnosticAction.IDENTIFY_SILO,
134
+ reasoning="Find the silo behind the failure.")
135
+ if phase == "refactor":
136
+ return ImmunoAction(action_type=ActionType.STRATEGIC,
137
+ strategic_action=StrategicAction.REDUCE_BUREAUCRACY,
138
+ target="dept-management", reasoning="Reduce approval latency.")
139
+ return ImmunoAction(action_type=ActionType.DIAGNOSTIC,
140
+ diagnostic_action=DiagnosticAction.MEASURE_ORG_LATENCY,
141
+ reasoning="Validate org improvements.")
142
+
143
+
144
+ # ─── Episode runners ───────────────────────────────────────────────────────
145
+
146
+
147
+ def _mesh_gate_label(env: ImmunoOrgEnvironment) -> str:
148
+ gate = getattr(env, "_last_pipeline_gate", None)
149
+ if gate is None:
150
+ return "β€”"
151
+ if isinstance(gate, PipelineGate):
152
+ return gate.value
153
+ return str(gate)
154
+
155
+
156
+ def _telemetry_row(env: ImmunoOrgEnvironment, obs) -> dict[str, str | int | float]:
157
+ """Surface War Room, 4-gate mesh, migration/honeypots, MITRE-ish vector."""
158
+ mig = {}
159
+ try:
160
+ mig = env.migration_engine.get_progress() or {}
161
+ except Exception:
162
+ pass
163
+ honeys = mig.get("active_honeypots") or []
164
+ if not isinstance(honeys, list):
165
+ honeys = []
166
+ att = "β€”"
167
+ if obs.detected_attacks:
168
+ att = obs.detected_attacks[0].vector.value
169
+ d0 = obs.directives[0] if obs.directives else "β€”"
170
+ if isinstance(d0, str) and len(d0) > 36:
171
+ d0 = d0[:33] + "…"
172
+ return {
173
+ "mesh_ok": round(float(getattr(env, "_last_pipeline_integrity", 1.0) or 1.0), 2),
174
+ "gate": _mesh_gate_label(env)[:28],
175
+ "war_room": int(getattr(env, "_last_war_room_turns", 0) or 0),
176
+ "honeypots": len(honeys),
177
+ "migr": str(mig.get("current_phase", "β€”"))[:14] if mig.get("active") else "off",
178
+ "migr_pct": int(100 * float(mig.get("progress_pct", 0) or 0)) if mig.get("active") else 0,
179
+ "honeytokens": int(mig.get("honeytoken_activations", 0) or 0) if mig.get("active") else 0,
180
+ "attack_vec": str(att)[:22],
181
+ "directive": str(d0),
182
+ }
183
+
184
+
185
+ def _pick_demo_action(env: ImmunoOrgEnvironment, obs, policy_fn, step_index: int, showcase_migration: bool):
186
+ """Optional injected steps so judges see migration + honeypots (not honeycomb UI β€” decoy nodes)."""
187
+ if not showcase_migration:
188
+ return policy_fn(env, obs)
189
+ phase = obs.current_phase.value
190
+ if env.migration_engine.state is None and step_index == 1 and phase in (
191
+ "detection", "containment", "rca", "refactor",
192
+ ):
193
+ return ImmunoAction(
194
+ action_type=ActionType.TACTICAL,
195
+ tactical_action=TacticalAction.START_MIGRATION,
196
+ target="core-backbone",
197
+ reasoning="[Demo] Start 50-step polymorphic migration (decoys, honeypots, honeytokens).",
198
+ parameters={"compliance": "SOC2"},
199
+ )
200
+ if env.migration_engine.state and step_index >= 3 and step_index % 4 == 0:
201
+ return ImmunoAction(
202
+ action_type=ActionType.TACTICAL,
203
+ tactical_action=TacticalAction.DEPLOY_HONEYPOT,
204
+ target="edge-pool",
205
+ reasoning="[Demo] Deploy honeypot node on the migration track.",
206
+ )
207
+ return policy_fn(env, obs)
208
+
209
+
210
+ def _run_episode(scenario, policy_fn, max_steps=30, *, showcase_migration: bool = False):
211
+ """Roll out a policy on a scenario, return (frames, total_reward).
212
+
213
+ `policy_fn(env, obs)` -> ImmunoAction
214
+ """
215
+ env = ImmunoOrgEnvironment(
216
+ difficulty=int(scenario["difficulty"]),
217
+ seed=int(scenario["seed"]),
218
+ )
219
+ hooks = scenario.get("hooks") or {}
220
+ attach_hooks(env, hooks)
221
+ obs = env.reset()
222
+ apply_scenario_hooks(env, hooks)
223
+
224
+ frames = []
225
+ total = 0.0
226
+ for step in range(min(max_steps, env.state.max_steps)):
227
+ action = _pick_demo_action(env, obs, policy_fn, step, showcase_migration)
228
+ obs, reward, done = env.step(action)
229
+ shaped = float(reward) + float(training_step_penalty(env, action))
230
+ total += shaped
231
+ tel = _telemetry_row(env, obs)
232
+ frames.append({
233
+ "step": step + 1,
234
+ "phase": obs.current_phase.value,
235
+ "action_type": action.action_type.value,
236
+ "action": (
237
+ (action.tactical_action and action.tactical_action.value)
238
+ or (action.strategic_action and action.strategic_action.value)
239
+ or (action.diagnostic_action and action.diagnostic_action.value)
240
+ or "?"
241
+ ),
242
+ "target": action.target or "-",
243
+ "reasoning": action.reasoning,
244
+ "reward": round(shaped, 3),
245
+ "threats_left": len(env.attacks.get_active_attacks()),
246
+ **tel,
247
+ })
248
+ if done:
249
+ break
250
+ return frames, total
251
+
252
+
253
+ def _trained_policy(env, obs):
254
+ """Trained-LLM policy. Falls back to heuristic if the adapter isn't on
255
+ the Hub yet (e.g. the HPC run hasn't pushed)."""
256
+ from immunoorg.trained_agent import TrainedAgentUnavailable, TrainedDefender
257
+
258
+ try:
259
+ td = TrainedDefender.get()
260
+ obs_text = format_observation_for_llm(obs.model_dump())
261
+ sys_prompt = get_defender_prompt()
262
+ data = td.predict_action(obs_text, sys_prompt)
263
+ except TrainedAgentUnavailable:
264
+ return _heuristic_action(env, obs)
265
+
266
+ try:
267
+ atype = ActionType(data.get("action_type", "diagnostic"))
268
+ except Exception:
269
+ atype = ActionType.DIAGNOSTIC
270
+
271
+ kwargs = dict(
272
+ action_type=atype,
273
+ target=data.get("target") or "",
274
+ secondary_target=data.get("secondary_target"),
275
+ parameters=data.get("parameters") or {},
276
+ reasoning=data.get("reasoning") or "",
277
+ )
278
+ try:
279
+ if atype == ActionType.TACTICAL and data.get("tactical_action"):
280
+ kwargs["tactical_action"] = TacticalAction(data["tactical_action"])
281
+ elif atype == ActionType.STRATEGIC and data.get("strategic_action"):
282
+ kwargs["strategic_action"] = StrategicAction(data["strategic_action"])
283
+ elif atype == ActionType.DIAGNOSTIC and data.get("diagnostic_action"):
284
+ kwargs["diagnostic_action"] = DiagnosticAction(data["diagnostic_action"])
285
+ except Exception:
286
+ kwargs["diagnostic_action"] = DiagnosticAction.QUERY_BELIEF_MAP
287
+ kwargs["action_type"] = ActionType.DIAGNOSTIC
288
+
289
+ return ImmunoAction(**kwargs)
290
+
291
+
292
+ # ─── Gradio handler ────────────────────────────────────────────────────────
293
+
294
+
295
+ def _frames_to_table(frames):
296
+ out = []
297
+ for f in frames:
298
+ out.append([
299
+ f["step"],
300
+ f["phase"],
301
+ f["action_type"],
302
+ f["action"],
303
+ f["target"],
304
+ f["reward"],
305
+ f["threats_left"],
306
+ f["mesh_ok"],
307
+ f["gate"],
308
+ f["war_room"],
309
+ f["honeypots"],
310
+ f["migr"],
311
+ f["migr_pct"],
312
+ f["honeytokens"],
313
+ f["attack_vec"],
314
+ f["directive"],
315
+ f["reasoning"][:72],
316
+ ])
317
+ return out
318
+
319
+
320
+ def _feature_dashboard_figure(heur_frames: list, trained_frames: list):
321
+ """Plotly: pipeline integrity, honeypots, honeytokens, War Room turns."""
322
+ import plotly.graph_objects as go
323
+ from plotly.subplots import make_subplots
324
+
325
+ fig = make_subplots(
326
+ rows=4,
327
+ cols=1,
328
+ shared_xaxes=True,
329
+ vertical_spacing=0.05,
330
+ subplot_titles=(
331
+ "4-gate DevSecOps mesh β€” pipeline integrity (1.0 = clean)",
332
+ "Honeypots deployed (moving-target / decoy layer)",
333
+ "Honeytoken activations (trap callbacks)",
334
+ "War Room β€” consensus rounds (CISO / DevOps / Architect)",
335
+ ),
336
+ )
337
+ specs = [
338
+ ("mesh_ok", "Heuristic", "#ff7f0e", "Trained", "#1f77b4"),
339
+ ("honeypots", "Heuristic honeypots", "#ff7f0e", "Trained honeypots", "#1f77b4"),
340
+ ("honeytokens", "Heuristic honeytokens", "#ff7f0e", "Trained honeytokens", "#1f77b4"),
341
+ ("war_room", "Heuristic WR turns", "#ff7f0e", "Trained WR turns", "#1f77b4"),
342
+ ]
343
+ for row, (key, n1, c1, n2, c2) in enumerate(specs, start=1):
344
+ xh = [f["step"] for f in heur_frames]
345
+ yh = [f[key] for f in heur_frames]
346
+ xt = [f["step"] for f in trained_frames]
347
+ yt = [f[key] for f in trained_frames]
348
+ fig.add_trace(
349
+ go.Scatter(
350
+ x=xh, y=yh, mode="lines+markers", name=n1,
351
+ line=dict(color=c1, shape="hv"), legendgroup="h", showlegend=(row == 1),
352
+ ),
353
+ row=row, col=1,
354
+ )
355
+ fig.add_trace(
356
+ go.Scatter(
357
+ x=xt, y=yt, mode="lines+markers", name=n2,
358
+ line=dict(color=c2, shape="hv", dash="dash"), legendgroup="t", showlegend=(row == 1),
359
+ ),
360
+ row=row, col=1,
361
+ )
362
+ fig.update_layout(
363
+ height=780,
364
+ margin=dict(t=36, b=24, l=48, r=24),
365
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, x=0),
366
+ font=dict(size=11),
367
+ )
368
+ fig.update_xaxes(title_text="step", row=4, col=1)
369
+ return fig
370
+
371
+
372
+ def _mesh_gate_bar_figure(heur_frames: list, trained_frames: list):
373
+ """Grouped bar: how often each mesh gate fired (per episode)."""
374
+ import plotly.graph_objects as go
375
+
376
+ def counts(frames: list) -> dict[str, int]:
377
+ c: Counter[str] = Counter()
378
+ for f in frames:
379
+ g = str(f.get("gate") or "").strip()
380
+ if g and g != "β€”":
381
+ # Short labels for x axis
382
+ short = g.replace("gate", "").replace("_", " ")[:22]
383
+ c[short] += 1
384
+ return dict(c)
385
+
386
+ ch, ct = counts(heur_frames), counts(trained_frames)
387
+ if not ch and not ct:
388
+ fig = go.Figure()
389
+ fig.add_annotation(
390
+ text="No mesh gate catches this episode (pipeline stayed clean).",
391
+ xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False,
392
+ )
393
+ fig.update_layout(height=280, margin=dict(t=40, b=20))
394
+ return fig
395
+
396
+ keys = sorted(set(ch) | set(ct), key=lambda k: (ch.get(k, 0) + ct.get(k, 0)), reverse=True)[:12]
397
+ fig = go.Figure(
398
+ data=[
399
+ go.Bar(name="Heuristic", x=keys, y=[ch.get(k, 0) for k in keys], marker_color="#ff7f0e"),
400
+ go.Bar(name="Trained", x=keys, y=[ct.get(k, 0) for k in keys], marker_color="#1f77b4"),
401
+ ]
402
+ )
403
+ fig.update_layout(
404
+ title="Mesh gate catches (count of steps where each gate flagged)",
405
+ barmode="group",
406
+ height=320,
407
+ margin=dict(t=50, b=80, l=48, r=24),
408
+ xaxis_tickangle=-28,
409
+ font=dict(size=11),
410
+ )
411
+ return fig
412
+
413
+
414
+ def _trained_status_text() -> str:
415
+ try:
416
+ from immunoorg.trained_agent import TrainedDefender
417
+
418
+ s = TrainedDefender.get().status()
419
+ except Exception as e:
420
+ return f"⚠️ trained adapter status check failed: {e}"
421
+
422
+ if s.get("repo_exists"):
423
+ if s.get("loaded"):
424
+ return f"βœ… Trained adapter LOADED from `{s['repo_id']}` (sha {s.get('sha','?')[:7]})"
425
+ return (f"🟒 Trained adapter found on the Hub at `{s['repo_id']}` β€” "
426
+ f"will load on first 'Run trained agent' click.")
427
+ return (f"⏳ Trained adapter not on the Hub yet at `{s['repo_id']}`. "
428
+ f"HPC pipeline run-in-progress β€” heuristic policy will be used "
429
+ f"until the LoRA is pushed.")
430
+
431
+
432
+ def run_demo(scenario_label, max_steps, showcase_migration):
433
+ family = _LABEL_TO_FAMILY[scenario_label]
434
+ scenario = _scenario_for(family)
435
+ show_mig = bool(showcase_migration)
436
+
437
+ heur_frames, heur_total = _run_episode(
438
+ scenario, _heuristic_action, int(max_steps), showcase_migration=show_mig
439
+ )
440
+ trained_frames, trained_total = _run_episode(
441
+ scenario, _trained_policy, int(max_steps), showcase_migration=show_mig
442
+ )
443
+
444
+ # Per-step reward chart
445
+ import numpy as np
446
+
447
+ chart_data = {
448
+ "step": list(range(1, max(len(heur_frames), len(trained_frames)) + 1)),
449
+ }
450
+ chart_data["heuristic"] = (
451
+ [f["reward"] for f in heur_frames]
452
+ + [None] * (len(chart_data["step"]) - len(heur_frames))
453
+ )
454
+ chart_data["trained"] = (
455
+ [f["reward"] for f in trained_frames]
456
+ + [None] * (len(chart_data["step"]) - len(trained_frames))
457
+ )
458
+
459
+ delta = trained_total - heur_total
460
+ if delta > 0.5:
461
+ verdict = f"πŸ† Trained agent WINS by **{delta:+.2f}** reward over heuristic baseline"
462
+ elif delta < -0.5:
463
+ verdict = f"πŸ“‰ Trained agent UNDERPERFORMS heuristic by **{delta:+.2f}** (try more training)"
464
+ else:
465
+ verdict = f"βž– Trained β‰ˆ heuristic this episode (Ξ” = {delta:+.2f})"
466
+
467
+ summary_md = f"""
468
+ ### Scenario: **{scenario_label}**
469
+
470
+ | Policy | total reward (over {len(heur_frames)} step{'' if len(heur_frames)==1 else 's'}) | threats_left at end |
471
+ | --- | ---: | ---: |
472
+ | Heuristic baseline | {heur_total:+.2f} | {heur_frames[-1]['threats_left'] if heur_frames else '?'} |
473
+ | Trained LLM | {trained_total:+.2f} | {trained_frames[-1]['threats_left'] if trained_frames else '?'} |
474
+
475
+ {verdict}
476
+ """
477
+
478
+ dash = _feature_dashboard_figure(heur_frames, trained_frames)
479
+ gates = _mesh_gate_bar_figure(heur_frames, trained_frames)
480
+
481
+ return (
482
+ summary_md,
483
+ _frames_to_table(heur_frames),
484
+ _frames_to_table(trained_frames),
485
+ chart_data,
486
+ dash,
487
+ gates,
488
+ _trained_status_text(),
489
+ )
490
+
491
+
492
+ # ─── War Room (LLM debate) β€” same Gradio page as episode demo ────────────
493
+
494
+
495
+ def _format_war_room_markdown(data: dict[str, Any]) -> str:
496
+ lines: list[str] = []
497
+ lines.append(
498
+ f"*LLM: `{data.get('model')}` Β· backend: `{data.get('llm_provider', '?')}`*\n"
499
+ )
500
+ verdict = data.get("verdict") or {}
501
+ lines.append("## Final verdict\n")
502
+ ca = verdict.get("consensus_action")
503
+ lines.append(
504
+ f"**{verdict.get('status', 'β€”')}**"
505
+ + (f" β€” action: `{ca}`" if ca else "")
506
+ + "\n"
507
+ )
508
+ for v in verdict.get("votes_detail") or []:
509
+ lines.append(f"- **{v.get('agent')}:** {v.get('action')}")
510
+ lines.append("\n## Initial positions\n")
511
+ for a in data.get("agents") or []:
512
+ warn = ""
513
+ if a.get("hallucination_flags"):
514
+ warn = " ⚠️ *flags (cross-exam)*: " + "; ".join(a["hallucination_flags"])
515
+ lines.append(
516
+ f"### {a.get('display_name', '?')}{warn}\n\n"
517
+ f"**Proposed action:** `{a.get('proposed_action', 'β€”')}`\n\n"
518
+ f"{a.get('position_text', '')}\n"
519
+ )
520
+ lines.append("\n## Cross-examination\n")
521
+ for c in data.get("cross_examination") or []:
522
+ xf = ""
523
+ if c.get("hallucination_flags"):
524
+ xf = "\n\n⚠️ " + " · ".join(c["hallucination_flags"])
525
+ lines.append(
526
+ f"**{c.get('examiner_name')}** β†’ **{c.get('target_name')}**{xf}\n\n"
527
+ f"{c.get('text', '')}\n\n---\n"
528
+ )
529
+ return "\n".join(lines)
530
+
531
+
532
+ def _war_room_handler(
533
+ threat_type: str,
534
+ severity: float,
535
+ source_ip: str,
536
+ target_service: str,
537
+ description: str,
538
+ preference: str,
539
+ ) -> str:
540
+ tt = (threat_type or "").strip()
541
+ sip = (source_ip or "").strip()
542
+ tgt = (target_service or "").strip()
543
+ desc = (description or "").strip()
544
+ if not tt or not sip or not tgt or not desc:
545
+ return "**Fill threat type, source IP, target service, and description.**"
546
+ pref = (preference or "").strip() or None
547
+ try:
548
+ sev = int(severity)
549
+ except (TypeError, ValueError):
550
+ sev = 5
551
+ sev = max(1, min(10, sev))
552
+ try:
553
+ data = asyncio.run(
554
+ run_war_room_debate(
555
+ threat_type=tt,
556
+ severity=sev,
557
+ source_ip=sip,
558
+ target_service=tgt,
559
+ description=desc,
560
+ preference_injection=pref,
561
+ )
562
+ )
563
+ except RuntimeError as e:
564
+ return f"**Configuration error:** {e}"
565
+ except Exception as e:
566
+ return f"**Error:** `{type(e).__name__}: {e}`"
567
+ return _format_war_room_markdown(data)
568
+
569
+
570
+ # ─── Build the UI ──────────────────────────────────────────────────────────
571
+
572
+
573
+ def build_demo() -> gr.Blocks:
574
+ table_headers = [
575
+ "step", "phase", "type", "action", "target", "reward", "threats",
576
+ "pipeline", "mesh gate", "WR turns", "honeypots", "migr phase",
577
+ "migr %", "honeytokens", "attack vec", "directive", "reasoning",
578
+ ]
579
+
580
+ with gr.Blocks(title="ImmunoOrg 2.0 β€” Live Demo", analytics_enabled=False) as demo:
581
+ gr.Markdown(
582
+ """
583
+ # πŸ›‘οΈ ImmunoOrg 2.0 β€” Live Demo
584
+
585
+ The agent has to defend an enterprise from a cyber-attack **and**
586
+ restructure the organization that lets the attack succeed in the first
587
+ place. Pick one of the 5 scenario families and watch the heuristic
588
+ baseline play it head-to-head against the GRPO-trained LLM defender.
589
+
590
+ **What the extra columns show (backend features, live from the sim):**
591
+
592
+ | Column | Feature in codebase |
593
+ | --- | --- |
594
+ | **pipeline / mesh gate** | 4-gate **DevSecOps Mesh** (`devsecops_mesh.py`): AST β†’ semantic β†’ Terraform β†’ sandbox; gate shows which layer flagged a payload. |
595
+ | **WR turns** | **War Room** multi-agent debate rounds toward consensus (`war_room.py`). |
596
+ | **honeypots / migr / honeytokens** | **50-step polymorphic migration** (`migration_engine.py`): decoy phase, honeypot nodes, honeytoken activations β€” *not* a separate β€œhoneycomb” UI; honeypots are tactical decoys here. |
597
+ | **attack vec** | Active attack vector (feeds **MITRE** / kill-chain context in the full env). |
598
+ | **directive** | Board directive text when the scenario injects one. |
599
+
600
+ **Charts below:** interactive **Plotly** dashboards β€” pipeline/decoys/War Room time series, plus **mesh gate** catch counts.
601
+
602
+ > πŸ“š [Problem statement](https://github.com/Charannoo/immunoorg/blob/master/PROBLEM_STATEMENT.md)
603
+ > Β· [Source](https://github.com/Charannoo/immunoorg)
604
+ > Β· [Blog](https://github.com/Charannoo/immunoorg/blob/master/BLOG_POST.md)
605
+ > Β· [Training notebook](https://github.com/Charannoo/immunoorg/blob/master/ImmunoOrg_Training_Colab.ipynb)
606
+ """
607
+ )
608
+
609
+ with gr.Accordion(
610
+ "🎭 Live LLM War Room β€” 3-agent debate (CISO / DevOps / Architect)",
611
+ open=True,
612
+ ):
613
+ gr.Markdown(
614
+ "Same page as the episode demo. Runs **initial positions** + **cross-examination** "
615
+ "via your configured LLM API (**GROQ_API_KEY**, **OPENAI_API_KEY**, or "
616
+ "**ANTHROPIC_API_KEY** in Space secrets). Optional: `POST /api/war-room` for scripts."
617
+ )
618
+ with gr.Row():
619
+ wr_threat = gr.Textbox(
620
+ label="Threat type",
621
+ placeholder="e.g. SQL injection probe",
622
+ )
623
+ wr_sev = gr.Slider(
624
+ minimum=1,
625
+ maximum=10,
626
+ value=5,
627
+ step=1,
628
+ label="Severity (1–10)",
629
+ )
630
+ with gr.Row():
631
+ wr_ip = gr.Textbox(label="Source IP", placeholder="203.0.113.42")
632
+ wr_tgt = gr.Textbox(label="Target service", placeholder="api-payments")
633
+ wr_desc = gr.Textbox(
634
+ label="Description",
635
+ lines=3,
636
+ placeholder="What was observed…",
637
+ )
638
+ wr_pref = gr.Textbox(
639
+ label="Preference injection (optional board directive)",
640
+ placeholder="Breaks deadlock β†’ Architect wins",
641
+ )
642
+ wr_btn = gr.Button("Run War Room debate", variant="secondary")
643
+ wr_out = gr.Markdown("*Results appear here after you run the debate.*")
644
+ wr_btn.click(
645
+ _war_room_handler,
646
+ inputs=[
647
+ wr_threat,
648
+ wr_sev,
649
+ wr_ip,
650
+ wr_tgt,
651
+ wr_desc,
652
+ wr_pref,
653
+ ],
654
+ outputs=[wr_out],
655
+ )
656
+
657
+ status_md = gr.Markdown(_trained_status_text())
658
+
659
+ with gr.Row():
660
+ scenario_dd = gr.Dropdown(
661
+ choices=list(_SCENARIO_LABEL.values()),
662
+ value=list(_SCENARIO_LABEL.values())[1],
663
+ label="Scenario family",
664
+ )
665
+ steps_sl = gr.Slider(5, 30, value=15, step=1, label="Max steps per episode")
666
+ mig_cb = gr.Checkbox(
667
+ value=True,
668
+ label="Demo: run START_MIGRATION + honeypot beats (shows decoys/honeytokens)",
669
+ )
670
+ run_btn = gr.Button("Run episode", variant="primary")
671
+
672
+ summary_md = gr.Markdown()
673
+
674
+ with gr.Row():
675
+ with gr.Column():
676
+ gr.Markdown("### Heuristic baseline")
677
+ heur_table = gr.Dataframe(headers=table_headers, wrap=True)
678
+ with gr.Column():
679
+ gr.Markdown("### Trained LLM (or heuristic fallback)")
680
+ trained_table = gr.Dataframe(headers=table_headers, wrap=True)
681
+
682
+ chart = gr.LinePlot(
683
+ x="step", y="heuristic",
684
+ title="Per-step reward (heuristic = orange, trained = blue)",
685
+ height=260,
686
+ )
687
+
688
+ gr.Markdown("### Feature dashboards (Plotly β€” zoom/pan/hover)")
689
+ with gr.Row():
690
+ signals_plot = gr.Plot(label="Pipeline, honeypots, honeytokens, War Room")
691
+ gate_plot = gr.Plot(label="Which mesh gate fired (AST / semantic / Terraform / sandbox)")
692
+
693
+ gr.Markdown(
694
+ """
695
+ ---
696
+
697
+ ### What the agent is reasoning about
698
+
699
+ - 28 actions across 3 categories: **tactical** (block_port, isolate_node, deploy_patch, **deploy_honeypot**, start_migration…),
700
+ **strategic** (merge_departments, reduce_bureaucracy, establish_devsecops…),
701
+ **diagnostic** (correlate_failure, identify_silo, vulnerability_scan…).
702
+ - 5-track composable reward:
703
+ uptime (25%) Β· threat neutralization (25%) Β· bureaucracy efficiency (20%) Β·
704
+ code-patch quality (20%) Β· pipeline integrity (10%) β€” pipeline ties to **mesh** columns.
705
+ - Trained on the elite 20/20/20/20/20 mix of scenario families
706
+ (basic / RAG / executive / silo / stealth) with TRL GRPO + Unsloth.
707
+
708
+ Uncheck **Demo: migration + honeypot** for a β€œpure” heuristic/LLM comparison without injected migration steps.
709
+ """
710
+ )
711
+
712
+ run_btn.click(
713
+ run_demo,
714
+ inputs=[scenario_dd, steps_sl, mig_cb],
715
+ outputs=[
716
+ summary_md, heur_table, trained_table, chart,
717
+ signals_plot, gate_plot, status_md,
718
+ ],
719
+ )
720
+
721
+ return demo
722
+
723
+
724
+ __all__ = ["build_demo"]