# Which invariants prevent adversarial-agent scoring harnesses from silently passing regressions **Date of Record:** 2026 November 12 **Scope:** Applies to LITMUS-style safety benchmark harnesses that score adversarial agent behavior and gate regressions on aggregate metrics with per-scenario pass/fail outcomes. Does not apply to pure red-team exploration runs that are not used for regression gating, nor to single-scenario unit tests where the aggregate is the scenario. ## Core Distinction A safety harness has two distinct correctness properties that are routinely conflated: *scoring correctness* (does the grader assign the right label to a given trajectory?) and *gating correctness* (does the harness fail the build when a regression exists?). The non-obvious boundary is that a harness can be perfectly scoring-correct and still gating-incorrect, because the gate is a function of an aggregate over scenarios whose difficulty, variance, and label noise are not homogeneous. The invariant that matters is not "the score is right" but "the gate is monotone in the adversarial capability it is meant to detect." A regression in one scenario class can be masked by sampling noise, by scenario weighting, or by a changed pass threshold in a *different* class — and the aggregate still looks healthy. ## What Was Observed During harness construction for an adversarial agent suite (prompt-injection resistance, tool-misuse refusal, data-exfiltration refusal, and multi-turn jailbreak persistence), three failure modes reproduced across runs: 1. **Aggregate masking under reweighting.** The suite's headline metric was a mean pass rate over scenarios. When a new scenario class was added with a low base difficulty, the mean rose even though the injection-resistance class regressed by 6 points. The gate passed. The aggregate was arithmetically correct; the gate was wrong because it was not stratified. 2. **Variance-blind thresholds.** Scenarios with stochastic decoding were scored once per run. A class with pass rate near 0.5 and n=40 has a standard error near 0.079; a 3-point regression is indistinguishable from noise. The gate used a fixed absolute delta (`< 2%` = pass) with no confidence interval, so it both false-passed real regressions and false-failed stable classes. 3. **Label-noise leakage across the boundary.** The grader itself was an LLM judge. Judge disagreement on borderline refusals was ~4%, concentrated in the exfiltration class. Because the gate compared raw pass rates across model versions, a judge-template change between runs was scored as an agent regression. The scoring was "correct" under each template; the *comparison* was invalid. A fourth, subtler mode: scenario-level `xfail`/quarantine lists were used to silence flaky scenarios. Over time the quarantine list absorbed the hardest scenarios, and the gate's effective difficulty drifted downward without any recorded change to thresholds. ## Condition of Validity The gating invariant holds only when all of the following are true: - The gate is **stratified by scenario class**, and each class has its own threshold and minimum sample size. A global mean is permitted only as a secondary, non-gating metric. - Every threshold is expressed as a **paired comparison against a pinned baseline** (same scenarios, same judge template hash, same decoding parameters), not as an absolute rate. - The delta threshold is **no smaller than the class's sampling noise floor** at the chosen n, or the comparison is repeated and aggregated with a confidence bound. - The **judge template is content-addressed** and the run fails closed if the hash differs from the baseline without an explicit baseline-rebump event. - The **quarantine list is versioned and audited**; any addition requires a recorded reason and a maximum quarantine age, and quarantined scenarios count as failures for gating purposes (or are excluded from both baseline and candidate symmetrically). If any of these is false, the harness may still produce a score, but it must not be used to gate. ## Decision Boundary The operational rule: **gate on the worst class, not the mean.** Concretely, compute per-class deltas against the pinned baseline; the build fails if *any* class's lower confidence bound on the delta exceeds its class-specific tolerance, or if any class's sample size falls below its minimum. Treat unstratified mean deltas as advisory only. When a class is quarantined below its minimum n, the gate must fail closed with a distinct `InsufficientEvidence` condition rather than silently dropping the class — dropping a class is a change to the gate's difficulty, which is itself a regression. The boundary between "advisory" and "gating" is the presence of a pinned, hashed baseline. No pinned baseline, no gate. ## Operational Effect Stratified gating with paired, noise-aware thresholds converts a harness that reported false greens into one that fails on the specific class that regressed, with a reproducible baseline hash and an auditable quarantine ledger. The cost is more frequent, narrower failures and a maintenance burden on the baseline-rebump process; the benefit is that a regression in adversarial robustness can no longer be averaged away, hidden under sampling noise, or laundered through a judge-template change. ```python import hashlib from dataclasses import dataclass from typing import Mapping class GateInvariantViolation(RuntimeError): """Raised when a LITMUS safety gate would silently pass a regression.""" @dataclass(frozen=True) class ClassResult: name: str n: int pass_rate: float baseline_rate: float min_n: int tolerance: float # max allowed positive delta (lower bound), per class noise_floor: float # standard error estimate for this class at this n def _hash_template(template: str) -> str: return hashlib.sha256(template.encode("utf-8")).hexdigest() def evaluate_gate( candidate: Mapping[str, ClassResult], baseline_template_hash: str, candidate_template_hash: str, quarantined: Mapping[str, int], ) -> None: # Fail closed on judge-template drift: a template change invalidates comparison. if candidate_template_hash != baseline_template_hash: raise GateInvariantViolation( "judge template hash differs from pinned baseline; " "rebump baseline explicitly before gating" ) if not candidate: raise GateInvariantViolation("no scenario classes present; empty gate is not a pass") for name, r in candidate.items(): # Quarantine must not silently reduce effective difficulty. if quarantined.get(name, 0) > 0 and r.n < r.min_n: raise GateInvariantViolation( f"class {name!r} quarantined below min_n={r.min_n} " f"(effective n={r.n}); insufficient evidence to gate" ) if r.n < r.min_n: raise GateInvariantViolation( f"class {name!r} has n={r.n} < min_n={r.min_n}; " "increase sampling rather than gating on noise" ) delta = r.pass_rate - r.baseline_rate lower_bound = delta - r.noise_floor # conservative lower confidence bound # Threshold must not be tighter than the class's own noise floor. if r.tolerance < r.noise_floor: raise GateInvariantViolation( f"class {name!r} tolerance {r.tolerance:.4f} is below its " f"noise floor {r.noise_floor:.4f}; gate is variance-blind" ) if lower_bound > r.tolerance: raise GateInvariantViolation( f"class {name!r} regressed: lower_bound_delta={lower_bound:.4f} " f"> tolerance={r.tolerance:.4f} (raw delta={delta:.4f})" ) ```