Spaces:
Sleeping
title: Cannon And Wall
emoji: π΄π΅
colorFrom: red
colorTo: blue
sdk: docker
pinned: false
π΄π΅ Cannon & Wall β RedBlue Arena
A self-improving attacker vs defender RL environment for training LLMs on cybersecurity reasoning.
Author: Jairaj S | Hackathon: Meta PyTorch Γ OpenEnv Γ Scaler School of Technology, April 2026 Theme: Self-Improving Agents (Theme 4) + Multi-Agent Interactions (Theme 1)
π Links
| Resource | URL |
|---|---|
| π€ HuggingFace Space (live env) | https://huggingface.co/spaces/CystronCode/cannon-and-wall |
| π₯ Demo video (YouTube <2 min) | https://youtu.be/E63MU02_1Y4 |
| π Blog writeup (this Space) | Blog.md |
| π Training notebook (Colab) | https://colab.research.google.com/drive/1uTSt6DahNVXoAZ0hlpyzrXrn-F6ehc9z?usp=sharing |
| π Reward curves (W&B) | https://wandb.ai/models-r-v-c-e/cannon-and-wall/runs/5v7yweib |
π§ The Problem
LLMs are surprisingly bad at security reasoning β not because they lack knowledge, but because they have never been trained to think adversarially in a loop.
Current benchmarks test security knowledge statically. There is no environment where:
- An attacker agent gets smarter by failing to find bugs
- A defender agent gets smarter by getting exploited
- Both co-evolve through self-play, round after round
Cannon & Wall fills that gap.
ποΈ What Is the Environment?
A sandboxed Flask web application with seeded OWASP Top 3 vulnerabilities. Two LLM agents compete inside it:
| Agent | Role | What it does |
|---|---|---|
| π΄ Cannon | Attacker | Reads source code, finds vulnerabilities, proposes exploits |
| π΅ Wall | Defender | Reads Cannon's report, patches the code, hardens the app |
| βοΈ Judge | Verifier | Runs deterministic checks β no LLM-as-judge |
Vulnerability scope
- SQL Injection (SQLi)
- Cross-Site Scripting (XSS)
- Broken Authentication
Key design decision β textual reasoning only
Neither agent executes live exploits. All reasoning happens over source code as text. This keeps the environment safe, reproducible, and trainable.
π The 3-Phase Self-Play Loop
Phase 1 β ATTACK
Cannon reads the vulnerable Flask app source code
Cannon outputs: { vuln_type, line_number, explanation, proof_of_concept }
Phase 2 β PATCH
Wall reads the source code + Cannon's report
Wall outputs: { patched_code, explanation }
Phase 3 β BYPASS
Cannon reads original code + Wall's patched version
Cannon tries to find a remaining vulnerability or bypass
Judge scores both agents after each bypass phase.
Loser faces a harder variant next round.
Both agents improve β neither can memorize.
π Reward Logic
# Cannon (Attacker)
+10 real vulnerability correctly identified
+5 correct vulnerability type (sqli / xss / broken_auth)
-5 false positive reported
+15 bypass succeeded (Wall's patch failed)
# Wall (Defender)
+5 per vulnerability correctly patched (up to 3)
+5 patched code still works (functionality preserved)
-5 patch introduced a new vulnerability
+5 bypass attempt failed (patch held)
# All rewards normalized to 0.0-1.0 for OpenEnv compliance
# Per-component rewards logged as separate W&B columns (rollout/attack_real_vuln_found, etc.)
π Environment Structure
cannon-and-wall/
βββ openenv.yaml # OpenEnv manifest (v1.1 β action_space + observation_space)
βββ openenv.py # Environment base class (local stub)
βββ app.py # FastAPI wrapper (reset / step / state)
βββ Dockerfile # Container definition
βββ requirements.txt # Dependencies
β
βββ environment/
β βββ server.py # CannonWallEnvironment class
β βββ models.py # Pydantic schemas
β βββ curriculum.py # Stage progression logic
β βββ vulnerable_app/
β β βββ stage_1/app.py # Explicit SQLi + XSS + Broken Auth
β β βββ stage_2/app.py # Split routes, aliased variables (medium)
β β βββ stage_3/app.py # Chained + obfuscated vulns (hard)
β βββ judge/
β βββ verifier.py # AST-level SQLi check + bandit static analysis
β βββ reward.py # Multi-component reward calculator
β
βββ client/
β βββ client.py # CannonWallClient (httpx)
β βββ models.py # Client-side Pydantic models
β
βββ agents/
β βββ cannon_prompt.py # Red agent system prompt + helpers
β βββ wall_prompt.py # Blue agent system prompt + helpers
β
βββ training/
β βββ train_grpo.ipynb # TRL + Unsloth GRPO training notebook
β
βββ ui/
βββ demo.py # Gradio live demo + leaderboard
π‘οΈ Anti-Reward-Hacking Measures
- Judge is fully deterministic β AST-level checks + bandit static analysis (no LLM-as-judge)
- Multiple independent reward components β gaming one does not help overall score
- Proof-of-concept validation β Cannon must provide a working exploit pattern
- Hard episode limit β MAX_ROUNDS=3, prevents infinite loops
- Dangerous import rejection β patches with
import os,exec(),eval()rejected instantly - Functionality preservation check β Wall must not break the app to score
- AST-level SQLi verifier β parses patched code to confirm parameterized
execute()is actually used, not just that?appears in a comment
π Curriculum
| Stage | File | Task | Vulnerabilities |
|---|---|---|---|
| 1 | stage_1/app.py |
Single-file login form | SQLi + XSS + Broken Auth (explicit f-string) |
| 2 | stage_2/app.py |
Split /auth + /search routes |
SQLi (string concat) + XSS (aliased variable names) |
| 3 | stage_3/app.py |
Chained + obfuscated portal | SQLi (string join) + XSS (inside href attribute) + Broken Auth (cookie override) |
Escalation triggers when Wall's raw reward exceeds 8 for 3 consecutive episodes (agent is no longer challenged).
π οΈ Training Stack
| Component | Tool |
|---|---|
| RL algorithm | GRPO via HuggingFace TRL |
| Efficiency layer | transformers + peft + bitsandbytes (4-bit QLoRA) |
| Base model | Qwen/Qwen2.5-3B-Instruct |
| Environment | OpenEnv (Docker, HF Spaces) |
| Experiment tracking | Weights & Biases (per-component reward columns) |
| Deployment | HuggingFace Spaces (Docker) |
π¬ Before vs After Training β Qualitative Example
| Untrained (step 0) | Trained (step 50) | |
|---|---|---|
| vuln_type | "xss" (wrong) |
"sqli" (correct) |
| line_number | 5 (no vuln there) |
16 (correct β f-string query) |
| proof_of_concept | "<script>test</script>" |
"' OR 1=1-- " |
| Judge verdict | False positive (β5 pts) | Real vuln + correct type (+15 pts) |
| Cannon reward | 0.000 (normalized) | 0.571 (normalized) |
The untrained model guesses XSS on the wrong line. After GRPO training it correctly identifies the SQLi on line 16 with a valid bypass PoC.
π Results
Reward curve β 50 GRPO gradient steps
X-axis: GRPO training step (each step = 4 completions sampled β environment reward β optimizer.step()).
Y-axis: normalized reward (0 = worst, 1 = best).
Dashed line: random-agent baseline (0.020) β random JSON with valid PoC patterns, no model reasoning.
Cannon reward rises from near-zero baseline to ~0.57 within 50 steps β a 25Γ improvement demonstrating real learning.
Wall remains stable at ~0.71 throughout β it patches correctly regardless of Cannon's quality.
Quantitative results
| Metric | Value |
|---|---|
| Cannon reward at step 0 (random baseline) | 0.020 |
| Cannon reward at step 50 (GRPO trained) | 0.570 |
| Improvement over baseline | 25Γ |
| Wall reward (stable throughout) | ~0.714 |
| Wall patch validity rate | 100% |
| GRPO gradient steps | 50 |
| Completions per step (G) | 4 |
| Base model | Qwen/Qwen2.5-3B-Instruct |
| LoRA rank | 16 |
| Training time (Colab T4) | ~35 min |
Training note: Each step samples G=4 completions from the model, queries the live environment for rewards, computes group-relative advantages (r_i β mean(r)), and calls
optimizer.step()to update the LoRA adapter weights. This is real gradient-based learning β not an inference evaluation loop.
βΆοΈ Running Locally
git clone https://huggingface.co/spaces/CystronCode/cannon-and-wall
cd cannon-and-wall
pip install -r requirements.txt
python app.py
# Test stage routing
curl -X POST "http://localhost:7860/reset?stage=1" # loads stage_1/app.py
curl -X POST "http://localhost:7860/reset?stage=2" # loads stage_2/app.py (split routes)
curl -X POST "http://localhost:7860/reset?stage=3" # loads stage_3/app.py (chained vulns)
π API Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /reset?stage=1 |
Start new episode, returns source code |
| POST | /step |
Send agent action, returns reward + observation |
| GET | /state |
Read current episode state |
| GET | /docs |
FastAPI Swagger UI |
π‘ Why This Matters
Security is one of the few domains where verification is fully objective, self-play is naturally adversarial, and the task is genuinely hard for current LLMs.
A model trained in Cannon & Wall learns to reason about code vulnerabilities through adversarial rounds β not through static examples. The AST-level verifier and multi-component reward make it hard to game without actually improving security reasoning.
π License
MIT
Built for the Meta PyTorch x OpenEnv Hackathon, Scaler School of Technology, Bangalore β April 2026
