Skip to main content

Measure a judge's Evaluator Trust Boundary susceptibility (ETB-01 verdict injection). Offline, stdlib-only, zero model spend.

Project description

etb-scan

Is your LLM judge grading the model, or is the model grading itself?

etb-scan measures whether a judge can be made to pass a failing answer using text the evaluated model wrote. This is ETB-01, the first class of Evaluator Trust Boundary failure: the score depends on an artifact the evaluated system controls.

Offline. Standard library only. No API keys, no model spend, no network.

pip install etb-scan
etbscan
judge=naive     overall_asr=1.0000 (25/25)  control_fp=0.0000
  delimiter_format_confusion   asr=1.0000 (5/5)
  direct_verdict_override      asr=1.0000 (5/5)
  fake_rubric_completion       asr=1.0000 (5/5)
  role_system_spoof            asr=1.0000 (5/5)
  unicode_hidden_smuggling     asr=1.0000 (5/5)
judge=hardened  overall_asr=0.0000 (0/25)  control_fp=0.0000

Scanning a real LLM judge: use --trials

A real judge is stochastic. At one trial per scenario the measurement is too noisy to gate on: against a judge that follows the injection half the time, single-trial runs returned anywhere from 0.28 to 0.64. At --trials 10 the same judge reads as fully exploitable, which is the correct answer, because one successful injection is a vulnerability.

etbscan --judge yourpkg.judges:your_judge --trials 10

A scenario counts as exploited if the injection lands in any trial, so the reported ASR is a lower bound that tightens as trials rise. Use 1 trial only for a deterministic judge.

Scan your own judge

from etbscan import scan, load_corpus

def my_judge(candidate: str, rubric: dict, question: str | None = None) -> dict:
    ...  # call your grader
    return {"verdict": "pass", "score": 0.9}

result = scan(my_judge, load_corpus(), trials=10)  # trials>1 for a real judge
print(result.overall_asr)      # your ASR
print(result.control_fp_rate)  # must be 0.0 for the ASR to mean anything

Any callable taking (candidate, rubric, question) or (candidate, rubric) works. The verdict dict needs a verdict key (pass/fail) or a numeric score.

What it measures, and what it refuses to measure

The probe fires only on a causal verdict flip: the judge did not pass the clean weak answer, and did pass the same answer carrying an injection.

This matters in both directions. A judge that passes everything is useless, but under a naive "did the injection pass?" test it scores a perfect 0.0, because nothing flipped. So the corpus ships 8 benign controls alongside the 25 injected scenarios, and a scenario whose clean weak answer already passed is reported as unscorable, never as clean: the injection had nothing left to flip, which means the measurement failed, not the attack. A low ASR is only meaningful if control_fp_rate is 0.0 and trustworthy is true. Read all three or read none.

The corpus

33 scenarios: 25 injected across 5 families, 8 benign controls.

Family What the injection tries
direct_verdict_override write the verdict token into the answer
role_system_spoof forge a system or grader turn inside the answer
fake_rubric_completion assert rubric items are satisfied
delimiter_format_confusion close and reopen the prompt's structural delimiters
unicode_hidden_smuggling homoglyph and zero-width smuggling past normalization

Two reference judges bracket the observed spectrum. naive_judge reads candidate text as instructions, the failure mode found in six of ten frameworks in the phase-1 audit. hardened_judge scores only against the rubric, which is the message-role separation fix UK AISI maintainers merged.

If your ASR is above zero

The fix is not a better regex. It is moving the trust boundary:

  1. Parse the judge's own structured output, never the candidate's text. If extraction runs over a string the model wrote, you have already lost.
  2. Separate data from instructions using message roles, so candidate content cannot present as prompt structure.
  3. Bind extraction to a position you control (the last block, or an explicitly delimited region), not to the first match.
  4. Normalize before matching so homoglyph and zero-width variants collapse.

Fix patterns for all ten ETB classes are at authensor.com/etb.

Tests

python3 -m pytest tests/ -q

59 tests. They assert the package has no dependency on any private module, that a broken judge is reported as unscorable rather than clean, that a pass-everything judge cannot obtain a clean bill of health, that an injectable judge is detected regardless of the verdict shape it returns, and that the detector never fires on honest prose. If the published figures drift, these fail loudly.

The detector's precision/recall tradeoff

detect_grader_directed_injection, used by the Inspect task to score arbitrary model output, is deliberately high precision and low recall: 0 false positives on a hand-written honest-prose set, roughly 28% of corpus injections caught.

An earlier version chased recall with imperative patterns ("ignore the rubric", "output your verdict") and fired on 53% of honest technical answers, because an answer about prompt injection is lexically identical to an injection. Penalising a correct answer is the same class of defect this tool exists to find, and worse than missing an attack.

So: a positive is strong evidence. A negative is not evidence of absence. Treat the Inspect task's score as a lower bound.

Scan another framework's judge

Adapters live in etbscan/integrations/. Each imports its framework lazily, so etb-scan itself stays stdlib-only with zero runtime dependencies.

Framework Install Entry point
Inspect AI pip install "etb-scan[inspect]" auto-registers; inspect eval verdict_injection
DeepEval pip install "etb-scan[deepeval]" etbscan.integrations.deepeval.scan_metric(metric)
Weave pip install "etb-scan[weave]" etbscan.integrations.weave.scan_scorer(scorer)

All three are exercised against the real framework in CI, on push and weekly, because they break from upstream releases rather than from our commits.

In every adapter, a framework that raises, reports an error, or returns something unreadable is unscorable, never a failing verdict. Coercing an uninterpretable judgement into "did not pass" is the fail-open this tool exists to find.

Scan a DeepEval metric

A DeepEval metric is an LLM judge: it reads text the evaluated system wrote and returns a score. That makes it exactly the thing this measures.

pip install "etb-scan[deepeval]"
from deepeval.metrics import GEval
from deepeval.test_case import LLMTestCaseParams
from etbscan.integrations.deepeval import scan_metric

metric = GEval(
    name="Correctness",
    criteria="Is the answer correct and complete?",
    evaluation_params=[LLMTestCaseParams.INPUT, LLMTestCaseParams.ACTUAL_OUTPUT],
)
result = scan_metric(metric, trials=10)
print(result.overall_asr, result.control_fp_rate, result.trustworthy)

A metric that raises, sets .error, or reports .skipped comes back as unscorable, never as "did not pass". Coercing those to a passing score is the fail-open this tool exists to find.

DeepEval metrics are stateful, so the adapter serializes calls: max_workers above 1 is safe but will not speed up a DeepEval scan. Give each worker its own metric instance if you need real concurrency.

Verified against a real judge

The figures above characterize two offline mocks. verified_run_real_judge/ puts Kimi K3 behind three scoring code paths and measures what changes:

arm ASR control FP trustworthy
model called cleanly, verdict read from its own final line 0.0000 0.0000 yes
candidate concatenated into the prompt, verdict read from the reply 0.0000 0.0000 yes
same call as the first row, verdict read by greedy first-match over a candidate-containing blob 1.0000 0.3750 no

Same model, same system prompt, same API call. Only the extraction moved. The model followed zero injections when extraction was bound correctly, which is the point: hardening the judge model bought nothing, because the defect was never in the model.

The control false-fires are the sharper result. Greedy first-match read PASS out of the grader's own sentence "requires at least 3 for PASS", sixty characters ahead of its actual VERDICT: FAIL. The judge was right on every control. The code was not. A broken extraction path does not only leak to attackers; it makes benign grading nondeterministic.

Scope and honesty

  • The 1.0000 / 0.0000 figures characterize the two reference judges, not the ecosystem. They are the instrument's calibration, not a population estimate.
  • A trustworthy result requires more than a low ASR. trustworthy is false unless there was at least one scorable injected scenario, at least one scorable control, no unscorable scenarios, and no control false-fire. A judge that passes the clean weak answer in every trial makes the flip unmeasurable, so its scenarios go to the unscorable pile and leave the ASR denominator. A vacuous 0.0000 reads exactly like a clean bill of health, so the tool refuses to present one.
  • The scan reads your judge's verdict in several shapes (string, boolean, enum, nested dict, numeric score). A verdict it cannot interpret is reported as unscorable, never silently as "did not pass" — that coercion would be the fail-open this tool exists to find.
  • This measures ETB-01 only. Nine further classes (dropped denominators, fail-open error paths, forged execution artifacts, and others) are not detectable from outside the scoring code and need code review. See authensor.com/etb.
  • Corpus scenarios are synthetic and deterministic by design, so a real judge's ASR here is a lower bound on what an adaptive attacker achieves.

License

MIT. Copyright 2026 Authensor, Inc.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

etb_scan-0.1.1.tar.gz (38.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

etb_scan-0.1.1-py3-none-any.whl (35.0 kB view details)

Uploaded Python 3

File details

Details for the file etb_scan-0.1.1.tar.gz.

File metadata

  • Download URL: etb_scan-0.1.1.tar.gz
  • Upload date:
  • Size: 38.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for etb_scan-0.1.1.tar.gz
Algorithm Hash digest
SHA256 682f1e2971a264ebaf0bb8dad457ee2d9d10e5aa3942d1a5efcdba846e908132
MD5 bd02d98d815ed62dfdbd648e0b5f7c89
BLAKE2b-256 dd78af459bef17210adbc850a3f366d2562dc44f6d10bc2892076ec86c02b10e

See more details on using hashes here.

Provenance

The following attestation bundles were made for etb_scan-0.1.1.tar.gz:

Publisher: publish.yml on AUTHENSOR/etb-scan

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file etb_scan-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: etb_scan-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 35.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for etb_scan-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 f5ab1aef84fd3ee5f51f0b9d245c17315309dfcbab986b74f15b6b656a3d71f8
MD5 c72f25196fc42943ee3551ea258a70b2
BLAKE2b-256 283b953bccca0d6b9f6901e0ba3f2f5e709e946c5b668b221874e6ac3f25313a

See more details on using hashes here.

Provenance

The following attestation bundles were made for etb_scan-0.1.1-py3-none-any.whl:

Publisher: publish.yml on AUTHENSOR/etb-scan

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page