\u2588\u2588 SENTINELOPS ARENA \u2588\u2588
EPISODE REPLAY \u2502 MULTI-AGENT SECURITY SIMULATION
\u250c\u2500 RED TEAM vs BLUE TEAM vs AUDITOR \u2500\u2510
"""
for agent_key, label in [("attacker", "RED TEAM"), ("worker", "BLUE TEAM"), ("oversight", "AUDITOR")]:
color = AGENT_COLORS[agent_key]
icon = AGENT_ICONS[agent_key]
html += f"""
{icon} {label}
"""
html += """
"""
# --- Render log entries grouped by tick ---
current_tick = -1
current_phase = None
for entry in log:
tick = entry["tick"]
phase_name, phase_color, phase_icon, phase_desc = get_phase(tick)
# Phase header when phase changes
if phase_name != current_phase:
current_phase = phase_name
# Determine banner style
if phase_name == "RECONNAISSANCE PHASE":
banner_bg = "linear-gradient(90deg, rgba(0,255,65,0.12), rgba(0,255,65,0.03))"
border_col = "#00ff4155"
elif phase_name == "SCHEMA DRIFT ATTACK":
banner_bg = "linear-gradient(90deg, rgba(255,42,42,0.15), rgba(255,42,42,0.03))"
border_col = "#ff2a2a66"
elif phase_name == "POLICY DRIFT ATTACK":
banner_bg = "linear-gradient(90deg, rgba(255,140,0,0.15), rgba(255,140,0,0.03))"
border_col = "#ff8c0066"
elif phase_name == "SOCIAL ENGINEERING":
banner_bg = "linear-gradient(90deg, rgba(191,95,255,0.15), rgba(191,95,255,0.03))"
border_col = "#bf5fff66"
elif phase_name == "RATE LIMITING":
banner_bg = "linear-gradient(90deg, rgba(255,215,0,0.15), rgba(255,215,0,0.03))"
border_col = "#ffd70066"
else:
banner_bg = "rgba(50,50,50,0.3)"
border_col = "#333"
html += f"""
{phase_icon}
{phase_name}
{phase_desc}
TICKS {next((f'{p[0]:02d}-{p[1]:02d}' for p in PHASES if p[2] == phase_name), '??-??')}
"""
# Tick divider
if tick != current_tick:
current_tick = tick
html += f"""
"""
agent = entry["agent"]
color = AGENT_COLORS.get(agent, "#888")
icon = AGENT_ICONS.get(agent, "\u2022")
bg = AGENT_BG.get(agent, "rgba(50,50,50,0.1)")
reward = entry["reward"]
action_type = entry["action_type"]
is_flagged = entry.get("flag", False)
# Detect special events
is_attack_launch = action_type == "launch_attack"
is_recovery = action_type in ("get_schema", "get_current_policy")
# Full-width attack launch banner
if is_attack_launch:
details_str = str(entry.get("details", ""))
# Determine attack type from details
if "schema_drift" in details_str:
atk_label = "SCHEMA DRIFT"
atk_icon = "\u26a0"
atk_color = "#ff2a2a"
atk_bg = "rgba(255,42,42,0.12)"
elif "policy_drift" in details_str:
atk_label = "POLICY DRIFT"
atk_icon = "\u2622"
atk_color = "#ff8c00"
atk_bg = "rgba(255,140,0,0.12)"
elif "social_engineering" in details_str:
atk_label = "SOCIAL ENGINEERING"
atk_icon = "\u2620"
atk_color = "#bf5fff"
atk_bg = "rgba(191,95,255,0.12)"
elif "rate_limit" in details_str:
atk_label = "RATE LIMIT"
atk_icon = "\u26a1"
atk_color = "#ffd700"
atk_bg = "rgba(255,215,0,0.12)"
else:
atk_label = "UNKNOWN ATTACK"
atk_icon = "\u2753"
atk_color = "#ff4444"
atk_bg = "rgba(255,68,68,0.12)"
html += f"""
{atk_icon}
ATTACK LAUNCHED: {atk_label}
{_esc(str(entry.get('details', ''))[:100])}
{icon} RED TEAM \u2502 Reward: {reward:.1f}
"""
continue
# Regular action card
# Build badges
badges = ""
if is_recovery:
badges += f"""
\u2714 RECOVERY"""
if is_flagged:
badges += f"""
\u26a0 FLAGGED"""
# Reward badge color
if reward > 0:
reward_color = "#00ff41"
reward_bg = "rgba(0,255,65,0.1)"
elif reward < 0:
reward_color = "#ff4444"
reward_bg = "rgba(255,68,68,0.1)"
else:
reward_color = "#484f58"
reward_bg = "rgba(72,79,88,0.1)"
reward_str = f"+{reward:.1f}" if reward > 0 else f"{reward:.1f}"
html += f"""
{icon}
{entry['agent_label']}
\u25b8
{action_type}
{badges}
"""
details = entry.get("details", "")
if details:
html += f"""
{_esc(str(details)[:150])}
"""
explanation = entry.get("explanation", "")
if explanation:
exp_color = "#ff4444" if is_flagged else "#00ff41"
exp_icon = "\u26a0" if is_flagged else "\u2714"
html += f"""
{exp_icon} {_esc(explanation)}
"""
html += f"""
{reward_str}
"""
# --- Final Scores ---
html += """
\u2588 FINAL SCORES \u2588
"""
max_score = max(abs(s) for s in scores.values()) if scores else 1
if max_score == 0:
max_score = 1
for agent_key, score in scores.items():
color = AGENT_COLORS.get(agent_key, "#888")
icon = AGENT_ICONS.get(agent_key, "\u2022")
label_map = {"attacker": "RED TEAM", "worker": "BLUE TEAM", "oversight": "AUDITOR"}
label = label_map.get(agent_key, agent_key.upper())
# Bar width as percentage (handle negative scores)
bar_pct = max(0, min((score / max_score) * 100, 100)) if score > 0 else 0
# Score display color
if score > 0:
score_display_color = "#00ff41"
elif score < 0:
score_display_color = "#ff4444"
else:
score_display_color = "#484f58"
html += f"""
{icon}
{label}
{score:.1f}
"""
html += """
SENTINELOPS ARENA \u2502 OPENENV HACKATHON SF 2026