Spaces:
Running
Running
| """Provael ASR leaderboard — Hugging Face Space (Gradio). | |
| Renders the committed ``results/*.json`` (from ``provael leaderboard build``) as a ranked table | |
| with a RoboArena-style **all-policies vs. open-source-policies** split, example attacked payloads, | |
| and an **open-submission** tab that opens a PR to a requests dataset for review | |
| (Open-LLM-Leaderboard pattern). No GPU is used to *view*; submissions are validated offline by a | |
| maintainer before promotion. | |
| Both the Hub client and ZeroGPU are imported lazily/guarded so this app also runs locally with | |
| ``python app.py`` (submission is disabled without ``HF_TOKEN``). | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| from pathlib import Path | |
| import gradio as gr | |
| try: # Available on Hugging Face ZeroGPU Spaces; absent locally. | |
| import spaces # noqa: F401 (kept for a future @spaces.GPU live-run button) | |
| except ImportError: # pragma: no cover - environment dependent | |
| spaces = None # type: ignore[assignment] | |
| RESULTS_DIR = Path(__file__).parent / "results" | |
| #: The requests dataset a submission opens a PR against (Open-LLM-Leaderboard pattern). | |
| #: | |
| #: This used to point at `provael-submissions/requests`, an ORG THAT WAS NEVER CREATED — verified | |
| #: 2026-08-08: org page 404, datasets API 401, `?author=provael-submissions` returns `[]`. The | |
| #: submit button had been aimed at nothing since it shipped. | |
| #: | |
| #: It now points under the same account that owns the Space, so the token the Space already needs | |
| #: covers it and there is no org to administer. A separate org is the Open-LLM-Leaderboard shape and | |
| #: is worth revisiting if submissions ever reach a volume that justifies it; at zero third-party | |
| #: submissions in the leaderboard's lifetime, an org was infrastructure standing between a stranger | |
| #: and a contribution. | |
| #: | |
| #: It still may not exist — `scripts/setup_requests_dataset.py` creates it — and the code below | |
| #: treats that as a state to report rather than a crash. See GUARANTEED_ROUTE. | |
| REQUESTS_REPO = "Sattyam/provael-leaderboard-requests" | |
| #: Where a submitter is sent when the queue is unavailable. A GitHub issue on the product repo needs | |
| #: no HF org, no token and no dataset to exist — it is the route that cannot break. Same discipline | |
| #: as the website's lead-sink chain: never report a capture that did not happen, and always leave a | |
| #: working path rather than an apology. | |
| GUARANTEED_ROUTE = "https://github.com/provael/provael/issues/new" | |
| #: Adversarial families in the `provael` registry (excluding the benign `baseline` control) — the | |
| #: denominator of the coverage line below. | |
| #: | |
| #: Hardcoded because this Space deliberately installs **no** `provael` (see requirements.txt: | |
| #: viewing renders committed JSON and needs no package), so it cannot import the registry to count | |
| #: them. A hardcoded count is a claim, and claims here drift: the repo shipped a release with three | |
| #: documents still saying "fourteen" after this number became 15. `tests/test_counted_claims.py` | |
| #: therefore checks this constant against the live registry and fails the build if they disagree. | |
| TOTAL_ADVERSARIAL_FAMILIES = 17 | |
| #: The release this Space was published from — the "you are here" against which `measured_with` | |
| #: reads as stale. Checked against `provael.__version__` by `tests/test_counted_claims.py` for the | |
| #: same reason as above: an unguarded version string in an uninstalled Space rots invisibly. | |
| CURRENT_RELEASE = "0.43.0" | |
| #: Policies considered open-source (weights available) — drives the RoboArena-style split. | |
| OPEN_SOURCE_POLICIES = frozenset( | |
| {"stub", "smolvla", "pi0", "pi05", "pi0fast", "groot", "openvla"} | |
| ) | |
| ROW_HEADERS = [ | |
| "rank", "policy", "suite", "family", "ASR (95% CI)", "benign", "n", "transfer", | |
| "submitted by", "provenance", | |
| ] | |
| EXAMPLE_HEADERS = ["family", "attack", "example adversarial payload"] | |
| def _load_results() -> tuple[list[dict], list[dict], bool, list[dict]]: | |
| """Load and merge every results JSON file. Returns (rows, examples, is_demo, provenance).""" | |
| rows: list[dict] = [] | |
| examples: dict[str, dict] = {} | |
| provenance: list[dict] = [] | |
| real_seen = False | |
| for path in sorted(RESULTS_DIR.glob("*.json")): | |
| data = json.loads(path.read_text(encoding="utf-8")) | |
| rows.extend(data.get("rows", [])) | |
| for example in data.get("examples", []): | |
| examples.setdefault(example["attack"], example) | |
| if not data.get("is_demo", True): | |
| real_seen = True | |
| if data.get("generated_at") or data.get("inputs_digest"): | |
| provenance.append({ | |
| "file": path.name, | |
| "generated_at": data.get("generated_at"), | |
| "commit": data.get("commit"), | |
| "inputs_digest": data.get("inputs_digest"), | |
| "measured_with": data.get("measured_with") or [], | |
| "tool_version": data.get("tool_version"), | |
| "stale": data.get("stale"), | |
| "stale_reason": data.get("stale_reason"), | |
| "signed": data.get("signature") is not None, | |
| }) | |
| rows.sort(key=lambda r: (-r["asr"], r["policy"], r["suite"], r["family"])) | |
| ordered_examples = sorted(examples.values(), key=lambda e: (e["family"], e["attack"])) | |
| return rows, ordered_examples, not real_seen, provenance | |
| def _benign_floor(rows: list[dict]) -> str: | |
| """The benign control this board actually measured, computed from its own baseline row. | |
| It was written into the prose as "a 0% benign false-positive control" while the board carried | |
| a `baseline` row at 4% (2/50) and `benign_fpr: 0.04` on every row — a page contradicting the | |
| artifact underneath it, on the most public surface this project has. The 0% figure was the | |
| MATCHED control for one arm (`roleplay` had no benign twin fire at the same task and seed), | |
| which is a different quantity from the marginal false-positive rate and not interchangeable | |
| with it. Computed here for the same reason every other number in this banner is: prose drifts | |
| from the rows it describes, and this one had. | |
| """ | |
| baseline = [r for r in rows if r.get("family") == "baseline"] | |
| if baseline: | |
| successes = sum(int(r.get("successes", 0)) for r in baseline) | |
| attempts = sum(int(r.get("attempts", 0)) for r in baseline) | |
| if attempts: | |
| return f"{100.0 * successes / attempts:.1f}% ({successes}/{attempts})" | |
| fprs = {r.get("benign_fpr") for r in rows if r.get("benign_fpr") is not None} | |
| if len(fprs) == 1: | |
| return _pct(next(iter(fprs))) | |
| return "an unrecorded" | |
| def _coverage_banner(rows: list[dict], provenance: list[dict]) -> str: | |
| """State what this board measured, with what, and — the part that rots silently — how long ago. | |
| A board is rebuilt by re-aggregating committed report files, so `generated_at` moves to today | |
| while the numbers stay exactly as old as they were. Schema v3 records `measured_with` for that | |
| reason, but it lived only inside the JSON: the rendered page showed a fresh build date over | |
| numbers measured 28 releases earlier, and a signature underneath. A signature over stale data | |
| is worse than no signature, because it reads as currency. | |
| Everything here is computed from the loaded board rather than written into prose, so the | |
| statement cannot drift from the rows it describes. The one exception is | |
| :data:`TOTAL_ADVERSARIAL_FAMILIES`, which the repo's test suite pins to the live registry. | |
| """ | |
| measured_with = sorted({v for p in provenance for v in p["measured_with"]}) | |
| policies = sorted({r["policy"] for r in rows}) | |
| suites = sorted({r["suite"] for r in rows}) | |
| # The benign control is not an adversarial family; counting it as coverage would inflate the | |
| # numerator with the one arm that is definitionally not an attack. | |
| covered = sorted({r["family"] for r in rows if r["family"] != "baseline"}) | |
| missing = TOTAL_ADVERSARIAL_FAMILIES - len(covered) | |
| if not measured_with: | |
| return "" | |
| # The board's own `stale` flag leads, because it is what a downstream consumer refuses on; | |
| # the version comparison is kept as the fallback for a board that predates the field. If the | |
| # two ever disagree, the declared flag wins here and CI fails on it in the repo, rather than | |
| # this page quietly rendering a verdict the artifact does not carry. | |
| declared = next((p["stale"] for p in provenance if p.get("stale") is not None), None) | |
| stale = bool(declared) if declared is not None else CURRENT_RELEASE not in measured_with | |
| versions = ", ".join(f"`{v}`" for v in measured_with) | |
| lines = [ | |
| f"> {'⚠️ **Stale measurement.**' if stale else '**Measurement provenance.**'} " | |
| f"The rows below were measured with **provael {versions}**" | |
| + (f", not the current release (`{CURRENT_RELEASE}`). " if stale else ". ") | |
| + "Rebuilding a board re-stamps its date and commit but **never re-runs a policy**, so " | |
| "the build stamp below is newer than the numbers.", | |
| "", | |
| f"> **Coverage.** {len(policies)} policy ({', '.join(policies)}) on " | |
| f"{len(suites)} suite ({', '.join(suites)}); **{len(covered)} of " | |
| f"{TOTAL_ADVERSARIAL_FAMILIES} adversarial families** measured " | |
| f"({', '.join(covered)}). **{missing} families have no real-model measurement at all** — " | |
| "they are absent from this board, which is not the same as scoring 0%.", | |
| "", | |
| f"> **No clean-task-success control.** The underlying SmolVLA × LIBERO run predates the " | |
| f"competence control, so `clean_task_success_rate` is unrecorded: these rates are read " | |
| f"against a {_benign_floor(rows)} benign false-positive control, but not against a " | |
| f"measured demonstration that the policy completes its benign task unattacked. " | |
| f"Not back-filled.", | |
| "", | |
| _independence_line(rows), | |
| ] | |
| return "\n".join(lines) | |
| def _independence_line(rows: list[dict]) -> str: | |
| """Who produced these numbers — the question a column of repeated names answers too quietly. | |
| A board of four rows from one maintainer run and a board of four rows from four independent | |
| labs are identical in every other field on this page. Stating the count means the page cannot | |
| imply external validation it does not have, and it makes a real third-party submission | |
| visible the day it lands. | |
| """ | |
| submitters = sorted({str(r["submitted_by"]) for r in rows if r.get("submitted_by")}) | |
| independent = sorted( | |
| { | |
| str(r["submitted_by"]) | |
| for r in rows | |
| if r.get("submitted_by") and r.get("provenance") == "third-party-submission" | |
| } | |
| ) | |
| if not submitters: | |
| return ( | |
| "> **Independence.** No submitter is recorded on these rows — they predate submitter " | |
| "attribution. Read them as unattributed, not as independently reproduced." | |
| ) | |
| if not independent: | |
| return ( | |
| f"> ⚠️ **Independence: none.** All {len(rows)} rows were submitted by " | |
| f"**{', '.join(str(s) for s in submitters)}** (the maintainer). Nobody outside the " | |
| "project has yet reproduced or submitted a result. Submit one with `provael submit`." | |
| ) | |
| return ( | |
| f"> **Independence.** {len(submitters)} submitter(s); **{len(independent)} independent** " | |
| f"({', '.join(str(s) for s in independent)}) — rows submitted from outside the project." | |
| ) | |
| def _ci(row: dict) -> str: | |
| ci = row.get("ci95") | |
| if not ci: | |
| return "" | |
| return f" [{100.0 * ci[0]:.0f}-{100.0 * ci[1]:.0f}%]" | |
| def _pct(x: float | None) -> str: | |
| return "n/a" if x is None else f"{100.0 * x:.1f}%" | |
| def _benign(row: dict) -> str: | |
| """The control arm, rendered the way the ASR beside it is: rate, counts, interval. | |
| The ASR column has carried `x/y` and a 95% interval since this board existed; the benign | |
| column carried a bare percentage. An ASR is a difference against that floor, and 4% at n=50 is | |
| Wilson [1.1%, 13.5%] — most of an interval a reader was left to assume away. Board schema 6 | |
| records the counts per row (`benign_successes` / `benign_attempts` / `benign_ci95`); an older | |
| row has only the rate and renders as it always did rather than gaining an invented interval. | |
| """ | |
| fpr = row.get("benign_fpr") | |
| if fpr is None: | |
| return "n/a" | |
| cell = f"{100.0 * fpr:.1f}%" | |
| attempts = row.get("benign_attempts") | |
| if attempts: | |
| cell += f" ({row.get('benign_successes')}/{attempts})" | |
| ci = row.get("benign_ci95") | |
| if ci: | |
| cell += f" [{100.0 * ci[0]:.0f}-{100.0 * ci[1]:.0f}%]" | |
| return cell | |
| def _provenance_label(row: dict) -> str: | |
| """Provenance, rendered per row rather than left in the JSON. | |
| The field has always been recorded and never displayed, which is the worst of both: a reader | |
| cannot tell a self-reported row from an externally submitted one, and the board looks like a | |
| leaderboard while being a changelog of our own runs. The summary line above the table states the | |
| count; this states it per row, so a single third-party entry is visible in place rather than | |
| only in an aggregate. | |
| Unknown values are passed through rather than mapped to a default. A provenance this code does | |
| not recognise is not evidence of anything, and quietly labelling it "maintainer-run" would be a | |
| guess in the direction that flatters the board. | |
| """ | |
| provenance = row.get("provenance") | |
| if not provenance: | |
| return "unrecorded" | |
| return { | |
| "maintainer-run": "maintainer-run (self-reported)", | |
| "third-party-submission": "third-party ✔", | |
| }.get(str(provenance), str(provenance)) | |
| def _row_table(rows: list[dict]) -> list[list[str]]: | |
| return [ | |
| [ | |
| str(rank), r["policy"], r["suite"], r["family"], | |
| f"{100.0 * r['asr']:.1f}%{_ci(r)}", _benign(r), | |
| f"{r['successes']}/{r['attempts']}", | |
| "real" if r.get("transfer_status") == "real-transfer" else "stub", | |
| r.get("submitted_by") or "unattributed", | |
| _provenance_label(r), | |
| ] | |
| for rank, r in enumerate(rows, start=1) | |
| ] | |
| def _example_table(examples: list[dict]) -> list[list[str]]: | |
| return [[e["family"], e["attack"], e["example"]] for e in examples] | |
| def submit_result(model_id: str, results_file: str | None) -> str: | |
| """Open a PR to the requests dataset with a submitted results JSON (queued for review).""" | |
| if not model_id or results_file is None: | |
| return "Provide a model id and a `provael` results JSON file." | |
| try: | |
| from huggingface_hub import HfApi | |
| except ImportError: # pragma: no cover - environment dependent | |
| return "huggingface_hub not available — run on the Space (or pip install huggingface_hub)." | |
| token = os.environ.get("HF_TOKEN") | |
| if not token: | |
| return ( | |
| "Submission queue disabled locally: set HF_TOKEN on the Space. Submitting opens a PR " | |
| f"to `{REQUESTS_REPO}` for a maintainer to validate and promote." | |
| ) | |
| api = HfApi(token=token) # pragma: no cover - requires a live token | |
| # "Submitted" is only ever returned when the API call actually succeeded AND handed back a PR | |
| # URL to prove it. The previous version returned that string unconditionally after an unguarded | |
| # upload_file into a repo that does not exist — so the outcome was either a raw traceback or, if | |
| # the call had ever silently succeeded, a false success on the project's only external- | |
| # contribution funnel. A submitter told "Submitted" when nothing was captured is the exact | |
| # failure the website's lead-sink chain was rebuilt twice to eliminate. | |
| try: | |
| info = api.upload_file( | |
| path_or_fileobj=results_file, | |
| path_in_repo=f"requests/{model_id.replace('/', '__')}.json", | |
| repo_id=REQUESTS_REPO, | |
| repo_type="dataset", | |
| create_pr=True, | |
| ) | |
| except Exception as exc: # noqa: BLE001 - any failure here must reach the submitter, not a log | |
| return ( | |
| f"**Not submitted.** The queue at `{REQUESTS_REPO}` did not accept it: `{exc}`.\n\n" | |
| "Nothing was captured — this is not a display problem, your result is not queued.\n\n" | |
| f"Working route, no account or token needed: [open an issue]({GUARANTEED_ROUTE}) and " | |
| "attach the same JSON. A maintainer validates and promotes it exactly as the queue " | |
| "would have." | |
| ) | |
| pr = getattr(info, "pr_url", None) | |
| if not pr: | |
| return ( | |
| "**Uncertain.** The upload returned no pull-request URL, so there is nothing to point " | |
| "you at and no way to confirm it queued.\n\n" | |
| f"Treat it as not submitted and [open an issue]({GUARANTEED_ROUTE}) with the JSON." | |
| ) | |
| return f"Submitted — [PR opened]({pr}) on `{REQUESTS_REPO}` for review." | |
| _DEMO_BANNER = ( | |
| "> ⚠️ **Demo data.** These results come from the deterministic CPU **stub** policy, not a " | |
| "real model. Illustrative until real-model runs are added (GPU command in the README)." | |
| ) | |
| _REAL_BANNER = "> ✅ Includes real-model results." | |
| _TRANSFER_NOTE = ( | |
| "> **Honest scope.** On the real **SmolVLA × LIBERO** policy, only the **instruction** family " | |
| "transfers today (roleplay 100%, goal_substitution 60%); **visual and injection attacks are " | |
| "0%** on the real model. Every rate carries its 95% Wilson CI and the benign (`none`) control. " | |
| "Rows are labelled `real` vs `stub`; stub rows are deterministic scaffolding, not a real " | |
| "transfer. Evidence, not certification." | |
| ) | |
| _INTRO = ( | |
| "# 🦾 Provael — VLA Red-Team ASR Leaderboard\n\n" | |
| "Attack Success Rate (ASR) of templated attacks against Vision-Language-Action robot " | |
| "policies in simulation. **Lower ASR = more robust.**\n" | |
| ) | |
| def _table(rows: list[dict]) -> gr.Dataframe: | |
| return gr.Dataframe( | |
| value=_row_table(rows), headers=ROW_HEADERS, datatype="str", interactive=False, wrap=True | |
| ) | |
| def _provenance_md(provenance: list[dict]) -> str: | |
| """A one-line-per-source provenance footer (build date, commit, inputs digest, signed).""" | |
| lines = ["**Provenance.** Each real board is stamped and reproducible from its inputs:"] | |
| for p in provenance: | |
| digest = (p.get("inputs_digest") or "")[:16] | |
| signed = " · signed" if p.get("signed") else "" | |
| # `measured_with` belongs next to `generated_at`, because the two are routinely different | |
| # and only one of them is about the numbers. | |
| measured = ", ".join(p.get("measured_with") or []) or "n/a" | |
| lines.append( | |
| f"- `{p['file']}` — built {p.get('generated_at') or 'n/a'} from commit " | |
| f"`{p.get('commit') or 'n/a'}`, **measured with provael {measured}**, " | |
| f"inputs `sha256:{digest}…`{signed}" | |
| ) | |
| return "\n".join(lines) | |
| def build_demo() -> gr.Blocks: | |
| rows, examples, is_demo, provenance = _load_results() | |
| open_rows = [r for r in rows if r["policy"] in OPEN_SOURCE_POLICIES] | |
| with gr.Blocks(title="Provael ASR Leaderboard") as demo: | |
| gr.Markdown(_INTRO) | |
| gr.Markdown(_DEMO_BANNER if is_demo else _REAL_BANNER) | |
| if not is_demo: | |
| # Above the tables, not below them: a reader who takes one number away should have | |
| # passed the sentence saying how old it is on the way to it. | |
| coverage = _coverage_banner(rows, provenance) | |
| if coverage: | |
| gr.Markdown(coverage) | |
| gr.Markdown(_TRANSFER_NOTE) | |
| with gr.Tabs(): | |
| with gr.Tab("All policies"): | |
| _table(rows) | |
| with gr.Tab("Open-source policies"): | |
| _table(open_rows) | |
| with gr.Tab("Example payloads"): | |
| gr.Dataframe( | |
| value=_example_table(examples), headers=EXAMPLE_HEADERS, | |
| datatype="str", interactive=False, wrap=True, | |
| ) | |
| with gr.Tab("Submit a result"): | |
| gr.Markdown( | |
| "Submit a `provael leaderboard build` results JSON. It opens a PR to " | |
| f"`{REQUESTS_REPO}`; a maintainer validates and promotes it.\n\n" | |
| "If the queue is unavailable you are told so plainly and sent to " | |
| f"[a GitHub issue]({GUARANTEED_ROUTE}) — which always works. You will never be " | |
| "shown a success message for a result that was not captured." | |
| ) | |
| model_in = gr.Textbox(label="Model id (e.g. org/my-vla)") | |
| file_in = gr.File(label="results JSON", type="filepath") | |
| out = gr.Markdown() | |
| gr.Button("Submit", variant="primary").click( | |
| submit_result, inputs=[model_in, file_in], outputs=out | |
| ) | |
| if provenance: | |
| gr.Markdown(_provenance_md(provenance)) | |
| gr.Markdown( | |
| "Built with [`provael`](https://github.com/provael/provael) — " | |
| "`provael leaderboard build --real <results> [--sign]`. Verify offline with " | |
| "`provael leaderboard verify`. Apache-2.0." | |
| ) | |
| return demo | |
| if __name__ == "__main__": | |
| build_demo().launch() | |