-
-
Antibody wordmark
-
the header of one antibody file
-
the flow that fires the agents when an issue is created
-
real curl: the decoy file leaks before the fix, 400 after
-
the six real antibodies on GitLab
-
GIF
the Warden catches a deleted test
-
every exploit blocked, but 3 real tests fail, so it isn't a fix
-
the 4 agents registered on GitLab
-
the real green pipeline
-
the copy of the checker inside the change clears itself; the base copy still blocks
-
170/1,030 · 41% · ~19.5%, each with its source
-
one shared helper, two leaking routes
-
issue → Planner → Builder → Red → Warden → production
-
the same three requests after the fix: 200, 400, 400
-
what Copilot, Snyk and GitLab Duo each check
The film · See it working · Architecture · The loop · Run it · Research
The vulnerability is real, and so is the fix
The decoy leaks at HTTP 200. After the root-cause fix, the same request — plus the sibling route nobody reported and the URL-encoded variant — all return 400.

The film
antibody-story.mp4 — 4:09, narrated, twenty sections in three acts. A marker
draws each diagram on a black field while the voice explains it; the camera pushes
into one part of a single fixed board and pulls back to show where it fits. Every
claim is answered by real footage from this repository in the same breath.
![]() |
![]() |
| §5 One helper, two routes. The one nobody reported leaks too. | §6 Three measured findings, each with its source. |
![]() |
![]() |
| §9 The exploit is fired: 200 for the real file, 400 for both attacks. | §17 How the pieces sit together, drawn stroke by stroke. |
| Act | Sections | What you see |
|---|---|---|
| I — a fix that looked finished | 1–5 | A merged MR with every check green, then the question. A live curl for ../secret.txt returns HTTP 200 and the decoy leaks; /preview leaks too. |
| II — not one bad day | 6–8 | Three cited findings. What Copilot Autofix, Snyk Agent Fix and GitLab Duo each re-check. The patch is marking its own homework. |
| III — make it prove itself | 9–20 | The exploit fired after the fix. The bank on GitLab and an antibody's own header. The Warden blocking a deleted test. Both traps. The Duo agents and flow, the architecture, the MCP tools, the pipeline's numbers, and the mark. |
§4 — the breach, live. ../secret.txt returns HTTP 200 and the decoy leaks:

§13 — the Warden refuses a deleted test. The agent removes the failing test; the
checker reads the diff and returns exit 1:

Earlier cuts are kept and build from the same repository: antibody-board.mp4
(2:40, one diagram), antibody-full.mp4 (4:28, twelve chapters) and
antibody-film.mp4 (a 30-second trailer).
make film-story # the drawn film: voice, score, layout sweep, render, encode
make film-story-sweep # check every section's layout without rendering
make film-board # the 2:40 board film
make film-full # the 4:28 long cut
Why the film cannot drift from the project
Every number on screen is read from the repository at render time — the
antibody count from tests/antibodies/, coverage from coverage.xml, the test
count from a real pytest collection — so a stale claim is impossible by
construction. Every terminal line is a verbatim capture stored in
film/real/breach.json, not a re-typed transcript.
The narration is synthesised offline with Piper and measured: make_voice.py
writes each line's real start and end, and both the picture and the music are cut
to those times, so they cannot drift apart. The score is sine tones and filtered
noise, ducked under the voice by a sidechain keyed on the narration itself —
royalty-free by construction, as the rules require.
Every diagram is an SVG path revealed by its own dash offset from seek(t), so a
frame depends only on its timestamp: no CSS animation, no timers, nothing carried
between frames. That is also what lets the render split across parallel workers.
Layout and legibility are verified mechanically rather than by eye.
film/sweep_story.py walks the real cue list and, at every beat where the camera
is at rest, measures each visible element against the frame, the caption bar and
its neighbours using rendered geometry; it must report zero faults before a render
starts. film/check_legible.py measures the smallest text in every section as
rendered on screen, after the section is scaled to fit.
See film/STORY.md for the storyboard and why the script is
shaped as a story, and film/GRADIENT.md for every claim
checked against its primary source.
The problem
Code generation is solved. What happens after it is not.
AI agents now write fixes and open merge requests on their own. Four things go wrong, and almost nobody checks for them:
| What goes wrong | How often | Who measured it |
|---|---|---|
| A patch passes every test and is still exploitable | 170 of 1,030 agent repairs | Tampere University |
| A fresh attack bypasses an already-"fixed" patch | 41% of runs | P2Patch, Simon Fraser |
| The agent deletes the failing test instead of fixing the bug | ~19.5% of runs | Hacktrace, NTU |
| Agent changes to CI merge with no human review | 43–56% of dual-scope PRs | UNSW |
All four share one root cause: the pipeline trusts the AI's own word that the work is done and safe.
The answer
Antibody removes that trust. It treats a codebase like an immune system.
Don't trust the fixer. Make it survive an attacker. Save every attack that ever worked, and re-run it forever. And watch the fixer for cheating, with a checker it cannot talk its way past.
Four things set it apart from the AI-attacks-AI tools that already exist:
- Adversarial re-attack. It doesn't re-run the old exploit. It generates a new attack against the new fix, and every successful bypass becomes a permanent requirement.
- Sibling-path hunting. If
/downloadwas broken, it checks whether/previewhas the same missing guard. The bug usually hides on a route outside the diff. - One system for both the security attack and agent-cheat detection. Existing tools do one or the other.
- Self-lowering autonomy. The pipeline drops from Hands-off to Supervised by itself when a change touches CI, auth or
CODEOWNERS, because a change must never validate itself.
Architecture
The cast
| Who | AI? | Job |
|---|---|---|
| Planner | Yes | Reads the issue, writes the spec and security acceptance criteria, posts the final audit. |
| Builder | Yes | Writes the fix on a shadow branch and hardens it when attacked. |
| Red | Yes | Writes real exploits, re-attacks the fix, hunts sibling routes, saves antibodies. |
| GreenOps | Yes | Skips the expensive loop for non-security changes, picks the low-carbon region, computes SCI. |
| Warden | No — plain Python | Deterministically detects test tampering and hidden scope creep. Cannot be prompt-injected. |
The Warden being non-AI is a deliberate, defensible choice: the Tampere study found LLM reviewers missed exactly these failures. A git-diff checker cannot be flattered or argued with, and returns the same verdict every time.
See it working
Everything below is a real capture from this repository. No mock-ups.
The nine-stage pipeline, green on GitLab
Every executed stage passed, including the container build pushed to the project registry. configure and monitor are correctly skipped until a cloud project and a schedule exist.

Attacks on the owned decoy are refused
Encoded path traversal, the sibling route, a non-HTTP scheme, and a cross-user object read are all blocked, while legitimate requests still work.
make demo-attack

The Warden catches the agent cheating
Three real commits in a throwaway clone: deleting a test is blocked, editing the pipeline alongside an app fix drops to Supervised, and an honest fix passes.
make demo-warden

The MCP tools, over HTTP
The four deterministic tools, served over HTTP and exercised by a real MCP client. Once deployed, this is the endpoint the AI Catalog registers.
make serve-mcp # then, in another shell:
python scripts/mcp_probe.py

A patch can defeat every attack and still not be a fix
This "fix" blocks path traversal by refusing every download. All eight antibody tests pass, because no attack gets through. The software is also broken. An exploit-only gate ships this; the paired oracle rejects it.
make demo-oracle

The verifier cannot disarm itself
An agent rewrites the Warden to approve everything, then deletes a test in the same change. The copy of the Warden in that change clears itself. CI never runs that copy: it extracts the Warden from the base revision first, and that one still sees the deleted test.
make demo-warden

It runs on real GitLab, not a mock-up
The four Duo agents, registered in this project's AI Catalog:

The ambient flow that fires them, triggered by a new issue:

The Planner's own analysis on the real issue — it finds the flaw in the shared helper, names the sibling route before anyone reported it, and tells the Builder not to make a cosmetic change:

The permanent bank on GitLab. Six antibodies, append-only:

Each antibody documents how it was found, in its own header:

The live dashboard
Rebuilt by the pipeline from each run's real proof bundle, SCI report and coverage.

The full suite

The loop
issue ─▶ Planner ─▶ Builder ─▶ Red attacks the fix ─▶ bypass? ─▶ save antibody ─▶ harden
│ │
└──────────── at most 2 rounds ◀───────────────┘
▼
Warden ─▶ autonomy gate ─▶ proof bundle ─▶ release
▼
monitor replays the whole bank against production, forever
│
a reproduction opens an incident ─────────────┘ (the loop restarts)
The antibody bank
Every exploit that ever reproduced lives in tests/antibodies/ as a pytest file and is never deleted. The bank only grows. Each is tagged with its CWE, route, birth date and merge request, and runs in three places: the re-attack loop, the staging replay, and the forever production probe.
| Weakness | Routes | Why a pair |
|---|---|---|
| CWE-22 path traversal | /download + /preview |
Both call storage.read_doc |
| CWE-918 server-side request forgery | /fetch + /thumbnail |
Both call storage.assert_fetchable |
| CWE-639 broken object-level authorization | /order/{id} + /invoice/{id} |
Both call storage._get_owned |
Each weakness sits on a pair of routes sharing one helper, because the fix belongs in the helper rather than on the single route a ticket happened to name. Every antibody genuinely reproduces on the vulnerable code and is blocked on the hardened code — verified, not asserted.
Seven gates, each able to stop a release
| Gate | What it catches |
|---|---|
| Tampered tests | The Warden reads the diff for deleted, skipped or weakened tests and refuses the merge. |
| Disarmed verifier | The Warden is extracted from the base revision before it runs, so a change cannot weaken the checker that judges it. |
| Weakened antibody | The bank is append-only. Editing or deleting a stored attack is tampering with evidence, not a code change. |
| Reproduced attack | Every antibody is replayed. One that reproduces fails the secure stage. |
| Broken behaviour | The paired oracle. A fix must defeat the exploit and keep the software working, judged together at the trusted revision. |
| Edited bundle | The release re-verifies the proof bundle's SHA-256 against the bank on disk, so a hand-edited bundle fails. |
| Self-validation | A change touching CI, auth, CODEOWNERS or the verifier itself drops to Supervised and waits for a human. |
The bundle gate is live, not theoretical. From a real CI run:
VERIFY OK: 6 antibodies, digest f20a1c87d5119d94..., warden not blocking
release gated: proof bundle present, warden not blocking
Nine stages, one loop
.gitlab-ci.yml defines nine stages. Seven run real work on an ordinary push;
configure and monitor are gated on deployment credentials this repository
does not carry, and are correctly skipped rather than faked green.
| Stage | What runs | Artifact |
|---|---|---|
| plan | Score change risk, write acceptance criteria | plan.json |
| create | Confirm the shadow-branch diff | change.diff |
| verify | GitLab SAST + dependency scanning, full suite, antibody bank, lint, coverage | JUnit + Cobertura |
| package | App and MCP containers, SBOM, proof bundle | sbom.json, proof-bundle.json |
| secure | Warden + replay every antibody | warden-verdict.json |
| release | Re-verify the bundle digest against the bank | tamper-evident gate |
| configure | GreenOps picks the region, deploys to Cloud Run | region.txt |
| monitor | Scheduled probe replays the bank against production, opens incidents | incident issue |
| govern | Posts the audit, publishes the dashboard | audit + Pages |
Autonomy levels
- Hands-off (default) — issue to live, untouched.
- Supervised (automatic fallback) — triggered when a change touches CI, auth or
CODEOWNERS. Everything runs, then one human approves before production. - Assisted (toggle) — every stage waits for approval.
How GitLab Duo is used
All four Duo Agent Platform features, not just one:
| Feature | How |
|---|---|
| Agents | Four custom agents registered in the AI Catalog (agents/, agents/register.py) |
| Flows | An ambient Flow Registry v1 loop (flows/antibody_loop_v1.yaml) |
| Triggers | Work-item created starts the loop with no human in the middle |
| MCP | Four deterministic tools, served over HTTP to the catalog and over stdio to an IDE |
The four tools
run_exploit · save_antibody · list_sibling_routes · score_change_risk
They live in one tested module and are reachable three ways, because GitLab Duo connects to MCP servers from IDEs and the Duo CLI rather than from server-side ambient flows:
- Hosted over HTTP, registered in the AI Catalog so they appear under AI → MCP servers. Build with
mcp_server/Dockerfile, deploy, thenmake register-mcp URL=https://<host>/mcp. - Over stdio, via
.gitlab/duo/mcp.json, for Duo Chat in an IDE. - Through a CLI bridge inside a flow or CI job:
python mcp_server/antibody_mcp.py score_change_risk app/main.py
The same tested functions serve all three, so there is no second implementation to drift.
Run it yourself
make install # venv + dependencies
make test # functional tests, antibody bank, tools, Warden, sustainability
make lint # exactly what CI enforces
make run # the target app on http://127.0.0.1:8000 (docs at /docs)
make help lists every workflow. Try the two demos:
make demo-attack # legitimate use, then every attack refused
make demo-warden # the Warden blocks a cheat, then allows an honest fix
Replay the bank against a live deployment:
ANTIBODY_TARGET="https://your-deployment" pytest tests/antibodies/ -q
See CONTRIBUTING.md for the rules CI enforces, and SECURITY.md for reporting.
Safety. The demo app is a target the operator owns and self-hosts, attacked only by the operator's own pipeline. The bait file
app/secret.txtis fake and is never a real system file.
Sustainability
The adversarial loop is expensive, so Antibody runs it only on security-relevant changes and deploys to the lowest-carbon region available. The gain is reported with the ISO/IEC 21031 SCI formula, SCI = ((E × I) + M) / R, with every input labelled and sourced.
| Input | Source |
|---|---|
| I carbon intensity | Google Cloud's published per-region data (regions.json, source) |
| E energy | Measured runner-minutes × a cited power figure (assumption stated) |
| M embodied | Green Software Foundation reference method (assumption stated) |
| R functional unit | One governed change shipped to production |
sustainability/measure.py reads real finished pipelines through the GitLab API and writes measured wall-clock minutes, so the report never runs on invented numbers. Token counts come from the Duo usage report; until supplied, the report states not measured rather than guessing. No headline percentage is hard-coded.
make measure # real pipeline durations
make sci # compute SCI and pick the region
Project status
| Piece | State |
|---|---|
| Target app, antibody bank, MCP tools, Warden, SCI | Built, tested, running in CI |
| Nine-stage pipeline | Green on GitLab, with lint and coverage gates |
| Duo agents, flow, trigger | Registered, enabled, and firing on issue creation |
| MCP server over HTTP | Verified with a real MCP client; registers once deployed |
| Cloud Run deploy | Code in deploy/; needs a GCP project |
| Films | antibody-story.mp4 (4:09, the drawn film), plus antibody-board.mp4 (2:40) and antibody-full.mp4 (4:28), rendered from this repo |
Repository map
app/ the target app, hardened, with sibling-route weakness pairs
tests/ functional tests, exploit working area, the permanent antibody bank
mcp_server/ four deterministic tools: stdio, HTTP, and CLI bridge
warden/ the deterministic anti-cheat checker
agents/ four GitLab Duo agents + AI Catalog registration
flows/ the ambient Flow Registry v1 loop + registration
dashboard/ the published proof dashboard
sustainability/ SCI calculator, measurement, proof bundle, incident opener
deploy/ Cloud Run IaC and deploy script
demo/ recording guide, Duo prompts, repeatable demo scripts
scripts/ diagram generator, MCP probe, terminal renderer
docs/ design docs and every image in this README
How this differs from existing tools
Helios, MindFort, Strix and Novee attack code and re-run the same exploit to confirm it is blocked. Antibody generates a new attack against the new fix (P2Patch), hunts the same bug on sibling routes (PRGuard), folds deterministic cheat-detection into the same loop (Hacktrace), and lowers its own autonomy when a change could validate itself (UNSW).
It also feeds GitLab SAST findings to the Red agent rather than stripping code comments — research shows comment-stripping makes detection worse.
Grounded in 2026 research
| Finding | Paper |
|---|---|
| Patches pass tests yet stay exploitable; LLM reviewers miss them | When Passing Tests Hides Vulnerabilities |
| A fix blocks only the payload it was tested on | P2Patch: Exploit-Guided Vulnerability Repair |
| The same bug hides on a sibling path outside the diff | From Verdict to Diagnosis (PRGuard) |
| Agent CI changes merge unreviewed, creating circular verification | Co-Modifying Code and Deployment Infrastructure |
| Agents cheat the grader when rewarded for green | Hacktrace |
| An exploit-only oracle accepts patches that break unrelated behaviour; a paired oracle rejects them | Vul4Py |
| ~73% of testable patches closed the hole, but only ~44% did so while keeping the software working | APR4Vul |
| Replaying the proof-of-concept alone accepts patches that silence the crash by changing other behaviour | CodeMechanic |
| Over 40% of patches judged correct by basic tests fail under stricter oracles | Patch Validation in AVR |
| A governance system can certify a false state when the actor can influence what measures it | Beyond Agent Self-Reporting |
| Lifecycle states should be gated on fresh, mechanically verifiable evidence, not agent claims | Proof-or-Stop |
| A patch validated only against the supplied proof-of-concept hides the exploits it missed | PatchBench |
| Patches can be functionally correct yet still carry a vulnerability | When "Correct" Is Not Safe |
Built With
- agent-duo
- ci/cd
- gitlab




