2026Author
Crucible
Ship a prompt or swap a model and you have no honest way to know whether the agent just got less safe, because LLMs fail differently every run and a single test pass tells you almost nothing.
The result
Point Crucible at any agent and it reports per-category hold rates against a 61-test suite with the exact inputs that broke it, then fails your CI build when a change makes the agent measurably less safe.
Crucible is an adversarial red-teaming harness for LLM agents, built from scratch in Python with zero runtime dependencies. Its test suite is organized around the OWASP LLM Top 10: prompt injection, jailbreaks, secret-leakage, hallucination, robustness, and over-refusal. It runs each attack many times, scores the results honestly, and surfaces everything on a live web dashboard.
- Framing
- OWASP LLM Top 10
- Suite
- 61 tests
- Scoring
- Severity-weighted
- CI
- Regression gate
Architecture
Target agent
any LLM agent under test
Adversarial suite
OWASP LLM Top 10, 61 tests
N-trial runner
repeat each attack
Judges + heuristic detectors
inconclusive calls excluded
Scoring
per-category hold rates, severity-weighted
Live dashboard
hold rates + breaking inputs
You aim Crucible at an agent and it runs the OWASP-aligned suite against it. Each test is fired repeatedly rather than once, so a rare failure does not hide behind a lucky pass. A mix of model judges and clearly-labeled heuristic detectors classifies every response as held, broken, or inconclusive. Those results roll up into per-category hold rates and a single severity-weighted risk score, published to a dashboard alongside the precise inputs that caused each break. The same numbers feed a CI gate, so a regression trips the build instead of reaching production quietly.
Key decisions and trade-offs
Hold rates over N trials, not a single pass or fail
LLM output is nondeterministic, so one verdict is noise. Running each attack many times and reporting the fraction the agent holds turns a coin flip into a measurement you can compare across prompt and model changes, and it exposes intermittent failures that a single pass would paper over.
Inconclusive judge calls leave the denominator
When the judge itself is unsure, counting that trial as either a pass or a failure would fabricate confidence the tool does not have. Crucible excludes inconclusive calls from the rate entirely and labels heuristic detectors as heuristic, so the reported number means exactly what it says. Honest and slightly smaller beats impressive and wrong.
A severity-weighted score, not a raw count
A leaked secret and a mild over-refusal are not the same failure, and averaging them flat would let low-stakes wins mask a dangerous one. Weighting by severity keeps the headline number aligned with real risk rather than test volume.
A CI regression gate mapped to the OWASP LLM Top 10
Safety that is only ever checked by hand drifts. Gating the build on the same category hold rates (injection, jailbreaks, secret-leakage, hallucination, robustness, over-refusal) makes an agent getting less safe a failing check a reviewer sees on the PR, not a surprise found in production.
Results
- 61-test suite spanning the six OWASP LLM Top 10 categories.
- Per-category hold rates reported over repeated trials, with the exact breaking inputs.
- Severity-weighted risk score.
- CI regression gate that fails a build when an agent gets measurably less safe.
- Live web dashboard.
Inside the repo
The source is public, so here is a map before you open it: what each core file does and how they fit together. Everything below is taken straight from the modules themselves. Read the source.
crucible/
- attacks.pyThe adversarial inputs and the built-in library of them: each attack bundles a prompt to send with how a failure is recognized.
- targets.pyThe works-on-any-agent seam: the thin interface that lets Crucible point at a local function, an HTTP endpoint, or a hosted agent.
- detectors.pyThe honest heart: how it decides whether an agent actually failed an attack, rather than assuming it did.
- runner.pyRuns the suite against a target over N trials, which is how it handles the nondeterminism problem instead of pretending one sample is a verdict.
- report.pyTurns raw results into the real deliverable: a readable reliability report with the exact inputs that broke the agent.
- compare.pyDiffs two runs to catch reliability regressions - the feature that makes this a CI gate and not a one-off audit.
- history.pyThe longitudinal view: reliability tracked across many runs over time.
- scorecard.pyRenders a run as a self-contained SVG scorecard, so someone browsing the repo sees the result without running anything.
- badge.pyA shields-style SVG badge generated from a run, for the README.
- target_config.pyBuilds an HTTP target from a declarative JSON config, so testing a deployed agent needs no code.
- io_formats.pyLoad attacks from a file and export results as JSON, the two extensibility seams.
- cli.pyThe command-line entry point; compare_cli.py is its counterpart for diffing two saved runs.
- web.pyA local dashboard for browsing a run.
How it fits together
Read attacks.py and detectors.py together: an attack is only meaningful next to the rule that decides whether it landed, and detectors.py is where the honesty of the whole tool lives. targets.py is the adapter that lets any agent be tested, runner.py executes the suite over repeated trials, and report.py turns that into something a human reads. compare.py plus history.py are what turn a single audit into a regression gate you can fail a build on.
- Tests
- Unit-tested across six test modules in tests/
- Stack
- Python, zero runtime dependencies