"""ST-WebAgentBench Leaderboard — HuggingFace Spaces Gradio App.
Displays benchmark results with:
- Main leaderboard table ranked by CuP
- Safety radar chart and risk heatmap
- Performance-Safety Frontier (Pareto front)
- Tier difficulty analysis
- Submission upload with 5-layer verification
"""
import base64
import fcntl
import hashlib
import hmac as _hmac
import json
import logging
import os
import re
import secrets
import tempfile
import time as _time
import traceback
from collections import Counter
from datetime import datetime, timezone
from enum import Enum
from pathlib import Path
from typing import List, Optional
import gradio as gr
from gradio.themes.utils import colors, fonts, sizes
from huggingface_hub import CommitScheduler, HfApi
import pandas as pd
import plotly.graph_objects as go
from validation.schema import (
Submission,
SAFETY_DIMENSIONS,
DIMENSION_DISPLAY,
EXPECTED_TASK_COUNT,
EXPECTED_POLICY_COUNT,
WEB_APPLICATIONS,
TIER_CONFIG,
)
from validation.validate import (
validate_submission,
recompute_metrics_from_evidence,
detect_anomalies,
validate_anti_gaming,
is_safe_string,
)
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# IBM logo embedded as base64 data URI (avoids LFS/Xet issues on HF Spaces)
# ---------------------------------------------------------------------------
_IBM_LOGO_B64 = (
"data:image/png;base64,"
"iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAB+6ElEQVR42u39e7xmWVXfC//GmOvZ"
"t9q169JdfQeqaRuFRgGJXERsBBUwapL32CQ5x5yjxwjHN+qbE43vOcfP+wIxiR/zJiaagx5bTTRG"
"TSgNR4MSQUN3BJu7iDTQdAPd0PfqrsuuXbX3fp61xu/9Y8651tpVu6pvVV0F9ft+KLpq7+ey5lxz"
"zTnmGGOOHyCEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEII"
"IYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGE"
"EEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBC"
"CCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQggh"
"hBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQ"
"QgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEII"
"IYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGE"
"EEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBC"
"CCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQggh"
"hBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQ"
"QgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEII"
"IYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGE"
"EEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBC"
"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"
"CCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQggh"
"hBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQ"
"QgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEII"
"IYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGE"
"EEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBC"
"CCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQggh"
"hBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQ"
"QgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEII"
"IYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGE"
"EEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBC"
"CCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCiCfJ/x8OjrE3jwy2QQAAAABJRU5ErkJggg=="
)
def _get_admin_password() -> str:
"""Read admin password at call time (not import time) so Space picks up secret changes."""
return os.environ.get("ADMIN_PASSWORD", "")
# ---------------------------------------------------------------------------
# Admin security: timing-safe comparison, rate limiting, sessions, audit
# ---------------------------------------------------------------------------
def _verify_admin_password(password: str) -> bool:
"""Constant-time password comparison to prevent timing attacks."""
admin_pw = _get_admin_password()
if not admin_pw or not password:
return False
return _hmac.compare_digest(password.encode("utf-8"), admin_pw.encode("utf-8"))
# Rate limiting — in-memory, resets on Space restart (acceptable).
_ADMIN_FAIL_LOG: list[float] = []
_ADMIN_MAX_FAILS = 5
_ADMIN_WINDOW_SECS = 300 # 5-minute sliding window
_ADMIN_LOCKOUT_SECS = 600 # 10-minute lockout after exceeding
def _check_rate_limit() -> str | None:
"""Return an error message if rate-limited, else None."""
now = _time.time()
# Prune old entries outside the lockout window
cutoff = now - _ADMIN_LOCKOUT_SECS
while _ADMIN_FAIL_LOG and _ADMIN_FAIL_LOG[0] < cutoff:
_ADMIN_FAIL_LOG.pop(0)
# Count failures in the sliding window
recent = [t for t in _ADMIN_FAIL_LOG if t > now - _ADMIN_WINDOW_SECS]
if len(recent) >= _ADMIN_MAX_FAILS:
last_fail = max(_ADMIN_FAIL_LOG)
unlock_at = last_fail + _ADMIN_LOCKOUT_SECS
remaining = int(unlock_at - now)
if remaining > 0:
return f"Too many failed attempts. Try again in {remaining} seconds."
return None
def _record_failed_attempt() -> None:
_ADMIN_FAIL_LOG.append(_time.time())
# Session management — in-memory tokens, 1-hour TTL.
_ADMIN_SESSIONS: dict[str, float] = {}
_SESSION_TTL = 3600 # 1 hour
def _create_admin_session() -> str:
"""Generate a session token and store it with an expiry."""
token = secrets.token_hex(32)
_ADMIN_SESSIONS[token] = _time.time() + _SESSION_TTL
# Prune expired sessions
now = _time.time()
expired = [k for k, v in _ADMIN_SESSIONS.items() if v < now]
for k in expired:
del _ADMIN_SESSIONS[k]
return token
def _verify_session(token: str) -> bool:
"""Check if a session token is valid and not expired."""
if not token or token not in _ADMIN_SESSIONS:
return False
if _time.time() > _ADMIN_SESSIONS[token]:
del _ADMIN_SESSIONS[token]
return False
return True
# Audit logging — append-only JSONL.
ADMIN_AUDIT_FILE = Path("data/admin_audit.jsonl")
def _log_admin_action(action: str, details: str) -> None:
"""Append an admin action to the audit log."""
ADMIN_AUDIT_FILE.parent.mkdir(parents=True, exist_ok=True)
record = {
"action": action,
"details": details,
"timestamp": datetime.now(timezone.utc).isoformat(),
}
with open(ADMIN_AUDIT_FILE, "a") as f:
f.write(json.dumps(record) + "\n")
# Master secret env var name — used to derive per-user signing keys.
# Set as HF Space secret — never exposed publicly.
_MASTER_KEY_ENV = "ST_BENCH_MASTER_KEY"
def _get_master_key() -> str:
"""Read the master key at call time (not import time) for testability."""
return os.environ.get(_MASTER_KEY_ENV, "")
# Email validation pattern
_EMAIL_RE = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
_APP_DIR = Path(__file__).resolve().parent
SUBMISSIONS_FILE = _APP_DIR / "data" / "submissions.jsonl"
KEY_REQUESTS_FILE = _APP_DIR / "data" / "key_requests.jsonl"
TASKS_FILE = _APP_DIR / "data" / "test.raw.json"
CANONICAL_HASHES_FILE = _APP_DIR / "data" / "canonical_hashes.json"
ADMIN_AUDIT_FILE = _APP_DIR / "data" / "admin_audit.jsonl" # override relative path above
# ---------------------------------------------------------------------------
# Data persistence — CommitScheduler auto-syncs data/ to HF dataset repo
# ---------------------------------------------------------------------------
_DATA_REPO_ID = "ST-WebAgentBench/st-webagentbench-data"
_DATA_DIR = _APP_DIR / "data"
_scheduler: CommitScheduler | None = None
_PERSISTENCE_ENABLED = False
def _init_persistence() -> bool:
"""Initialize CommitScheduler for data persistence. Returns True if enabled."""
global _scheduler
_DATA_DIR.mkdir(parents=True, exist_ok=True)
token = os.environ.get("HF_TOKEN")
if not token:
logger.warning("No HF token found — data persistence disabled (HF_TOKEN not in env)")
return False
logger.warning("HF_TOKEN found, initializing persistence...")
api = HfApi(token=token)
try:
# Download existing data files from the repo before starting the scheduler
for filename in ["submissions.jsonl", "key_requests.jsonl", "admin_audit.jsonl"]:
local = _DATA_DIR / filename
if not local.exists() or local.stat().st_size == 0:
try:
api.hf_hub_download(
repo_id=_DATA_REPO_ID,
repo_type="dataset",
filename=filename,
local_dir=str(_DATA_DIR),
)
logger.info("Restored %s from data repo", filename)
except Exception:
logger.info("No existing %s in data repo (first run?)", filename)
# Start the scheduler — auto-commits data/ every 2 minutes
_scheduler = CommitScheduler(
repo_id=_DATA_REPO_ID,
folder_path=_DATA_DIR,
every=2,
repo_type="dataset",
private=True,
allow_patterns=["*.jsonl"],
squash_history=True,
hf_api=api,
)
logger.warning(
"CommitScheduler started — persisting to %s every 2 min",
_DATA_REPO_ID,
)
return True
except Exception:
logger.error("Failed to initialize persistence", exc_info=True)
return False
# Load canonical task definitions for validation
_TASKS_DATA = None
_CANONICAL_HASHES = None
def _load_tasks_data():
global _TASKS_DATA
if _TASKS_DATA is None and TASKS_FILE.exists():
with open(TASKS_FILE) as f:
_TASKS_DATA = json.load(f)
return _TASKS_DATA
def _load_canonical_hashes():
"""Load canonical code hashes, preferring the env-var source.
Priority:
1. CANONICAL_HASHES env var (JSON string) — keeps hashes private
2. data/canonical_hashes.json file — fallback for local development
"""
global _CANONICAL_HASHES
if _CANONICAL_HASHES is not None:
return _CANONICAL_HASHES
# Try env var first (set as HF Space secret)
env_hashes = os.environ.get("CANONICAL_HASHES", "").strip()
if env_hashes:
try:
parsed = json.loads(env_hashes)
# Support both {"1.0.0": {...}} and flat {...} formats
if "1.0.0" in parsed:
_CANONICAL_HASHES = parsed["1.0.0"]
else:
_CANONICAL_HASHES = parsed
logger.info("Loaded canonical hashes from environment variable")
return _CANONICAL_HASHES
except json.JSONDecodeError:
logger.warning("Failed to parse CANONICAL_HASHES env var")
# Fallback to file
if CANONICAL_HASHES_FILE.exists():
with open(CANONICAL_HASHES_FILE) as f:
all_hashes = json.load(f)
_CANONICAL_HASHES = all_hashes.get("1.0.0", {})
logger.info("Loaded canonical hashes from file")
return _CANONICAL_HASHES
# ---------------------------------------------------------------------------
# Per-user signing key management
# ---------------------------------------------------------------------------
def derive_user_key(email: str) -> str:
"""Derive a per-user signing key from the master secret and email.
key = HMAC-SHA256(master_key, normalised_email)
"""
master = _get_master_key()
normalised = email.strip().lower()
return _hmac.new(
master.encode("utf-8"),
normalised.encode("utf-8"),
hashlib.sha256,
).hexdigest()
def _log_key_request(email: str, team: str, institution: str) -> None:
"""Append a key-request record to the log (admin-only visibility)."""
KEY_REQUESTS_FILE.parent.mkdir(parents=True, exist_ok=True)
record = {
"email": email.strip().lower(),
"team": team.strip(),
"institution": institution.strip(),
"timestamp": datetime.now(timezone.utc).isoformat(),
}
with open(KEY_REQUESTS_FILE, "a") as f:
f.write(json.dumps(record) + "\n")
def _load_key_requests() -> list[dict]:
"""Load all key-request records."""
if not KEY_REQUESTS_FILE.exists():
return []
records = []
for line in KEY_REQUESTS_FILE.read_text().strip().split("\n"):
if line.strip():
try:
records.append(json.loads(line))
except json.JSONDecodeError:
continue
return records
def handle_key_request(email: str, team: str, institution: str) -> str:
"""Validate inputs, derive the user key, log the request, return the key."""
if not _get_master_key():
return "ERROR: Key generation is not configured on this Space. Contact the maintainers."
email = (email or "").strip()
team = (team or "").strip()
institution = (institution or "").strip()
if not email:
return "Please enter your email address."
if not _EMAIL_RE.match(email):
return f"Invalid email address: {email}"
if not team:
return "Please enter your team name."
if not is_safe_string(team, max_length=256):
return "Team name contains disallowed characters."
if institution and not is_safe_string(institution, max_length=256):
return "Institution contains disallowed characters."
user_key = derive_user_key(email)
_log_key_request(email, team, institution)
return (
f"Your signing key (set this as an environment variable before running the benchmark):\n\n"
f"export ST_BENCH_SIGNING_KEY=\"{user_key}\"\n\n"
f"IMPORTANT: Use the same email ({email}) as --contact-email when generating your submission."
)
RISK_COLORS = {"low": "#22c55e", "medium": "#eab308", "high": "#ef4444"}
# ---------------------------------------------------------------------------
# UI Design Constants
# ---------------------------------------------------------------------------
CUSTOM_CSS = """
/* === Global === */
.gradio-container {
max-width: 1200px !important;
margin: 0 auto !important;
}
/* === Hero Header === */
#hero-header {
background: linear-gradient(135deg, #1e3a8a 0%, #312e81 50%, #1e293b 100%);
border-radius: 16px;
padding: 40px 48px 32px;
margin-bottom: 8px;
position: relative;
overflow: hidden;
}
#hero-header::before {
content: '';
position: absolute;
top: -50%;
right: -20%;
width: 500px;
height: 500px;
background: radial-gradient(circle, rgba(99, 102, 241, 0.15) 0%, transparent 70%);
pointer-events: none;
}
#hero-header h1 {
color: white;
font-size: 2rem;
font-weight: 700;
margin: 0 0 6px 0;
letter-spacing: -0.02em;
}
#hero-header .subtitle {
color: #cbd5e1;
font-size: 1.05rem;
margin: 0 0 16px 0;
font-weight: 400;
}
#hero-header .iclr-badge {
display: inline-block;
background: linear-gradient(135deg, #6366f1, #818cf8);
color: white;
font-size: 0.75rem;
font-weight: 600;
padding: 3px 10px;
border-radius: 9999px;
letter-spacing: 0.03em;
vertical-align: middle;
margin-left: 8px;
}
#hero-header .nav-links {
margin-top: 12px;
display: flex;
gap: 20px;
flex-wrap: wrap;
}
#hero-header .nav-links a {
color: #93c5fd;
text-decoration: none;
font-size: 0.9rem;
font-weight: 500;
transition: color 0.15s ease;
display: inline-flex;
align-items: center;
gap: 4px;
}
#hero-header .nav-links a:hover {
color: white;
}
#hero-header .stats-strip {
display: flex;
gap: 32px;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid rgba(255,255,255,0.1);
flex-wrap: wrap;
}
#hero-header .stat-item {
text-align: left;
}
#hero-header .stat-value {
color: white;
font-size: 1.5rem;
font-weight: 700;
line-height: 1.2;
}
#hero-header .stat-label {
color: #94a3b8;
font-size: 0.78rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
}
#hero-header .logo-row {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 12px;
}
#hero-header .logo-row img {
height: 64px;
filter: brightness(0) invert(1);
opacity: 0.9;
}
/* === Tabs === */
.tabs > .tab-nav {
border-bottom: 2px solid #e2e8f0 !important;
gap: 0 !important;
padding: 0 4px !important;
background: transparent !important;
}
.tabs > .tab-nav > button {
border: none !important;
border-bottom: 2px solid transparent !important;
margin-bottom: -2px !important;
padding: 10px 18px !important;
font-weight: 500 !important;
font-size: 0.9rem !important;
color: #64748b !important;
background: transparent !important;
transition: color 0.15s ease, border-color 0.15s ease !important;
border-radius: 0 !important;
box-shadow: none !important;
}
.tabs > .tab-nav > button:hover {
color: #1e293b !important;
background: transparent !important;
}
.tabs > .tab-nav > button.selected {
color: #2563eb !important;
border-bottom-color: #2563eb !important;
font-weight: 600 !important;
background: transparent !important;
}
/* === Tables (Dataframe) === */
/* Container styling */
.table-wrap {
border-radius: 12px !important;
border: 1px solid #e2e8f0 !important;
overflow: hidden !important;
}
/* Ensure last row has a visible bottom border for table closure */
table tbody tr:last-child td {
border-bottom: 2px solid #e2e8f0 !important;
}
/* Override Gradio 6 internal: force nowrap on header text */
.header-content {
white-space: nowrap !important;
overflow-wrap: normal !important;
word-break: normal !important;
}
/* Override Gradio 6 internal: use auto layout instead of fixed */
table :is(thead, tfoot, tbody) {
table-layout: auto !important;
}
/* Header cell styling */
table thead th {
background: #f1f5f9 !important;
color: #334155 !important;
font-weight: 600 !important;
font-size: 0.82rem !important;
text-transform: uppercase !important;
letter-spacing: 0.04em !important;
border-bottom: 2px solid #e2e8f0 !important;
}
/* Data cell styling */
table tbody td {
font-size: 0.88rem !important;
border-bottom: 1px solid #f1f5f9 !important;
}
/* Row hover */
table tbody tr:hover {
background: #eff6ff !important;
}
/* === Accordion (FAQ) === */
.faq-section .accordion {
border: 1px solid #e2e8f0 !important;
border-radius: 10px !important;
margin-bottom: 8px !important;
overflow: hidden !important;
box-shadow: none !important;
}
.faq-section .accordion > .label-wrap {
padding: 14px 18px !important;
background: white !important;
}
.faq-section .accordion > .label-wrap:hover {
background: #f8fafc !important;
}
.faq-section .accordion .prose {
padding: 4px 18px 18px !important;
color: #475569 !important;
line-height: 1.65 !important;
}
.faq-section h3 {
color: #1e293b !important;
font-size: 1.05rem !important;
font-weight: 600 !important;
margin-top: 28px !important;
margin-bottom: 12px !important;
padding-bottom: 6px !important;
border-bottom: 1px solid #e2e8f0 !important;
}
/* === Form Cards === */
.form-card {
background: white !important;
border: 1px solid #e2e8f0 !important;
border-radius: 12px !important;
padding: 24px !important;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.04) !important;
}
/* === Filter Row === */
/* === Filter Row === */
.filter-row {
background: #f8fafc !important;
border: 1px solid #e2e8f0 !important;
border-radius: 12px !important;
padding: 16px 20px !important;
margin-bottom: 16px !important;
display: flex !important;
align-items: end !important;
gap: 16px !important;
}
.filter-row > div {
flex: 1 !important;
min-width: 0 !important;
}
.filter-row .wrap {
gap: 4px !important;
}
/* === Responsive === */
@media (max-width: 768px) {
#hero-header {
padding: 28px 24px 24px;
}
#hero-header h1 {
font-size: 1.5rem;
}
#hero-header .stats-strip {
gap: 20px;
}
#hero-header .stat-value {
font-size: 1.2rem;
}
.tabs > .tab-nav > button {
padding: 8px 12px !important;
font-size: 0.82rem !important;
}
}
"""
# --- Plotly Style Constants ---
PLOTLY_FONT = "Inter, system-ui, sans-serif"
PLOTLY_TEXT_COLOR = "#334155" # slate-700
PLOTLY_TITLE_COLOR = "#1e293b" # slate-800
PLOTLY_GRID_COLOR = "#e2e8f0" # slate-200
PLOTLY_COLORWAY = [
"#3b82f6", # blue-500
"#6366f1", # indigo-500
"#8b5cf6", # violet-500
"#06b6d4", # cyan-500
"#10b981", # emerald-500
"#f59e0b", # amber-500
]
def _plotly_layout(**overrides) -> dict:
"""Consistent Plotly layout kwargs."""
defaults = dict(
font=dict(family=PLOTLY_FONT, color=PLOTLY_TEXT_COLOR, size=13),
title_font=dict(family=PLOTLY_FONT, color=PLOTLY_TITLE_COLOR, size=16),
plot_bgcolor="rgba(0,0,0,0)",
paper_bgcolor="rgba(0,0,0,0)",
margin=dict(l=48, r=24, t=56, b=48),
legend=dict(
font=dict(size=12),
bgcolor="rgba(255,255,255,0.8)",
bordercolor="#e2e8f0",
borderwidth=1,
),
colorway=PLOTLY_COLORWAY,
)
defaults.update(overrides)
return defaults
def _empty_figure(message: str, height: int = 400) -> go.Figure:
"""Polished empty-state chart."""
fig = go.Figure()
fig.add_annotation(
text=f"{message}
"
f"Submit results to populate this chart",
showarrow=False,
xref="paper", yref="paper", x=0.5, y=0.5,
font=dict(size=16, color="#64748b", family=PLOTLY_FONT),
)
fig.update_layout(
**_plotly_layout(height=height),
xaxis=dict(visible=False),
yaxis=dict(visible=False),
)
return fig
# ---------------------------------------------------------------------------
# Submission status workflow
# ---------------------------------------------------------------------------
class SubmissionStatus(Enum):
SUBMITTED = "submitted"
VALIDATING = "validating"
VERIFIED = "verified"
FLAGGED = "flagged"
REJECTED = "rejected"
PUBLISHED = "published"
# ---------------------------------------------------------------------------
# Data loading
# ---------------------------------------------------------------------------
def load_submissions() -> list[dict]:
"""Load all submissions from the JSONL data file."""
if not SUBMISSIONS_FILE.exists():
return []
submissions = []
for line in SUBMISSIONS_FILE.read_text().strip().split("\n"):
if line.strip():
try:
submissions.append(json.loads(line))
except json.JSONDecodeError:
continue
return submissions
def save_submission(submission: dict) -> None:
"""Append a submission to the JSONL data file (with file locking).
Also saves an individual JSON file under data/submissions/{agent_id}/{run_id}.json
for organized storage in the private HF dataset repo.
"""
SUBMISSIONS_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(SUBMISSIONS_FILE, "a") as f:
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
try:
f.write(json.dumps(submission) + "\n")
f.flush()
finally:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
# Save individual submission file for organized storage
try:
agent_id = submission.get("metadata", {}).get("agent_id", "unknown")
run_id = submission.get("integrity", {}).get("run_id", "unknown")
# Sanitize for filesystem safety
safe_agent = re.sub(r"[^\w\-.]", "_", agent_id)[:64]
safe_run = re.sub(r"[^\w\-.]", "_", run_id)[:64]
sub_dir = _DATA_DIR / "submissions" / safe_agent
sub_dir.mkdir(parents=True, exist_ok=True)
sub_file = sub_dir / f"{safe_run}.json"
with open(sub_file, "w") as f:
json.dump(submission, f, indent=2)
logger.info("Saved submission file: %s", sub_file.relative_to(_DATA_DIR))
except Exception:
logger.warning("Failed to save individual submission file", exc_info=True)
# ---------------------------------------------------------------------------
# Dynamic tier description helper
# ---------------------------------------------------------------------------
def _build_tier_description() -> str:
"""Generate the Tiers tab description from TIER_CONFIG."""
if not TIER_CONFIG:
return "### Difficulty Tier Breakdown\n\nNo tier information available."
parts = ["### Difficulty Tier Breakdown\n"]
for group, tiers in TIER_CONFIG.items():
group_display = group.replace("_", " ").title()
total_ids = sum(len(ids) for ids in tiers.values())
all_ids = sorted(tid for ids in tiers.values() for tid in ids)
id_range = f"{min(all_ids)}-{max(all_ids)}" if all_ids else "N/A"
parts.append(
f"Tasks {id_range} are organized into {len(tiers)} difficulty tiers "
f"({group_display}):\n"
)
for tier_name in sorted(tiers.keys(), key=lambda t: {"easy": 0, "medium": 1, "hard": 2}.get(t, 99)):
ids = sorted(tiers[tier_name])
parts.append(f"- **{tier_name.capitalize()}** ({min(ids)}-{max(ids)}): {len(ids)} tasks")
parts.append("")
parts.append("**Drop-off%** measures how much CuP degrades from the easiest to hardest tier.")
return "\n".join(parts)
# ---------------------------------------------------------------------------
# Table builders
# ---------------------------------------------------------------------------
def build_main_table(submissions: list[dict], sort_by: str = "CuP",
model_filter: str = "All", open_only: bool = False,
verified_only: bool = False) -> pd.DataFrame:
"""Build the main leaderboard DataFrame."""
if not submissions:
return pd.DataFrame(columns=[
"Rank", "Agent", "Model", "Team", "CuP", "CR",
"Gap%", "semi-CuP", "Avg Risk", "Status", "Open", "Date",
])
rows = []
for s in submissions:
meta = s.get("metadata", {})
results = s.get("results", {})
metrics = results.get("metrics", {})
# Filter
if model_filter != "All":
if meta.get("model_family", "").lower() != model_filter.lower():
continue
if open_only and not meta.get("is_open_source"):
continue
status = s.get("status", "published")
if verified_only and status not in ("verified", "published", "admin_override"):
continue
def _safe_float(val, default=0.0):
"""Convert to float safely, returning default for missing/invalid."""
if val is None:
return default
try:
return float(val)
except (TypeError, ValueError):
return default
cr = _safe_float(metrics.get("CR"))
cup = _safe_float(metrics.get("CuP"))
gap = ((cup - cr) / cr * 100) if cr > 0 else 0.0
# Average risk from dimensions
dims = results.get("dimensions", [])
avg_risk = 0.0
if dims:
risk_values = [_safe_float(d.get("active_risk_ratio", 0)) for d in dims]
avg_risk = sum(risk_values) / len(risk_values) if risk_values else 0.0
semi_cup = _safe_float(metrics.get("semi_CuP"))
date_str = s.get("submission_date", "")[:10]
rows.append({
"Agent": meta.get("agent_id", "?"),
"Model": meta.get("model_name", "?"),
"Team": meta.get("team", "?"),
"CuP": round(cup, 3),
"CR": round(cr, 3),
"Gap%": round(gap, 1),
"semi-CuP": round(semi_cup, 3),
"Avg Risk": round(avg_risk, 3),
"Status": status.replace("_", " ").title() if isinstance(status, str) else "Published",
"Open": "Yes" if meta.get("is_open_source") else "No",
"Date": date_str if date_str else "--",
})
df = pd.DataFrame(rows)
if df.empty:
return df
# Sort
sort_map = {
"CuP": ("CuP", False),
"CR": ("CR", False),
"semi-CuP": ("semi-CuP", False),
"Risk Ratio": ("Avg Risk", True),
"Gap": ("Gap%", True),
"Date": ("Date", False),
}
col, ascending = sort_map.get(sort_by, ("CuP", False))
df = df.sort_values(col, ascending=ascending).reset_index(drop=True)
df.insert(0, "Rank", range(1, len(df) + 1))
return df
# ---------------------------------------------------------------------------
# Visualizations
# ---------------------------------------------------------------------------
def build_radar_chart(submissions: list[dict],
selected_agents: list[str]) -> go.Figure:
"""Build a radar chart comparing safety profiles of selected agents."""
fig = go.Figure()
if not selected_agents:
return _empty_figure("Select agents to compare", 500)
dim_labels = [DIMENSION_DISPLAY.get(d, d) for d in SAFETY_DIMENSIONS]
chart_colors = PLOTLY_COLORWAY[:4]
for i, agent_name in enumerate(selected_agents[:4]):
# Find submission
sub = None
for s in submissions:
if s.get("metadata", {}).get("agent_id") == agent_name:
sub = s
break
if not sub:
continue
dims = sub.get("results", {}).get("dimensions", [])
dim_map = {d["dimension"]: d for d in dims}
# Safety = 1 - active_risk_ratio (higher is safer)
values = [
1 - dim_map.get(d, {}).get("active_risk_ratio", 0)
for d in SAFETY_DIMENSIONS
]
# Close the polygon
values.append(values[0])
labels = dim_labels + [dim_labels[0]]
fig.add_trace(go.Scatterpolar(
r=values,
theta=labels,
fill="toself",
name=agent_name,
line=dict(color=chart_colors[i % len(chart_colors)]),
opacity=0.6,
))
fig.update_layout(**_plotly_layout(
title="Safety Dimension Radar (higher = safer)",
height=500,
polar=dict(
radialaxis=dict(visible=True, range=[0, 1], gridcolor=PLOTLY_GRID_COLOR),
angularaxis=dict(gridcolor=PLOTLY_GRID_COLOR),
bgcolor="rgba(0,0,0,0)",
),
))
return fig
def build_risk_heatmap(submissions: list[dict]) -> go.Figure:
"""Build a heatmap of risk ratios (agents x dimensions)."""
if not submissions:
return _empty_figure("No submissions yet")
agent_names = []
z_values = []
text_values = []
for s in submissions:
name = s.get("metadata", {}).get("agent_id", "?")
dims = s.get("results", {}).get("dimensions", [])
dim_map = {d["dimension"]: d for d in dims}
row = []
text_row = []
for d in SAFETY_DIMENSIONS:
rr = dim_map.get(d, {}).get("active_risk_ratio", 0)
tier = dim_map.get(d, {}).get("active_risk_tier", "unknown")
row.append(rr)
text_row.append(f"{rr:.3f} ({tier})")
agent_names.append(name)
z_values.append(row)
text_values.append(text_row)
dim_labels = [DIMENSION_DISPLAY.get(d, d) for d in SAFETY_DIMENSIONS]
fig = go.Figure(data=go.Heatmap(
z=z_values,
x=dim_labels,
y=agent_names,
text=text_values,
texttemplate="%{text}",
colorscale=[[0, "#22c55e"], [0.05, "#22c55e"],
[0.05, "#eab308"], [0.15, "#eab308"],
[0.15, "#ef4444"], [1.0, "#ef4444"]],
zmin=0,
zmax=0.5,
colorbar=dict(title="Risk Ratio"),
))
fig.update_layout(**_plotly_layout(
title="Risk Heatmap by Safety Dimension",
height=max(300, 60 * len(agent_names) + 100),
xaxis=dict(side="top", tickfont=dict(size=11)),
yaxis=dict(tickfont=dict(size=12)),
))
return fig
def build_pareto_frontier(submissions: list[dict]) -> go.Figure:
"""Build a CR vs CuP scatter plot with Pareto frontier."""
fig = go.Figure()
if not submissions:
return _empty_figure("No submissions yet", 550)
# Diagonal line (perfect safety: CuP = CR)
fig.add_trace(go.Scatter(
x=[0, 1], y=[0, 1],
mode="lines",
line=dict(color="#94a3b8", dash="dash", width=1),
name="Perfect Safety (CuP=CR)",
showlegend=True,
))
# Agent dots
crs, cups, names, teams, risks = [], [], [], [], []
for s in submissions:
meta = s.get("metadata", {})
metrics = s.get("results", {}).get("metrics", {})
dims = s.get("results", {}).get("dimensions", [])
avg_risk = sum(d.get("active_risk_ratio", 0) for d in dims) / max(len(dims), 1)
crs.append(metrics.get("CR", 0))
cups.append(metrics.get("CuP", 0))
names.append(meta.get("agent_id", "?"))
teams.append(meta.get("team", "?"))
risks.append(avg_risk)
# Color by risk level
dot_colors = []
for r in risks:
if r <= 0.05:
dot_colors.append("#22c55e")
elif r <= 0.15:
dot_colors.append("#eab308")
else:
dot_colors.append("#ef4444")
hover_text = [
f"{n}
Team: {t}
CR: {cr:.3f}
CuP: {cup:.3f}
"
f"Gap: {((cup-cr)/cr*100) if cr > 0 else 0:.1f}%
Avg Risk: {r:.3f}"
for n, t, cr, cup, r in zip(names, teams, crs, cups, risks)
]
fig.add_trace(go.Scatter(
x=crs,
y=cups,
mode="markers+text",
marker=dict(size=14, color=dot_colors, line=dict(width=1.5, color="white")),
text=names,
textposition="top center",
textfont=dict(size=10, family=PLOTLY_FONT),
hovertext=hover_text,
hoverinfo="text",
name="Agents",
))
# Compute and draw Pareto frontier
points = sorted(zip(crs, cups), key=lambda p: p[0])
pareto_x, pareto_y = [], []
max_cup = -1
for cr, cup in points:
if cup > max_cup:
pareto_x.append(cr)
pareto_y.append(cup)
max_cup = cup
if len(pareto_x) > 1:
fig.add_trace(go.Scatter(
x=pareto_x, y=pareto_y,
mode="lines",
line=dict(color="#4f46e5", width=2, dash="dot"),
name="Pareto Frontier",
))
fig.update_layout(**_plotly_layout(
title="Performance-Safety Frontier",
xaxis_title="CR (Completion Rate)",
yaxis_title="CuP (Completion under Policy)",
xaxis=dict(range=[-0.02, 1.02], gridcolor="#f1f5f9", zeroline=False),
yaxis=dict(range=[-0.02, 1.02], gridcolor="#f1f5f9", zeroline=False),
height=550,
legend=dict(x=0.02, y=0.98),
))
return fig
def build_tier_table(submissions: list[dict]) -> pd.DataFrame:
"""Build the tier analysis table."""
if not submissions:
return pd.DataFrame(columns=[
"Agent", "Easy-CuP", "Med-CuP", "Hard-CuP",
"Easy-CR", "Med-CR", "Hard-CR", "Drop-off%",
])
rows = []
for s in submissions:
meta = s.get("metadata", {})
tiers_list = s.get("results", {}).get("tiers", [])
if not tiers_list:
continue
tier_map = {t["tier"]: t for t in tiers_list}
easy = tier_map.get("easy", {})
medium = tier_map.get("medium", {})
hard = tier_map.get("hard", {})
easy_cup = easy.get("CuP", 0)
hard_cup = hard.get("CuP", 0)
dropoff = ((hard_cup - easy_cup) / easy_cup * 100) if easy_cup > 0 else 0
rows.append({
"Agent": meta.get("agent_id", "?"),
"Easy-CuP": round(easy_cup, 3),
"Med-CuP": round(medium.get("CuP", 0), 3),
"Hard-CuP": round(hard_cup, 3),
"Easy-CR": round(easy.get("CR", 0), 3),
"Med-CR": round(medium.get("CR", 0), 3),
"Hard-CR": round(hard.get("CR", 0), 3),
"Drop-off%": round(dropoff, 1),
})
return pd.DataFrame(rows)
_APP_DISPLAY = {
"gitlab": "GitLab",
"shopping_admin": "ShopAdmin",
"suitecrm": "SuiteCRM",
}
def build_app_table(submissions: list[dict]) -> pd.DataFrame:
"""Build the per-app breakdown table (flat: one row per agent+app)."""
if not submissions:
return pd.DataFrame(columns=[
"Agent", "App", "CuP", "CR", "semi-CuP", "Gap%", "Tasks",
])
rows = []
for s in submissions:
meta = s.get("metadata", {})
apps_list = s.get("results", {}).get("apps", [])
if not apps_list:
continue
agent_id = meta.get("agent_id", "?")
for app_data in apps_list:
app_key = app_data.get("app", "")
cr = app_data.get("CR", 0)
cup = app_data.get("CuP", 0)
semi_cup = app_data.get("semi_CuP", 0)
gap = ((cup - cr) / cr * 100) if cr > 0 else 0
rows.append({
"Agent": agent_id,
"App": _APP_DISPLAY.get(app_key, app_key),
"CuP": round(cup, 3),
"CR": round(cr, 3),
"semi-CuP": round(semi_cup, 3),
"Gap%": round(gap, 1),
"Tasks": app_data.get("task_count", 0),
})
return pd.DataFrame(rows)
# ---------------------------------------------------------------------------
# Submission validation (lightweight, for the UI)
# ---------------------------------------------------------------------------
def validate_upload_full(file) -> tuple[str, Optional[dict], str]:
"""Full 5-layer validation of an uploaded submission.
Returns (status: "verified"|"flagged"|"rejected",
parsed_data_or_None,
detailed_report_string).
"""
if file is None:
return "rejected", None, "No file uploaded."
# --- Layer 0: Parse JSON ---
# Handle both Gradio 4.x (object with .name) and 5.x (filepath string)
try:
file_path = file.name if hasattr(file, "name") else str(file)
file_size = os.path.getsize(file_path)
if file_size > 50_000_000:
return "rejected", None, f"REJECTED: File too large ({file_size / 1_000_000:.1f}MB). Maximum is 50MB."
if file_size == 0:
return "rejected", None, "REJECTED: Empty file uploaded."
with open(file_path, "r") as f:
data = json.load(f)
except json.JSONDecodeError as e:
return "rejected", None, f"REJECTED: Invalid JSON — {e}"
except Exception as e:
return "rejected", None, f"REJECTED: Could not read file — {e}"
report_lines = []
# --- Layer 1: Pydantic schema validation ---
try:
submission = Submission(**data)
report_lines.append("Schema validation: PASS")
except Exception as e:
return "rejected", None, f"REJECTED: Schema validation failed — {e}"
# --- Layer 2: Structural validation + integrity ---
try:
tasks_data = _load_tasks_data()
canonical_hashes = _load_canonical_hashes()
# Derive the expected per-user signing key from the submission's contact email
user_signing_key = None
if _get_master_key():
contact_email = (
submission.metadata.contact_email
if submission.metadata and submission.metadata.contact_email
else ""
)
if contact_email:
user_signing_key = derive_user_key(contact_email)
structural_errors = validate_submission(
submission,
tasks_data=tasks_data,
canonical_hashes=canonical_hashes,
signing_key=user_signing_key,
)
except Exception as e:
logger.exception("Structural validation crashed")
return "rejected", None, f"REJECTED: Internal error during structural validation — {e}"
hard_errors = [e for e in structural_errors
if "missing" in e.lower() or "mismatch" in e.lower()
or "impossible" in e.lower() or "unsafe" in e.lower()
or "invalid" in e.lower()]
soft_warnings = [e for e in structural_errors if e not in hard_errors]
if hard_errors:
report_lines.append(f"Structural validation: FAIL ({len(hard_errors)} errors)")
for err in hard_errors[:10]:
report_lines.append(f" ERROR: {err}")
if len(hard_errors) > 10:
report_lines.append(f" ... and {len(hard_errors) - 10} more errors")
if soft_warnings:
report_lines.append(f" + {len(soft_warnings)} warnings")
return "rejected", None, "REJECTED\n\n" + "\n".join(report_lines)
if soft_warnings:
report_lines.append(f"Structural validation: WARN ({len(soft_warnings)} warnings)")
for w in soft_warnings[:5]:
report_lines.append(f" WARN: {w}")
if len(soft_warnings) > 5:
report_lines.append(f" ... and {len(soft_warnings) - 5} more warnings")
else:
report_lines.append("Structural validation: PASS")
# --- Layer 3: Metric recomputation ---
try:
metric_discrepancies = recompute_metrics_from_evidence(submission)
except Exception as e:
logger.exception("Metric recomputation crashed")
return "rejected", None, f"REJECTED: Internal error during metric recomputation — {e}"
metric_errors = [d for d in metric_discrepancies if "mismatch" in d.lower()]
metric_warnings = [d for d in metric_discrepancies if d not in metric_errors]
if metric_errors:
report_lines.append(f"Metric recomputation: FAIL ({len(metric_errors)} discrepancies)")
for err in metric_errors[:5]:
report_lines.append(f" ERROR: {err}")
if len(metric_errors) > 5:
report_lines.append(f" ... and {len(metric_errors) - 5} more discrepancies")
return "rejected", None, "REJECTED\n\n" + "\n".join(report_lines)
if metric_warnings:
report_lines.append(f"Metric recomputation: WARN ({len(metric_warnings)} issues)")
else:
report_lines.append("Metric recomputation: PASS")
# --- Layer 4: Statistical anomaly detection ---
try:
anomaly_flags = detect_anomalies(submission)
except Exception as e:
logger.exception("Anomaly detection crashed")
anomaly_flags = [f"Anomaly detection error: {e}"]
if anomaly_flags:
report_lines.append(f"Anomaly detection: {len(anomaly_flags)} flag(s)")
for flag in anomaly_flags[:5]:
report_lines.append(f" FLAG: {flag}")
if len(anomaly_flags) > 5:
report_lines.append(f" ... and {len(anomaly_flags) - 5} more flags")
else:
report_lines.append("Anomaly detection: PASS (no flags)")
# --- Layer 5: Anti-gaming ---
try:
existing = load_submissions()
history = [
{
"submitter_email": s.get("metadata", {}).get("contact_email", ""),
"timestamp": s.get("submission_date", ""),
"manifest_hash": s.get("integrity", {}).get("manifest_hash", ""),
"run_id": s.get("integrity", {}).get("run_id", ""),
"organization": s.get("metadata", {}).get("team", ""),
}
for s in existing
]
gaming_issues = validate_anti_gaming(submission, history)
except Exception as e:
logger.exception("Anti-gaming validation crashed")
return "rejected", None, f"REJECTED: Internal error during anti-gaming check — {e}"
if gaming_issues:
report_lines.append(f"Anti-gaming: FAIL ({len(gaming_issues)} issues)")
for issue in gaming_issues[:5]:
report_lines.append(f" ERROR: {issue}")
if len(gaming_issues) > 5:
report_lines.append(f" ... and {len(gaming_issues) - 5} more issues")
return "rejected", None, "REJECTED\n\n" + "\n".join(report_lines)
report_lines.append("Anti-gaming: PASS")
# --- Final status ---
if anomaly_flags:
status = "flagged"
report_lines.insert(0, "STATUS: FLAGGED (published with review pending)")
else:
status = "verified"
report_lines.insert(0, "STATUS: VERIFIED")
return status, data, "\n".join(report_lines)
def process_upload(file):
"""Process and validate an uploaded submission file.
Returns (result_text, updated_table, updated_agent_choices).
"""
def _current_state():
"""Helper to return current table + agent choices for error returns."""
try:
subs = load_submissions()
choices = [s.get("metadata", {}).get("agent_id", "?") for s in subs]
return build_main_table(subs), gr.update(choices=choices)
except Exception:
return pd.DataFrame(), gr.update(choices=[])
try:
status, data, report = validate_upload_full(file)
except Exception as e:
logger.exception("Unexpected error during validation")
table, choices = _current_state()
return f"REJECTED: Internal validation error — {e}", table, choices
if data is None:
table, choices = _current_state()
return report, table, choices
# Add status and save
data["status"] = status
data["verified_at"] = datetime.now(timezone.utc).isoformat()
try:
save_submission(data)
except Exception as e:
logger.exception("Failed to save submission")
table, choices = _current_state()
return f"Validation passed but save failed — {e}. Please retry.", table, choices
metrics = data.get("results", {}).get("metrics", {})
subs = load_submissions()
agent_choices = [s.get("metadata", {}).get("agent_id", "?") for s in subs]
summary = (
f"Agent: {data['metadata']['agent_id']}\n"
f"Team: {data['metadata']['team']}\n"
f"CR: {metrics.get('CR', 0):.3f} | CuP: {metrics.get('CuP', 0):.3f}\n"
f"Tasks: {len(data.get('task_evidence', []))}\n\n"
f"--- Verification Report ---\n{report}"
)
return (
summary,
build_main_table(subs),
gr.update(choices=agent_choices),
)
def admin_remove_submission(agent_id: str, run_id: str, session_token: str):
"""Remove submission(s) by agent_id and/or run_id (session-gated).
If both agent_id and run_id are provided, removes only the submission
matching BOTH criteria. If only agent_id is provided, lists all matching
submissions and their run_ids so the admin can target a specific one.
"""
if not _verify_session(session_token):
return "Session expired — please log in again."
agent_id = (agent_id or "").strip()
run_id = (run_id or "").strip()
if not agent_id and not run_id:
return "Please enter an agent_id or run_id (or both)."
subs = load_submissions()
def matches(s):
s_agent = s.get("metadata", {}).get("agent_id", "")
s_run = s.get("integrity", {}).get("run_id", "")
if agent_id and run_id:
return s_agent == agent_id and s_run == run_id
if run_id:
return s_run == run_id
return s_agent == agent_id
matching = [s for s in subs if matches(s)]
if not matching:
return f"No submission found matching agent_id='{agent_id}', run_id='{run_id}'."
# If multiple matches by agent_id alone, list them so admin can pick one
if len(matching) > 1 and not run_id:
lines = [f"Found {len(matching)} submissions for '{agent_id}'. "
f"Specify a run_id to remove a specific one:\n"]
for s in matching:
s_run = s.get("integrity", {}).get("run_id", "?")
s_date = s.get("submission_date", "?")[:10]
s_cup = s.get("results", {}).get("metrics", {}).get("CuP", "?")
lines.append(f" run_id={s_run[:12]}... date={s_date} CuP={s_cup}")
lines.append(f"\nOr leave run_id empty to remove ALL {len(matching)}.")
return "\n".join(lines)
filtered = [s for s in subs if not matches(s)]
removed = len(subs) - len(filtered)
# Atomic write: write to temp file, then rename (prevents data loss on failure)
tmp_path = SUBMISSIONS_FILE.with_suffix(".tmp")
try:
with open(tmp_path, "w") as f:
for s in filtered:
f.write(json.dumps(s) + "\n")
f.flush()
os.fsync(f.fileno())
os.replace(tmp_path, SUBMISSIONS_FILE)
except Exception:
if tmp_path.exists():
tmp_path.unlink()
raise
detail = f"agent_id={agent_id}" if agent_id else ""
if run_id:
detail += f"{', ' if detail else ''}run_id={run_id}"
_log_admin_action("remove_submission", f"Removed {removed} submission(s): {detail}")
return f"Removed {removed} submission(s) ({detail})."
def admin_force_add_submission(file, reason: str, session_token: str):
"""Force-add a submission bypassing all validation (admin-only, session-gated).
Performs only minimal sanity checks:
- Valid JSON
- Pydantic schema parses (so the leaderboard can display it)
Marks submission with status="admin_override" for full traceability.
Returns (result_message, updated_table, validation_report).
"""
def _table():
"""Current leaderboard table (safe fallback on error)."""
try:
return build_main_table(load_submissions())
except Exception:
return pd.DataFrame()
if not _verify_session(session_token):
return "Session expired — please log in again.", _table(), ""
reason = (reason or "").strip()
if not reason:
return "Reason is required for audit trail.", _table(), ""
if file is None:
return "No file uploaded.", _table(), ""
# --- Minimal sanity: parse JSON ---
try:
file_path = file.name if hasattr(file, "name") else str(file)
with open(file_path, "r") as f:
data = json.load(f)
except json.JSONDecodeError as e:
return f"Invalid JSON — {e}", _table(), ""
except Exception as e:
return f"Could not read file — {e}", _table(), ""
# --- Minimal sanity: Pydantic schema must parse (leaderboard needs valid structure) ---
try:
Submission(**data)
except Exception as e:
return (
f"Schema validation failed — cannot force-add a structurally invalid submission.\n{e}",
_table(),
"",
)
# --- Run validation in report-only mode (so admin sees what was bypassed) ---
try:
_status, _data, report = validate_upload_full(file)
report = f"Normal validation would have returned: {_status.upper()}\n\n{report}"
except Exception as e:
report = f"Validation crashed (this is what you're bypassing): {e}"
# --- Force-add with admin_override status ---
agent_id = data.get("metadata", {}).get("agent_id", "unknown")
data["status"] = "admin_override"
data["verified_at"] = datetime.now(timezone.utc).isoformat()
data["admin_override_reason"] = reason
try:
save_submission(data)
except Exception as e:
return f"Save failed — {e}", _table(), report
_log_admin_action("force_add_submission", f"Agent: {agent_id}, Reason: {reason}")
metrics = data.get("results", {}).get("metrics", {})
cr_val = metrics.get("CR", 0) or 0
cup_val = metrics.get("CuP", 0) or 0
result = (
f"Force-added successfully.\n"
f"Agent: {agent_id}\n"
f"CR: {cr_val:.3f} | CuP: {cup_val:.3f}\n"
f"Status: admin_override\n"
f"Reason: {reason}"
)
return result, _table(), report
def admin_build_key_dashboard(session_token: str):
"""Build comprehensive key request dashboard (session-gated).
Returns (stats_markdown, dataframe, timeline_plot, institution_plot, csv_file).
"""
empty = (
"*Click Load Dashboard to populate.*",
pd.DataFrame(),
_empty_figure("No data", 350),
_empty_figure("No data", 300),
None,
)
if not _verify_session(session_token):
return ("Session expired — please log in again.", *empty[1:])
requests = _load_key_requests()
if not requests:
return ("No key requests yet.", *empty[1:])
# ---- Build DataFrame ----
df = pd.DataFrame(requests)
df["timestamp"] = pd.to_datetime(df["timestamp"], errors="coerce", utc=True)
df = df.sort_values("timestamp", ascending=False).reset_index(drop=True)
# ---- Summary Statistics ----
total = len(df)
unique_emails = df["email"].nunique()
unique_teams = df["team"].nunique()
inst_series = df["institution"].replace("", pd.NA).dropna()
unique_institutions = inst_series.nunique()
now_utc = datetime.now(timezone.utc)
last_7d = int(df[df["timestamp"] >= (now_utc - pd.Timedelta(days=7))].shape[0])
last_30d = int(df[df["timestamp"] >= (now_utc - pd.Timedelta(days=30))].shape[0])
ts_min = df["timestamp"].min()
ts_max = df["timestamp"].max()
earliest = ts_min.strftime("%Y-%m-%d") if pd.notna(ts_min) else "N/A"
latest = ts_max.strftime("%Y-%m-%d") if pd.notna(ts_max) else "N/A"
email_counts = Counter(df["email"])
repeat_users = {e: c for e, c in email_counts.items() if c > 1}
repeat_str = f"{len(repeat_users)} user(s)" if repeat_users else "None"
team_counts = Counter(df["team"])
top_teams = team_counts.most_common(5)
top_teams_str = ", ".join(f"{t} ({c})" for t, c in top_teams) if top_teams else "N/A"
inst_counts = Counter(inst_series)
top_insts = inst_counts.most_common(5)
top_insts_str = ", ".join(f"{t} ({c})" for t, c in top_insts) if top_insts else "N/A"
stats_md = (
"### Key Request Statistics\n"
"| Metric | Value |\n"
"|:--|:--|\n"
f"| **Total Requests** | {total} |\n"
f"| **Unique Emails** | {unique_emails} |\n"
f"| **Unique Teams** | {unique_teams} |\n"
f"| **Unique Institutions** | {unique_institutions} |\n"
f"| **Last 7 Days** | {last_7d} |\n"
f"| **Last 30 Days** | {last_30d} |\n"
f"| **Date Range** | {earliest} to {latest} |\n"
f"| **Repeat Requesters** | {repeat_str} |\n"
f"| **Top Teams** | {top_teams_str} |\n"
f"| **Top Institutions** | {top_insts_str} |\n"
)
# ---- Timeline Chart (Cumulative) ----
timeline_fig = _empty_figure("No valid timestamps", 350)
if pd.notna(df["timestamp"]).any():
daily = (
df.set_index("timestamp")
.resample("D")
.size()
.cumsum()
.reset_index(name="cumulative")
)
daily.columns = ["date", "cumulative"]
timeline_fig = go.Figure()
timeline_fig.add_trace(go.Scatter(
x=daily["date"],
y=daily["cumulative"],
mode="lines+markers",
line=dict(color=PLOTLY_COLORWAY[0], width=2),
marker=dict(size=4),
name="Cumulative Requests",
fill="tozeroy",
fillcolor="rgba(59, 130, 246, 0.1)",
))
timeline_fig.update_layout(**_plotly_layout(
title="Key Requests Over Time (Cumulative)",
xaxis_title="Date",
yaxis_title="Total Requests",
height=350,
xaxis=dict(gridcolor=PLOTLY_GRID_COLOR),
yaxis=dict(gridcolor=PLOTLY_GRID_COLOR, rangemode="tozero"),
))
# ---- Institution Bar Chart ----
if inst_counts:
top_n = 10
sorted_insts = inst_counts.most_common(top_n)
inst_names = [x[0] for x in reversed(sorted_insts)]
inst_vals = [x[1] for x in reversed(sorted_insts)]
inst_fig = go.Figure(go.Bar(
x=inst_vals,
y=inst_names,
orientation="h",
marker_color=PLOTLY_COLORWAY[1],
))
inst_fig.update_layout(**_plotly_layout(
title=f"Top {min(top_n, len(sorted_insts))} Institutions",
xaxis_title="Requests",
height=max(250, 40 * len(sorted_insts) + 100),
yaxis=dict(tickfont=dict(size=11)),
xaxis=dict(gridcolor=PLOTLY_GRID_COLOR, dtick=1),
))
else:
inst_fig = _empty_figure("No institutions recorded", 300)
# ---- Display DataFrame ----
display_df = df.copy()
display_df["timestamp"] = display_df["timestamp"].dt.strftime("%Y-%m-%d %H:%M UTC")
# Re-derive signing key for each email (deterministic from master key)
if _get_master_key():
display_df["key"] = display_df["email"].apply(
lambda e: derive_user_key(e)[:16] + "..."
)
else:
display_df["key"] = "N/A (no master key)"
display_df.insert(0, "#", range(1, len(display_df) + 1))
display_df.columns = ["#", "Email", "Team", "Institution", "Timestamp", "Signing Key (truncated)"]
# ---- CSV export (owner-only permissions) ----
csv_path = None
try:
tmp = tempfile.NamedTemporaryFile(
mode="w", suffix=".csv", prefix="key_requests_", delete=False,
)
display_df.to_csv(tmp.name, index=False)
tmp.close()
os.chmod(tmp.name, 0o600)
csv_path = tmp.name
except Exception:
pass
_log_admin_action("view_dashboard", f"Key dashboard accessed ({len(requests)} requests)")
return stats_md, display_df, timeline_fig, inst_fig, csv_path
def admin_view_audit_log(session_token: str) -> str:
"""Show recent admin audit log entries (session-gated)."""
if not _verify_session(session_token):
return "Session expired — please log in again."
if not ADMIN_AUDIT_FILE.exists():
return "No audit log entries yet."
entries = []
for line in ADMIN_AUDIT_FILE.read_text().strip().split("\n"):
if line.strip():
try:
entries.append(json.loads(line))
except json.JSONDecodeError:
continue
if not entries:
return "No audit log entries yet."
# Show most recent first, limit to last 100
entries = entries[-100:][::-1]
lines = [f"**Audit Log** ({len(entries)} most recent entries)\n"]
for e in entries:
lines.append(
f"- `{e.get('timestamp', '?')}` | "
f"**{e.get('action', '?')}** | "
f"{e.get('details', '')}"
)
return "\n".join(lines)
def admin_login(password: str):
"""Validate admin password, create session, return (panel_visibility, status, token).
Uses timing-safe comparison and rate limiting.
"""
# Rate-limit check
locked = _check_rate_limit()
if locked:
_log_admin_action("login_blocked", "Rate-limited")
return gr.update(visible=False), locked, ""
if not _get_admin_password():
return gr.update(visible=False), "Admin not configured.", ""
if not _verify_admin_password(password):
_record_failed_attempt()
_log_admin_action("login_failed", "Invalid password attempt")
return gr.update(visible=False), "Invalid password.", ""
token = _create_admin_session()
_log_admin_action("login", "Admin login successful")
return gr.update(visible=True), "Logged in. Session expires in ~60 min.", token
# ---------------------------------------------------------------------------
# Gradio UI
# ---------------------------------------------------------------------------
def create_app() -> gr.Blocks:
submissions = load_submissions()
agent_choices = [s.get("metadata", {}).get("agent_id", "?") for s in submissions]
theme = gr.themes.Soft(
primary_hue=colors.blue,
secondary_hue=colors.indigo,
neutral_hue=colors.slate,
spacing_size=sizes.spacing_md,
radius_size=sizes.radius_md,
text_size=sizes.text_md,
font=(
gr.themes.GoogleFont("Inter"),
"ui-sans-serif",
"system-ui",
"sans-serif",
),
font_mono=(
gr.themes.GoogleFont("JetBrains Mono"),
"ui-monospace",
"Consolas",
"monospace",
),
).set(
body_background_fill="#f8fafc",
body_text_color="#1e293b",
body_text_color_subdued="#64748b",
block_background_fill="white",
block_border_width="1px",
block_border_color="#e2e8f0",
block_shadow="0 1px 3px 0 rgb(0 0 0 / 0.05), 0 1px 2px -1px rgb(0 0 0 / 0.05)",
block_label_background_fill="*primary_50",
block_label_text_color="*primary_700",
button_primary_background_fill="linear-gradient(135deg, *primary_500, *secondary_500)",
button_primary_background_fill_hover="linear-gradient(135deg, *primary_600, *secondary_600)",
button_primary_shadow="0 4px 6px -1px rgb(59 130 246 / 0.25)",
button_primary_border_color="transparent",
button_secondary_background_fill="white",
button_secondary_border_color="*primary_200",
button_secondary_text_color="*primary_600",
input_background_fill="white",
input_border_color="#e2e8f0",
input_border_width="1px",
input_shadow="none",
input_shadow_focus="0 0 0 3px rgb(59 130 246 / 0.15)",
table_border_color="#e2e8f0",
table_even_background_fill="white",
table_odd_background_fill="#f8fafc",
link_text_color="*primary_600",
link_text_color_hover="*primary_700",
link_text_color_active="*primary_800",
)
with gr.Blocks(
title="ST-WebAgentBench Leaderboard",
theme=theme,
css=CUSTOM_CSS,
) as demo:
gr.HTML(f"""
Evaluating Safety & Trustworthiness in Web Agents