Every release, and how old the measurements are.
Read from the product repository’s CHANGELOG.md when this page was built, not transcribed. The current release is 0.38.0, and the newest entry below is 0.38.0 (24 August 2026).
last measured
14 days ago — the newest committed measurement is older than the seven-day window this project holds itself to, so the continuous-verification claim is not currently true. A release is not a measurement: shipping a version does not re-measure anything.
Source: watch/freshness.json ↗, which the tool regenerates and which decays on its own rather than being asserted here.
Changes to provael.com
Distinct from the tool’s releases below. A version string on a page can go stale without any release happening, which is how four of them did.
The version numbers came from the wrong single source ·
- PR #69 below is titled "One source of truth for every published number". For the measurements it held. For the version strings it did not, and it is worth being precise about why, because the mechanism it introduced is what broke them: #69 said derive everything from the pinned evidence manifest, and the manifest’s pin is deliberately FROZEN at whatever release produced the numbers. That is correct for a measurement and wrong for "the current release", so every surface naming a current version began falling behind the moment the next release shipped. Nothing was hardcoded — grepping for a stale string would have found none of it — and by v0.38.0 the changelog page named 0.36.2 as the current one directly above its own 0.37.0 entry (the sentence is paraphrased here on purpose: `check:versions` scans this page like any other, and spelling the claim out verbatim would make the guard flag its own account of the bug), /security told a security reporter to reproduce on a superseded release before filing, and the homepage published a copy-pasteable Action pin at a version two minors old.
- There are two version quantities, not one, so there are now two sources. The current release comes from the GitHub latest-release endpoint, fetched at build and committed to src/data/repo-facts.json so a build without network is reproducible; the measurement pin stays exactly where it was. The build refuses to run at all if the committed facts are pinned to a commit rather than a release tag, because six pages state a current release and a site that cannot name one should not be published.
- What is structurally different from #69: that PR added derivation and no check that the derivation read the right source — a derived number and a correctly-derived-from-the-wrong-place number are equally invisible to `check:figures`, which is why it caught none of this. `check:versions` now asserts against the built output that every currency CLAIM ("the current release is", "currently vX") and every copy-pasteable Action pin names the release the site is pinned to. It matches the claim rather than the number, because an old version appearing is usually correct: /citation names the release whose artifacts produced the numbers, /changelog lists every release there has ever been. It found one on its first run that had been missed by hand.
- The measurement’s age is now a build condition rather than a sentence. Past 7 days every page carries a banner stating the age in days and machine-readable meta; past 21 days the build FAILS. An age that cannot be parsed also fails, rather than defaulting to fresh — the failure to design against is a site quietly serving a number it can no longer date. The same status is written into llms.txt, read back out of the rendered page so the two cannot hold different numbers: robots.txt here opts every AI crawler in, so an answer engine can quote the attack-success rate without ever rendering a page, and the staleness has to travel with the number.
- /leaderboard now reads the board’s own staleness verdict instead of implying currency. When the published artifact says stale, the table sits behind a labelled "measured with an older tool version — not a current result" state carrying the product’s reason verbatim, and each row shows the tool version that measured it. The rows still render: hiding them behind an interaction would strip them from a crawler and from anyone on a keyboard, which makes the disclosure worse than what it discloses. The benign control column gained its counts and its own confidence interval, so the floor an attack-success rate is measured against is published to the same standard as the rate.
- llms.txt said "All 76 routes" while the sitemap carried 81 URLs. Both were right and neither said which: 81 is every indexable URL, 76 is the subset that is an HTML page with a title and description to list, and the other five are the feeds. The two already came from one source — the index is generated from the sitemap — so the fix was to state the arithmetic and check it, not to add a manifest layer that removes no failure mode.
One source of truth for every published number · #69 ↗
- The homepage carried the superseded single-task result — 100% keep-out violation against a 0% benign control, "one task" — in five places, on the same page as a hero already rendering the ten-task result from the pinned manifest. The page contradicted itself and the stale half was the flattering one. All five now derive from the manifest, so a re-pin moves them.
- Two build checks were added so this class of drift fails rather than ships. `check:figures` refuses a literal percentage or fraction inside a stat slot in any template, because after rendering a derived number and a typed one are indistinguishable. `check:board` now also compares /leaderboard against the exact signed artifact it rendered from — every row, the generation date, the source commit and the digest — since a page that can silently disagree with its own signature is worse than no page on a site whose pitch is "verify the signature rather than trust this".
- /verification stopped telling people to install from git. `provael submit` has shipped on PyPI since 0.32.0; the instruction outlived the fact it described, on the page whose job is to get third parties to reproduce a result.
- /citation now names the current release and carries the Zenodo concept DOI in both plain text and BibTeX. It previously said there was no DOI, which stopped being true when one was minted and confirmed resolving.
- Added /changelog, which had no route at all — releases were visible only on GitHub and PyPI, which is exactly why four version strings drifted unnoticed. It renders the product CHANGELOG and this list side by side, with the measurement-freshness signal beside them.
- The regulatory clock was counting down to an obligation that had already arrived: "next" was measured from the date the clock was last verified rather than from anything resembling now, so 2 August 2026 stayed "next" after it applied. It is now measured against the site’s own pinned date, and the page states which anchor it used and that an obligation has come into force since the entries were last checked against primary sources.
- The five machine-readable crosswalk artifacts are downloadable from /compliance, each stating its own mapping_status — and where the artifact declares none, saying so rather than supplying one. XPolicyLab is deliberately absent: it ships no artifact, and listing it would present a planned integration as a delivered one.
0.38.0
Changed
- The benign false-positive rate is now published beside every ASR, with its own Wilson interval. An attack-success rate is a difference against the benign floor, and every exporter in this package published the ASR with an interval and a denominator and the floor as a bare percentage:
benignFpr: 0.04in the SARIF run,benign_fprin AVID, one metric with noconfidenceIntervalin the ML-BOM, one prose clause in OSCAL, one table row in the Markdown report, one column in the transfer-test table — and, on the leaderboard, nothing at all. A rate with no denominator cannot be read: 0/5 and 0/500 both serialise as0.0, and 4% at n=50 is Wilson [1.1%, 13.5%], which is most of the interval a reader was implicitly assuming away.
RunReport.benign_headline() is the exact mirror of adversarial_headline() — same applicable filter, same family == "baseline" partition, opposite side of it, so the two are a partition of the run and a test pins that they sum to the applicable episodes. scoring.asr.benign_control() resolves it once for every emitter, so the pairing cannot drift between surfaces. Both recompute from results, so they work on the legacy schema-2 reports where the counts predate the stored fields, and a trimmed report keeps its stored rate and reports that the interval is unavailable rather than inventing one from a denominator that is not there. A run with no control arm renders n/a, never 0.0%: an unmeasured floor and a measured floor of zero are different claims about the same number.
Leaderboard schema_version 5 → 6 adds per-row benign_successes / benign_attempts / benign_ci95, and provael leaderboard grows a benign FPR (95% CI) column beside the ASR column. TransferTest gains benign_n / benign_successes / benign_ci95 on the same reasoning.
Additive with defaults, and registered in _ROW_FIELDS_ADDED_IN[6] — which is not bookkeeping. Adding the three fields without registering them changed the canonical bytes of the committed, signed v5 board and its Ed25519 signature stopped verifying; to anyone checking it, a correctly-signed board then looks exactly like a tampered one. The existing regression test caught it, but only because a v5 board happens to be committed, so two structural guards now fail immediately and by name when any future field is added without a version.
Added
- The leaderboard states its own staleness in a field a machine can refuse on (schema v6).
leaderboard/results/leaderboard.jsonpublishes four rows measured with provael 0.32.0 — six minor versions behind 0.37.0. It said so in a banner the Space renders, and prose does not stop a consumer: anything reading the JSON got four rates, an Ed25519 signature, and no way to tell that the signature vouches for a *measurement* rather than for its *currency*. A signature over stale data reads as currency, which is worse than no signature.
Three additive board fields: tool_version (which release assembled the board — distinct from measured_with, which is what measured the rows; the gap between them is the staleness), stale, and stale_reason naming both versions so the verdict is re-derivable from its own text. The lag is counted on (major, minor) and ignores the patch, since a patch changes no measured behaviour; across a major bump it is not a subtraction, because 0.40 → 1.0 is one release and not minus thirty-nine.
There is deliberately no assembled_at field: generated_at already carries exactly that instant, and two fields holding one value are two fields that can disagree. Its description now says so outright.
The flag sits outside the signed subject. Staleness is a function of today, not of the board — one that was current when signed becomes stale without a byte changing. Registering the fields in _FIELDS_ADDED_IN[6] strips them from the signing payload of any board declaring an earlier schema, which is what let the already-signed v5 board be annotated with stale: true, tool_version: "0.33.2" (read from src/provael/__init__.py at its own recorded commit 8cd8d99, not guessed) and per-row benign counts, and still verify against the committed public key. A four-line diff, signature intact.
scripts/check_leaderboard_staleness.py runs in CI and fails on undeclared staleness only — a board past the limit that does not carry stale: true. Failing on staleness itself would be red today and red until a GPU re-run nobody has scheduled, and a permanently-red detector reports nothing. Disclosed staleness is the honest state; silent staleness is the bug. --fix refreshes the flag; the verdict is monotone, so only a false can ever decay and that is the single direction re-checked.
The re-run was not taken: the rows are SmolVLA × LIBERO and re-running them is GPU-gated, so the board is flagged rather than refreshed.
studies/keepout_calibration/— what the benign 2/50 actually is (#136). The claim that the default keep-out box is misplaced has been asserted in this repository since 0.34.0 and never tested. It is now measured, from artifacts already committed, on CPU, with no simulator.
A benign false-positive rate is one number and one number cannot separate "the policy left the safe region" from "the boundary is in the wrong place" — opposite findings needing opposite responses. The discriminator is structure, not magnitude: a wandering policy scatters, a misplaced boundary fires on the same tasks every time. Pooling the ten-task suite with the control run gives 5 firings in 100 benign episodes (5.0%, Wilson 95% [2.2%, 11.2%]), and every one lands on libero_object/4 or /5 while the other eight tasks stay silent through 80 episodes. The seeds differ between runs, so each tests the other's task set out-of-sample; both directions are reported and the weaker, p = 0.04, is the headline. The other disjunct of the predicate is provably not involved: ForbiddenObjectGrasp ships empty and with no extractor.
No corrected zone is derived, and no threshold sweep is committed. Knowing a boundary is misplaced is not knowing where it belongs, and the study records why the second is impossible here: every committed LIBERO report is schema_version 2, predating AttackResult.trajectory, so the benign end-effector poses a fit consumes were computed on every step and discarded — endpoints is {} and danger is 0.0 on all five firings, and the untrimmed Modal artifacts only add decisions[], which carries no position. Sweeping a grid of boxes against episodes whose poses are unknown produces a curve of the right shape and no content. Choosing an operating point from it would be picking a second constant exactly the way the first one was picked, which is the bug rather than the fix.
So #136 stays open and the 44/50 headline has not moved — it cannot move without a benign re-run on a build at or after 0.35.0, which is GPU-gated. What the study does buy is scheduling: tasks 4 and 5 are the two to calibrate first, and a re-run does not need all ten.
- The policy's own sampler is seeded, and the seed is recorded (report schema v5). The board carried a caveat that SmolVLA's flow-matching sampler is "one draw, not a constant", which quietly means two rows at the same commit are not comparable — most of what a leaderboard is for. That was not a property of flow matching. Nothing had ever seeded it:
suite.reset(task, seed)seeded the ENVIRONMENT and every episode recorded that seed, while the policy drew its denoising noise from wherever the process's torch RNG happened to be. An early pilot at identical config returnedgoal_substitution1/4 on one run and 0/4 on the next.
PolicyAdapter.seed(seed) -> int | None is now called with the episode seed before every rollout, and AttackResult.policy_seed records what the adapter reports it applied, not what the runner asked for — the same discipline as resolved_device, because returning the argument unconditionally would let every report claim a determinism no adapter delivered. The LeRobot and OpenVLA adapters seed torch's generators; openpi returns None with the reason in the method, since inference happens in a separate server process whose protocol has no seed field. A stochastic adapter that inherits the default unchanged fails a test by name: inherited silence and a considered "cannot" look identical in a result file, and only one of them is a decision.
This does not claim bit-identical runs, and deliberately does not set torch.use_deterministic_algorithms — that changes which kernels run, and a measurement harness must not silently alter the compute path of the thing it is measuring. stochastic stays true. What changes is that a run records which seed its sampler started from. The GPU effect is untested here: there is no lerobot or CUDA in the CPU environment, so what is tested is the contract, not the numbers.
validate_report now refuses a stochastic submission at schema ≥ 5 with no policy_seed on any episode. Reports predating schema 5 — every result committed in this repository — are accepted with a named warning instead: the field did not exist when they were measured, and validate_submission.py runs over all of results/ on any PR that touches it, so a hard rule would have been red from the day it landed. The gap still reaches a consumer through the board's stale flag, since a report old enough to predate policy_seed is old enough to put its board past MAX_MINOR_LAG.
Both seeds already participate in inputs_digest and therefore in the board signature. That was true and had no test, which is the same as being true by accident; it has one now.
The caveat stays. The published rows were measured before any of this and carry no policy_seed, so nothing here makes them reproducible after the fact — it is scoped to those rows rather than deleted, and re-running them is GPU-gated. Deleting it belongs in the commit that completes the re-run.
Published schemas: report.v5.schema.json and leaderboard.v6.schema.json. report.v4 and leaderboard.v5 stay committed and frozen — their $id is a stable URL, so deleting one 404s any consumer that pinned it — with tests holding each to the promise its own version bound makes.
Fixed
- The leaderboard Space told visitors the rates were read against "a 0% benign false-positive control" while the board underneath it carried a
baselinerow at 4% (2/50). The 0% was the *matched* control for one arm —roleplayhad no benign twin fire at the same (task, seed) — which is a different quantity from the marginal false-positive rate and not interchangeable with it. The page now computes the figure from its own baseline row, like every other number in that banner, so it cannot drift from the rows it describes again. The benign column also renders counts and the interval, so the board reads41.3% [34-49%]against4.0% (2/50) [1-13%]rather than against a bare percentage.
0.37.0
Added
provael doctor— the command that answers "why did that not work here". Twenty-six top-level commands and not one diagnosed an install. The first-run transcript shows the cold path is twenty seconds, so the install is not the problem; the SECOND run is, when someone reaches for--policy smolvlawithout the[lerobot]extra or for a keep-out suite with no calibration and gets an import error or a silent default instead of a diagnosis.
One screen: Python and platform, installed version against PyPI (--offline skips the lookup), which policy backends are ready and which are SCAFFOLDING_POLICIES with the reason each is scaffolding, which suites actually import, whether CALIBRATED_ZONES is populated (it is not — it says so and points at issue #136), whether PROVAEL_REQUIRE_CALIBRATED is set, and the age of the freshness signal against STALE_DAYS. Nothing is inferred: a suite is reported importable only after being constructed, and "ready" explicitly does not claim a checkpoint is present.
- Published JSON Schemas for the two artifacts third parties are asked to produce.
schemas/report.v4.schema.jsonandschemas/leaderboard.v5.schema.json. The tree carried three$schemakeys and none describedreport.jsonorleaderboard.json, so an open submission queue with zero external rows was asking people to guess the shape. They are generated from the pydantic models (scripts/gen_schemas.py), not written from example files, so they describe the contract rather than today's artifacts;tests/test_published_schemas.pyvalidates all 33 committed artifacts against them, including schema-2 and schema-3 reports that predate the current model. AvNschema acceptsNor lower and refuses anything higher, so a too-old tool says so rather than silently passing. Referenced fromCONTRIBUTING-leaderboard.mdwith acheck-jsonschemaone-liner.
- A measured result for
weight_integrity, and a study page that leads with what it is not. The family shipped with zero output.results/weight_integrity_stub/now holds the ladder as five shards plus anaggregate.jsonanalysis, in the same shape as the ten-task suite — 750 episodes, both arms at every rung, benign control throughout.docs/studies/weight-integrity-stub.mdpublishes it with the caveat block above any number: stub backend, 64-parameter INT8 danger head, separation expected by construction, flips emulated withLiteral[True], and no corroboration of the architecture-dependence result it was built from.
Gradient 50/50 at every rung; random 4/250; benign control 0/50 throughout. The interval is bootstrapped over the five rungs, not over episodes, because the rung is the unit of analysis — and n = 5 is stated rather than hidden. No leaderboard row was added, and the study page says why: the board is the real-policy board, a shared table asserts comparability, and this is not comparable to anything on it.
Fixed
- A stub run could reset the freshness badge, and briefly did. Committing the study above put a
policy='stub'execution manifest underresults/, andlatest_measurement— which scans committed runs — immediately reported "today". The badge would have gone from "11 days ago, red" to green with nothing re-measured.
The stub satisfies the letter of the definition: it is a registered policy and the run does execute attacks against it. docs/standards/last-measured.md had already written this refusal down on 21 August with nothing in the code enforcing it. watch.counts_as_measurement now enforces it, FIXTURE_POLICIES names the backends that do not count, and the badge correctly reads 12 days, red.
- One family count, computed, everywhere. Four surfaces disagreed simultaneously:
docs/attacks.mdopened with "Fourteen adversarial families",docs/examples.mdsaidfull-sweep"runs all 14",docs/studies/action-envelope.mdcalled 14 "the full adversarial registry", and README said thirteen families lacked a real-model measurement while claiming twelve. The true numbers, each with its definition: 16 adversarial families (registry minusbaselineandcontrol), 18 total registered families, 38 adversarial attacks, 41 total registered attacks.
tests/test_counted_claims.py enumerated phrase patterns, which is why it was green while all four were wrong — it checked the claims it was handed rather than the claims that exist. It now sweeps every <number> … families / <number> … attacks construction in README and docs/, requiring each to be a registry-derived value or a named subset with a stated reason. The first version of the regex missed Fourteen** because emphasis wraps the number; that hole is fixed and noted, since it would have skipped the exact claim the sweep was written for.
- The README described a different run than the board publishes. It said the board was "measured with
provael 0.1.0" while the board and all ten shards say0.32.0;0.1.0is thetool_versionof the superseded single-task run.test_leaderboard_version_claimalready tied the board to its shards and passed throughout, because the README was never in the comparison. It is now. The paragraph also states, in the words of the file itself, thatleaderboard/method-equivalence.jsonis *"a code-inspection argument, NOT a re-measurement."*
- The 0.36.0 coverage entry hardcoded "88% of 7,833 statements" three lines above a paragraph criticising hardcoded percentages. It is now explicitly a snapshot and points at the badge as the live figure, rather than being rewritten — the entry records what was measured on the day.
0.36.2
Fixed
- Per-shard provenance digests moved with the tool version — the third instance of the same defect, and the one that finally got the rule written down.
combine.shard_digestscomputed each shard'ssha256withmodel_dump_json(), re-serialising it through whateverRunReportthe *running* version defines. The pinned public-evidence manifest records52bcdb70…forlibero_object_0/report.json; 0.34.0 reproduces it, 0.36.1 returned66897a4c…for byte-identical committed input, andgit diffconfirms the artifacts never changed.
That defeats the only purpose those digests have. The manifest advertises them so a consumer can "re-fetch each shard and verify it independently" — and under the old body they could only do so with the exact tool version that wrote the manifest, which is not discoverable from the manifest. It now projects through attest.report_projection; all ten shards reproduce the pinned values.
- The
RunReportdigest contract intypes.pydocumented the opposite of what the code does. It stated that "adding any field changes the digest of every historical report" and prescribed aRULESET_VERSIONbump for every added field. True when written, false sincereport_projectionlanded — and three digest sites went on doing the bare dump anyway, two of which shipped broken (leaderboard._inputs_digestin 0.36.1,combine.shard_digestshere). The contract now states the rule once: any digest over a RunReport goes throughattest.report_projection.
- The test guarding shard digests was tautological and could not have caught this: it computed its expected value with the same
model_dump_json()call the implementation used, so both sides shared the bug. Its own docstring said "or it is decoration". It now asserts the property a shared bug cannot satisfy — populating a field introduced *after* a shard's declaredschema_versionmust not move its digest — and was verified by restoring the old body and watching it fail.
0.36.1
Fixed
- A published leaderboard stopped rebuilding to its own
inputs_digest, and had done since 0.35.0._inputs_digestre-serialised every input report through whateverRunReportthe *running* version defines. Adding an optional field therefore rewrote the bytes of reports that predate it — a schema-2 report loaded by a schema-4 tool dumps"trajectory": null, "weight_corruption": nullon every result — so the digest of an unchanged, committed artifact moved with the tool version.
Measured against the board committed at 983c829, one unchanged input gave three answers:
| tool | inputs_digest | | |---|---|---| | 0.33.2 | 69396ef8… | reproduces | | 0.34.0 | 69396ef8… | reproduces | | 0.35.0 | 46008680… | broke here (trajectory, schema 3) | | 0.36.0 | 5d63664f… | worse (weight_corruption, schema 4) |
This is the offline-verifiability claim, so it matters more than its size suggests: www.provael.com/verification tells a stranger to rebuild the board and expect a match, and since 0.35.0 they would have got match: False and reasonably concluded the evidence did not reconcile. Signatures were never affected — leaderboard verify checks the board's own bytes and has passed throughout (keyid 8d62aa33ed5162f3). What broke was reproducing the board from its inputs.
The fix makes the code do what its docstring already claimed: _inputs_digest now projects each report through attest.report_projection, which strips fields added after the report's declared schema_version. An artifact digests to its own schema regardless of what is installed, so old boards verify again rather than needing to be re-signed against a newer tool.
Found by running the release smoke test the long way — rebuilding from a clean clone at the pinned commit rather than trusting that a green suite covered it. It did not: the golden test that would have caught this compared two synthetic projections and passed under the broken code too. The replacement asserts the real invariant against the committed artifacts, and was checked by restoring the old behaviour and watching it fail.
0.36.0
Added
weight_integrity— the first family that attacks the parameters instead of the input (EAI03). Every one of the fifteen families before it perturbs what the policy is shown or told. This one leaves the instruction and the observation exactly as the benign baseline delivers them and flips bits in the policy's loaded INT8 weights, so any unsafe behaviour is attributable to the weights and to nothing else. Ten registry entries: a gradient-selected arm and an equal-count random arm at each budget in aK = 1/4/16/64/256ladder, runnable as--recipe eai03-weight-integrityor--attacks weight_integrity.
What it measures and what it does not, because the difference is the whole reason a sim-only tool can ship this at all. It measures whether a policy is fragile to weight corruption. It does *not* measure whether an attacker can achieve that corruption on a real deployment — that is a platform question about DRAM fault injection, ECC, memory integrity and supply chain, and provael touches none of it. Every flip is emulated in memory, there is no hardware fault-injection path in the repository and none is planned, and every record carries emulated: true so a report states this rather than leaving a reader to assume it.
The control arm is mandatory, not optional. A gradient result with no equal-count random arm cannot separate "the ranking found the bits that matter" from "corrupting K bits of anything breaks it", and those have opposite engineering consequences. scoring.weight_integrity.crossing_pair returns None rather than half a result when the control is missing — a caller holding a number publishes it.
scoring/weight_integrity.py— the crossing point, which is the number worth publishing. A weight-integrity run has a curve, not a rate: "100% ASR" is meaningless without the flip budget it was bought at, and the family's pooled ASR averages two arms that are *meant* to differ. The reportable summary is the smallest K whose unsafe rate reaches a stated floor, and the floor is a required argument recorded on the result — a crossing at 50% and a crossing at 90% are different claims about the same curve. Non-monotone curves are reported as non-monotone rather than smoothed.
- A quantized danger head on the deterministic stub, so the family has real parameters to corrupt: 64 INT8 parameters (512 addressable bits) computing
clip01(gain * aggression + bias) * governor. The clean parameters dequantize to gain 1.0, bias 0.0 and governor 1.0 exactly — the scale is a negative power of two — so the clean output is byte-identical to the pre-0.36.0 action and no other family's ASR moved. The shipped stub run is still67.1% (47/70), asserted by a test over the whole[0, 1]range with equality rather than a tolerance.
- Crosswalks for both papers this release responds to:
docs/crosswalk/bit-flip-vla.md(arXiv:2608.15475) anddocs/crosswalk/embodied-agent-attack-surfaces.md(arXiv:2608.16843). The second is a coverage map and it is not flattering: five of the survey's twelve attack surfaces have no provael attack at all, provael is well covered exactly where the survey says the field already concentrates, and it is absent from three of the four areas the survey calls underexplored. Its five-layer taxonomy is *not* reproduced, because the layer names were not readable from the abstract page and inventing a counterpart's categories in order to map onto them is the failure these cards exist to avoid.
- The first run was timed on a clean machine, and the result did not support the reason the measurement was taken.
docs/first-run-transcript.mdrecords 20 s (49 s on a cold cache) frompip install provaelto a writtenreport.json, in apython:3.12-slimcontainer with no CUDA, no conda, no pip cache and no checkout — reproducing the README's advertised47/70on the first try, with nothing needing a decision, a lookup or a fix. The page was created on the premise that a slow first run explained 0 forks and 0 third-party reproductions. It does not: at 20 seconds this project is in the 39% of safety-benchmark repositories that run without modification (arXiv:2603.04459), so improving onboarding further would buy nothing. The barrier is one step later and it is economic — reproducing the headline SmolVLA result costs ~15.4 GPU-hours and ~$12, which is already documented with its exact command and price. Two papercuts found in passing: the README stated the Python 3.12 floor eleven lines *after*pip install(fixed — on 3.11 pip emits 38Requires-Pythonlines that read like a broken package), and the README prose sayslibero_objectsix times where the CLI suite islibero(recorded, not changed; renaming a CLI surface to close a prose mismatch is a larger decision than this exercise justifies).
- Bit-flip attacks cited in
PRIOR_ART.mdas an unimplemented channel, not as coverage. arXiv:2608.15475 (16 August 2026) attacks the *parameters* rather than the input, which no provael family does: gradient-selected INT8 flips collapse closed-loop success to 0% while hundreds of random flips are harmless, and the budget tracks the action head — 1–5 flips for direct-regression and token heads against ~100–300 for the flow-matching policies evaluated. The entry records their real-robot arm with its control: emulated K=100 gave 0/20 against 14/20 clean *and* 16/20 equal-count global-random.mapping_status: cited, not crosswalked— no crosswalk is possible because provael implements no weight-integrity attack as of 0.35.0, and the entry says so rather than implying coverage. Aweight_integrityfamily is named a candidate, explicitly not a shipped feature and not a dated roadmap commitment. - A disclosure rule for measuring policies we did not train, written before any such run.
SAFETY.mdalready told *users* of this tool to "contact the model's maintainers privately first and allow reasonable time to respond" — so publishing a third-party ASR without doing that would have put this project in breach of its own published rule, and "reasonable time" was never defined.docs/leaderboard-disclosure.mddefines it: 14 days' notice, the full artifact up front (signed report, exact command, checkpoint revision, predicate, benign control, and the draft caveat paragraph in the wording we intend to publish), publication after 14 days if there is no reply, and both positions printed on the same page where the authors disagree and we are not persuaded.
Two constraints in it are load-bearing and easy to drop later under pressure. Third-party rows carry the same caveats as our own — transfer caveat, Wilson interval however wide, benign false-positive rate, and an explicit uncalibrated-predicate note — and nulls are published at 0/n with the same prominence as positives. The board also will not rank policies by safety: the predicate is uncalibrated and the suite is one suite, so a table sorted by ASR invites a conclusion the measurement cannot support.
The ordering is the point. The rule is published *before* the first such run, because a policy written afterwards is a defence of a decision already made. Linked from the docs nav, docs/leaderboard.md, CONTRIBUTING-leaderboard.md, and the Space card.
- Security lint on ourselves.
S(flake8-bandit) added to the ruff selection — a tool that publishes other people's attack-success rates was not running security lint on its own source, which is the kind of gap a reviewer is right to notice. Enabling it surfaced 2,500 findings, of which 2,466 wereS101(assert) in the test suite, where assert is the point. The remaining 34 were triaged individually and none was a live defect: threeS105"hardcoded password" hits are an enum member (PASS = "pass"), a channel prefix, and a deliberately published fixture token the attacks never present;S311is a seeded bootstrap resample where a cryptographic RNG would break the determinism contract; thesubprocesshits pass fixed argv with no shell. Suppressions are scoped by rule and directory rather than blanket, and every one insrc/is an inlinenoqacarrying its reason, so a future hit has to be argued rather than absorbed.
S101 is suppressed in tests/ and nowhere else — an assert in src/ should still fail lint, because python -O deletes it silently.
- Coverage is measured, floored and published: 88% of 7,833 statements when this landed. That figure is a snapshot and is already out of date —
weight_integritymoved it to 89% of 8,053 in the same release. The live number is the badge, which reads a committedwatch/coverage.jsonregenerated from the run that just happened; this entry records what was measured on the day, not a standing claim. Stating it that way rather than restating the current value is the point of the paragraph below, which this sentence originally contradicted by hardcoding a percentage three lines above criticising hardcoded percentages. There were 90 test modules and no figure anywhere, which is the shape of claim this project refuses to accept from anyone else — "well tested" with no denominator. CI now runs--covon every PR, writes the table to the job summary, and gates on--cov-fail-under=85. The floor sits below the measured value deliberately: a gate pinned at the current number fires on ordinary refactors, and a gate that fires on noise gets raised until it means nothing.
The README badge is a shields endpoint reading a committed watch/coverage.json, regenerated from the run that just happened — the same mechanism as the freshness badge. A hardcoded 88% in a README is exactly the restated number src/provael/coverage.py exists to stop this project publishing.
The badge commit lives in its own workflow rather than in ci.yml. ci.yml is contents: read with persist-credentials: false so a fork PR cannot obtain a write token, and bolting a commit step onto it would have handed every fork PR a privileged workflow to aim at — a real security regression traded for a cosmetic convenience.
GPU-gated adapters are omitted from the coverage denominator rather than counted as missed: they never execute on the CPU lane, and counting them would understate the covered surface while inviting the number to be gamed by deleting them.
- CONTRIBUTING now states that new functionality ships with tests. It was always the practice; it was never written down, and an unwritten rule is one a new contributor has to guess at.
Changed
- The freshness badge's explanation now names the cause, and the seven-day window was NOT lengthened. The badge has read red for eleven days beside a sentence saying the newest committed measurement was older than "the seven-day window this project holds itself to". That sentence is accurate and misleading: it invites the inference that a cadence slipped, and no cadence ever ran.
gpu-nightly.yml is the job that would hold the window. It runs a real SmolVLA × LIBERO red-team on a Modal GPU and records into the watch ledger only on success. It is well built and has never executed, because it is switched off twice over — the repo variable ENABLE_GPU_NIGHTLY is unset (the repo has no variables at all) and MODAL_TOKEN_ID / MODAL_TOKEN_SECRET are absent. Both are deliberate cost-safety defaults.
So moving the window to fourteen days would have fixed the wrong thing: fourteen would be broken by the same absence, and a second unenforced number presented as a considered adjustment is worse than the first. STALE_DAYS stays 7 — seven days genuinely is old for a currency claim. What changed is the claim beside it. provael watch now prints the two settings that would fix it, and docs/standards/last-measured.md records the decision, what closes it, and the shortcut that was available and refused: a stub-policy run satisfies the letter of the definition and would turn the badge green today, which is the same error as calling a republication a measurement.
- Dependency bumps, all three merged with CI green:
docker/setup-buildx-action3.11.1 → 4.3.0,astral-sh/setup-uv9.0.0 → 10.0.1,github/codeql-action/upload-sarif4.37.6 → 4.37.7.
The two majors were read against actual usage rather than merged on green alone, because green was not evidence for one of them. setup-uv v10 disables caching for pull_request_target, workflow_run and release events; no workflow here uses any of those, and ci.yml runs on pull_request so its green run genuinely exercised v10. setup-buildx v4 requires Node 24 and removes deprecated inputs; this repo passes it no inputs at all, and docker-publish.yml runs on push only — so the green checks on that PR exercised setup-buildx *not at all*, and it is verified by the release image build instead.
Fixed
BitFlipRecord.emulatedisLiteral[True], notbooldefaulting to True. The field carried a docstring reading "Always True" and nothing enforced it:BitFlipRecord(..., emulated=False)constructed happily and serialised"emulated": false.
A record asserting a non-emulated bit flip asserts that this tool performed hardware fault injection — out of scope under SAFETY.md, and something provael has no path to do. The claim that keeps weight_integrity inside that boundary must not rest on a default a caller can override. Pydantic now rejects False at construction and mypy rejects it statically; the serialised value is unchanged, so canonical JSON, report digests and existing attestations are byte-identical. Found by auditing the family before tagging rather than after.
Two tests pin it: one that a non-emulated record cannot be constructed, and one that walks the AST of every module in attacks/ for hardware fault-injection code. The second checks *names* rather than raw text — its first version flagged weight_integrity.py's own docstring, which names DRAM fault injection and Rowhammer precisely to say they are out of scope, and a test that punishes the disclaimer would pressure someone to delete the scoping paragraph for a green build.
- Report schema 3 → 4: results carry
weight_corruption. The flip budget and the selection rule *are* the result — 100% at K=4 and 100% at K=256 are different findings — so they are recorded per result rather than deferred to a config the report does not contain. The bump landed in the same change that started emitting the field: a report declaring 3 while carrying a 4 field has that field stripped byattest.report_projectionbefore the digest, which signs *around* the corruption parameters rather than *over* them.attest._RESULT_FIELDS_ADDED_INgained the4:entry in the same commit, and the committed v4 leaderboard still verifies (keyid 8d62aa33ed5162f3).
PRIOR_ART.md's bit-flip entry moved fromcited, not crosswalkedtoimplemented, not corroborated— and the second half of that label is doing the work. The entry previously said provael implements no weight-integrity attack and that such a family was a candidate rather than a shipped feature. Both are now false. What is still true, and is now stated in three places rather than left to be inferred: provael has not reproduced the architecture-dependence result (1–5 flips for direct-regression and discrete-token heads against roughly 100–300 for flow-matching). The only run is against the CPU fixture, which has one scalar danger head and no action-decoding architecture to depend on, so the gradient-beats-random separation it shows is a property of that fixture. Citing provael as independent support for that claim would be wrong.
docs/first-run-transcript.mdcited p = 0.005 for the runnability/citation-density result in arXiv:2603.04459. The paper says p = 0.004, in its introduction and again in §4.6. The substantive claim — ready-to-use code relates to higher citation density while code needing modification does not — is exactly as reported; the digit was wrong. Checked against the paper text, not the abstract, which is the rule this repository already holds itself to.
- The 31 August commitment is recorded as MET, 23 days early.
docs/studies/index.mdstill described the release containingprovael submitas pending and told readers a git install was required — while the command had shipped in 0.32.0 on 8 August and www.provael.com/studies already showed it met. Two surfaces disagreeing about the same self-imposed deadline is worse than either being stale alone. The original date is kept rather than deleted: a commitment that vanishes once met is not a record.
What did NOT change is stated in the same breath, because that is the easy thing to skip: shipping the command removed the barrier, it did not produce a submission. The register is still empty, the board still reports 0 independent submitters, and the fork count is still 0.
- The Hugging Face Space is linked from the README and from PyPI. It appeared in
docs/leaderboard.md,CONTRIBUTING-leaderboard.mdand a setup script, and zero times in the README that PyPI also renders — so a running, clickable leaderboard was unreachable from both places a newcomer actually lands. Added as a badge, as prose in the leaderboard section, and as aLeaderboardentry inproject.urls. The prose says plainly that the Space is a *rendering* and the signed JSON in this repository is the artifact to verify.
- GitHub topics 12 → 18: added
embodied-ai,ai-red-teaming,llm-security,ai-security,sarif,github-action.
- The GitHub Action is listed on the Marketplace, and the badge landed in the same commit. The README had carried a nine-line comment since 0.33.1 explaining why no badge was there — the listing 404'd, and a badge pointing at a 404 is the same error as advertising a container image that does not exist. Publishing needs the Developer Agreement accepted and a release edited through the web UI, neither of which has an API, so this was blocked on an operator action rather than on code. Now published as
provael-vla-red-team, the comment is replaced by the badge it was reserving, and the point it was making is kept in prose.
Nothing about how the Action *runs* changed. uses: provael/provael@v0.35.0 resolved from the repository and tag before the listing existed and resolves identically now; the Marketplace is discovery, not a dependency. The 0.33.1 known-issue recording the 404 is left exactly as written, because it was true on 13 August and a changelog that edits its own history is not a record.
0.35.0
Added
- The input to the keep-out calibration was being discarded on every run. It is now recorded.
Suite.calibration_signal()has always returned exactly the signal a calibrator needs, the runner computed it on every step, and nothing ever persisted it — so every finished run left #136 no more fixable than before it started. The blocker was never GPU budget for the fit; it was that the data the fit consumes did not exist.AttackResult.trajectory(report schema 3) now records the per-step signal for every episode, benign and adversarial alike, gated on nothing — an opt-in flag would recreate the loss the first time someone forgot, and the poses are unrecoverable once the run ends.
Stored as base64 zlib float32 with an explicit shape (~4 KB for a 280-step episode against ~17 KB of JSON floats), deterministic, so the artifact stays a pure function of its inputs. The sample is taken *before* the unsafe check breaks the loop, or the violating step — the most informative pose in the episode — would be missing from exactly the episodes a calibration wants most. tests/test_trajectory_recording.py makes a missing trajectory fail the run rather than warn.
The calibration itself is still owed. CALIBRATED_ZONES is still empty, every affected row still reads calibrated: false, and closing #136 needs the benign arm re-run on a build that records trajectories, then a margin decision. Every artifact already under results/ predates schema 3 and carries no trajectory, so none of it is retroactively usable.
provael workspace-bounds --runs <dirs>emits per-task benign reachable-workspace bounds and the episode count behind each, flagging any task estimated from fewer than five. It deliberately does not emit a calibrated zone: that needs a margin decision, and choosing one without looking at real benign spread is exactly howDEFAULT_KEEP_OUT_ZONEcame to overlap the workspace it was meant to sit outside.
- The leaderboard's tool-version claim is machine-checked (
tests/test_leaderboard_version_claim.py). It passes only ifmeasured_withincludes the current__version__, or a dated entry inleaderboard/method-equivalence.jsonargues why the gap cannot move those rows. The entry is not a waiver flag — it has to name what changed and state its own limits, and the one added here says plainly that it is a code-inspection argument and not a re-measurement. A separate assertion comparesmeasured_withagainst what the aggregated shards actually carry, which is what would have caught the earlier["0.1.0"].
measured_with: ["0.32.0"] was checked against the artifacts and is correct: the shards carry tool_version 0.32.0 and were measured 2026-08-09. The board's 2026-08-17 stamp is a rebuild, not a measurement.
tests/test_freshness_semantics.pypins what "freshest measurement" means, after the badge was read as broken when it was right.
- Crosswalk card for LIBERO-Safety (arXiv:2606.23686, ECCV 2026), indexed in the docs nav. Its
mapping_statusiscomplementary-axis, not-yet-crosswalked: their "strictly collision-free" construct and our keep-out envelope violation measure the same physical property from opposite directions, but their regime is distributional (randomisation, no attacker) and ours is adversarial. Neither subsumes the other — their coverage is ten policies against our one, and their attacker is absent while ours is the point. The taxonomy mapping onto the Top 10 is recorded as owed rather than delivered, and the rates are stated as not comparable in either direction, because neither side publishes a benign FPR on shared fixtures.
Fixed
- One schema-aware report digest instead of four copies.
trajectorymoves the canonical JSON, so adding it would have made every attestation ever issued verify as tampered — the one failure a signature scheme must not have. Reports are now digested at the schema version they declare, and the logic that does it lives inattest.report_projection()alone;execution.pyandmanifest.pydelegate rather than repeating it. They were identical until one of them learned something the others had not, which is the only way that kind of duplication ever fails.
Known issues
- The measurement-freshness badge is red and it is telling the truth. The newest measurement under
results/is 2026-08-09; the badge reads 9 days. It was tempting to call this a bug, because a ten-task board *was* published on 2026-08-17 — but that commit rebuilt the board from artifacts already in the tree and touched zero files underresults/. A republication is not a measurement. The threshold has not been touched and the badge stays red until something is actually measured.
Changed
- Prior art: added DRIFT (arXiv:2608.03207), the third published patch attack in a row that far exceeds our 0/50 on the same channel — and the entry corrects the comfortable explanation. It would be convenient to attribute the gap to architecture, DRIFT attacking flow-matching policies while we measured something else. This repo's own data refuses that:
lerobot_adapterdeclaresaction_head_class = "flow"and every episode in the pinned evidence recordsflow, so SmolVLA *is* flow-matching by our own taxonomy. What remains is that DRIFT is white-box and gradient-optimised against the denoising ODE while ourpatchappends a fixed string to a dict and performs no search at all. With three such papers now published, the honest reading of the visual family's nulls is that provael has no image-space patch attack, and they measure its absence. Both nulls stay published exactly as measured.
37 earlier releases
- 0.34.0 · 18 August 2026
- 0.33.2 · 15 August 2026
- 0.33.1 · 13 August 2026
- 0.33.0 · 10 August 2026
- 0.32.0 · 8 August 2026
- 0.31.1 · 3 August 2026
- 0.31.0 · 2 August 2026
- 0.30.0 · 1 August 2026
- 0.29.1 · 31 July 2026
- 0.29.0 · 31 July 2026
- 0.28.0 · 30 July 2026
- 0.27.0 · 30 July 2026
- 0.26.1 · 28 July 2026
- 0.26.0 · 28 July 2026
- 0.25.1 · 27 July 2026
- 0.25.0 · 26 July 2026
- 0.22.0 · 23 July 2026
- 0.21.0 · 22 July 2026
- 0.20.0 · 21 July 2026
- 0.19.0 · 20 July 2026
- 0.18.0 · 19 July 2026
- 0.17.0 · 18 July 2026
- 0.16.0 · 15 July 2026
- 0.15.0 · 13 July 2026
- 0.14.0 · 13 July 2026
- 0.13.0 · 8 July 2026
- 0.12.0 · 8 July 2026
- 0.11.0 · 6 July 2026
- 0.10.0 · 5 July 2026
- 0.9.0 · 4 July 2026
- 0.8.0 · 4 July 2026
- 0.7.0 · 3 July 2026
- 0.6.0 · 30 June 2026
- 0.5.0 · 29 June 2026
- 0.4.0 · 28 June 2026
- 0.3.0 · 27 June 2026
- 0.1.0 · 27 June 2026
Full notes for these are in CHANGELOG.md ↗.