Skip to content

DFlash: separate "may run" from "may publish" so the bench is testable - #815

Merged
anupsv merged 1 commit into
mainfrom
feat/dflash-dry-run-interlock
Jul 30, 2026
Merged

DFlash: separate "may run" from "may publish" so the bench is testable#815
anupsv merged 1 commit into
mainfrom
feat/dflash-dry-run-interlock

Conversation

@anupsv

@anupsv anupsv commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

The problem

The enablement interlock required official_scoring_enabled and
reference_baseline.publication_allowed — the two go-live fields — before the job
would run at all.

So the only way to exercise this pipeline was to declare the track live first. The
correctness gates, every fail-closed guard, and the newly reused serial gate leg
could never be validated before the switch they gate had already been thrown.
That's backwards, and it's how the fifth copy of the decode floor survived review —
it was only ever found by dispatching for real.

The split

Enablement is two questions, now asked separately:

Question Requires Applies to
may this job RUN? confirm_track_enabled, matching track_id, non-empty golden pins both kinds of dispatch — a dry run is still a run on the operator's box
may it PUBLISH a ranked score? official_scoring_enabled and publication_allowed run_benchmark=true

A gates-only dispatch is admitted with a ::notice:: and a
MLXFAST_DFLASH_DRY_RUN_UNENABLED marker.

It publishes nothing. It writes a sealed local gates-score.json plus the
gates-only correctness artifacts; every timed and scoring step is gated on
inputs.run_benchmark. I verified this by enumerating the 51 of 63 steps that run
when run_benchmark=false — none reach a ranked score.

Ordering makes it useful: the four reused serial gates are steps 31–39, ahead of
the two fail-closed operator blockers (empty prompt pool at 41, golden row-count at
42). A dry run exercises the gates, then stops cleanly.

Checked twice, on purpose

The PUBLISH check now also runs at Compute DFlash score and enforce floor, which
additionally refuses any dispatch carrying the dry-run marker. A guard 49 steps
upstream of the thing it protects is a guard whose coverage depends on step order.

Tests: DFlashEnablementInterlockTests, 7 tests

Six read the step text. The seventh executes the real step body over the full
truth table
— because the Amendment 23 redactor gap would have passed a
text-matching test (the arms were absent, not misspelled).

case expected
unenabled + ranked + confirmed refuse (the attack the split could have introduced)
unenabled + dry run + confirmed admit (the point)
unenabled + dry run + unconfirmed refuse
enabled + ranked + unconfirmed refuse
enabled + ranked + confirmed admit
enabled + dry run + confirmed admit

Attacked before trusting it. Inverting the ranked refusal, and weakening the
confirm interlock to ranked-only, are both caught — each naming the exact case that
was wrongly admitted.

One test pins that the fixture stays inert on main (official_scoring_enabled
false, publication_allowed false, timed_prompt_pool empty), so "completing" this
by quietly flipping the flags fails the suite instead.

Verification

  • swift test: 526 tests, 21 suites, green
  • swift build -c release: clean
  • Workflow YAML parses

Next

With this merged, a gates-only dispatch can validate the four reused serial gates
against real hidden material for the first time — they are currently wired and
unit-tested but have never executed.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.

The enablement interlock required official_scoring_enabled AND
reference_baseline.publication_allowed -- the two go-live fields -- before the
job would run AT ALL.  So the only way to exercise this pipeline was to
declare the track live first, and the correctness gates, the fail-closed
guards and the reused serial gate leg could never be validated before the
switch they gate had already been thrown.

Enablement is two questions and they are now asked separately:

  may this job RUN?      confirm_track_enabled, matching track_id, non-empty
                         golden pins.  Unchanged, and still mandatory for
                         BOTH kinds of dispatch -- a dry run is still a run
                         on the operator's box.
  may it PUBLISH a
  ranked score?          official_scoring_enabled AND publication_allowed.
                         Required for run_benchmark=true.  A gates-only
                         dispatch is admitted with a ::notice:: and a
                         MLXFAST_DFLASH_DRY_RUN_UNENABLED marker.

A gates-only dispatch produces no ranked score: it writes a sealed local
gates-score.json plus the gates-only correctness artifacts, and every timed
and scoring step is gated on inputs.run_benchmark.  Ordering makes it useful
-- the four reused serial gates are steps 31-39, ahead of the two fail-closed
operator blockers (empty prompt pool at 41, golden row count at 42) -- so a
dry run exercises the gates and then stops cleanly.

The PUBLISH check is now made TWICE: at the enablement step and again at
"Compute DFlash score and enforce floor", which also refuses a dispatch
carrying the dry-run marker.  A guard 49 steps upstream of the thing it
protects is a guard whose coverage depends on step order.

DFlashEnablementInterlockTests, 7 tests.  Six read the step text; the
seventh EXECUTES the real step body over the full truth table, because the
Amendment 23 redactor gap would have passed a text-matching test -- the arms
were absent, not misspelled.  Attacked before trusting: inverting the ranked
refusal and weakening the confirm interlock to ranked-only are both caught,
each naming the exact case that was wrongly admitted.

The fixture stays inert on main (official_scoring_enabled false,
publication_allowed false, timed_prompt_pool empty) and a test pins that, so
"completing" this by flipping the flags fails the suite instead.

swift test: 526 tests, 21 suites, green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@anupsv
anupsv requested a review from a team July 30, 2026 22:44
@anupsv
anupsv had a problem deploying to benchmark-private-prompts July 30, 2026 22:44 — with GitHub Actions Error
@anupsv
anupsv merged commit 1c45e24 into main Jul 30, 2026
2 of 3 checks passed
anupsv added a commit that referenced this pull request Jul 31, 2026
…819)

Found by an independent review of #814-#817, not by any failing run.  This is a
defect I introduced, and it is the worst shape available here: an ITERATED
ORACLE for the hidden golden.

The mechanism.  "Stage DFlash correctness artifacts" became always() in #816 so
that a FAILED gates-only run still yields diagnostics.  That is right in itself,
but it made the step run in the one state its own comment assumed away: when
"Validate sealed gates score" did NOT run.

On a failed hidden-gates run the gates pass writes gates-score.json and THEN
exits nonzero.  The validation step is skipped along with everything after it,
but the always() staging step still runs, stages the file, and uploads it.  That
sealed score's .metrics carry first_failing_step, expected_token and
actual_token -- values derived FROM THE HIDDEN GOLDEN.

Three things turned a disclosure into an oracle:

  - deny-private-artifacts.sh is NAME- and SIZE-based.  It cannot see values
    inside a permitted filename; a gates-score.json containing
    expected_token: 31337 passes it with exit 0.
  - The redaction machinery never runs in gates-only mode ("Redact DFlash
    benchmark failure" is failure() && inputs.run_benchmark), so a failed dry
    run gets no redacted category AND the raw file -- the exact inversion of
    the design.
  - A gates-only failure charges no attributable-failure budget, so the run is
    repeatable for free.  One hidden token per run.

redact-benchmark-failure.sh states the invariant this broke in its own header:
"a failing score.json must never be uploaded raw."

Serial does not have this exposure.  Its hidden-gate steps are gated on
inputs.run_benchmark, so its dry run never holds hidden material at all, and its
correctness staging additionally requires the validation step to have SUCCEEDED
on submission branches.  The DFlash copy dropped that guard and removed the
run_benchmark gating -- so #814 created the arm and #815 made it routinely
reachable.

The fix re-asserts the sealed-score predicate INLINE in the staging step, using
the same jq "Validate sealed gates score" uses, and DROPS a score that fails it
rather than failing the step: the run has already failed for its own reason and
the other artifacts are still worth having.

public-gate-report.json gets the guard serial has and the DFlash copy dropped.
Its .error can embed sandboxed-worker stderr -- text the SUBMITTED code
controls -- so on a submission branch it is withheld unless .error is empty.  On
an organizer ref it is organizer output and is kept even when it records a
failure.

DFlashGatesOnlyArtifactLeakTests, 4 tests, all driving the REAL step body:
the failed-score case is withheld and no hidden value reaches the staged set;
a clean score is still staged (so the guard is not a mute); the public gate
report is withheld on a submission branch and kept off one; and a structural
test asserts the inline predicate checks every field the validation step checks,
so a future tightening of one cannot silently bypass the other.

Proven load-bearing: disabling the content gate makes the leak test fail,
naming the file and the reason.

NOT fixed here, filed for follow-up: a failed gates-only run still produces no
failure_category, because the redactor is gated on inputs.run_benchmark.  Adding
a failure() && !inputs.run_benchmark arm needs its own change; withholding the
raw file closes the leak, surfacing a category is the improvement on top.

swift test: 534 tests, 24 suites, green.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
anupsv added a commit that referenced this pull request Jul 31, 2026
#815 split the enablement interlock so the DFlash pipeline could be
validated WITHOUT flipping the go-live flags: run_benchmark=false
publishes no score, so it is admitted against an inert track. The point
of that split is to exercise "DFlash correctness and parity gate
(untimed)" -- the one gate this track owns.

It could not. Two steps above that gate were not gated on
inputs.run_benchmark:

  * "Select hidden DFlash timed target from the pool", which fails
    closed on an empty timed_prompt_pool; and
  * the timed-golden half of "Prepare hidden DFlash goldens", which is
    addressed BY that selection.

The pool is deliberately empty on main (pinned by
theTrackFixtureRemainsInertOnMain), so before go-live EVERY dispatch
died red at pool selection -- one step short of the gate the dry run
exists to run. The dry run validated nothing the serial pipeline had
not, and operators were being taught to read red as success. A dry run
also downloaded a hidden timed golden it would never time against.

Scope both to the ranked path. The DFlash CORRECTNESS golden is pinned
independently of the pool, so the untimed leg -- including the parity
gate -- now runs and a clean dry run ends GREEN.

The ranked path is unchanged and is not relaxed:

  * a ranked dispatch still runs the selection step and still fails
    closed on an empty, absent, null or malformed pool. Losing per-run
    sampling would lose contract layer L6 (anti-lottery), under which a
    failed ranked run becomes a free retry and every output-side gate
    degrades into submit-until-green;
  * the timed golden's pin check against the SAMPLED entry is unchanged;
  * the row-count preflight is scoped, not relaxed -- it stays
    unconditionally binding whenever the run times;
  * a new, independent refusal fires if a ranked run ever reaches the
    download with an empty selection. Honest scope: this is defence in
    depth and a named diagnostic, not the only barrier --
    download-r2-object.sh already refuses an empty key -- but it names
    the cause instead of dying on a generic exit 2 in a helper.

Tests: Tests/MLXFastTests/DFlashDryRunReachesItsGateTests.swift, 8 tests.
Six of them EXECUTE the real step bodies (pool selection, golden
preparation) against sandboxes; the step-set tests evaluate the real
`if:` expressions with an evaluator that hard-fails on any construct it
does not recognise rather than guessing.

Verified by attack: reverting the workflow to origin/main fires 3 tests
(8 issues), and five separately-injected defects -- a ranked run allowed
past an empty pool, the empty-selection refusal removed, the whole
goldens step gated ranked-only, the parity gate itself gated ranked-only,
and the timed pin check dropped -- each fire the test that names them.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant