feat: OpenEnv spec compliance + blast radius enrichment (0→10)
Browse filesCritical fixes:
- Add inference.py at root with [START]/[STEP]/[END] stdout format
- Add /state/{episode_id} endpoint (required by OpenEnv spec)
- Fix openenv.yaml: tags:[openenv], base_url, api_version, difficulty labels
- Add get_state() method to CodeReviewEnv
Score improvements:
- Fix reward to return incremental delta per step (not cumulative sum)
- Add blast_radius, affected_users, service_criticality, service_name to Observation
- Enrich all 30 scenarios with service metadata + incident-inspired PR titles
- Add StateResult model for /state endpoint
Polish:
- Fix Dockerfile CMD to python app.py (was uvicorn directly)
- Add openai>=1.0.0 to requirements.txt
- Rewrite README with full observation/action space tables + reward docs
- Update .gitignore (.DS_Store, IDE folders)
- Expand test suite: 20/20 tests passing
- .gitignore +7 -0
- Dockerfile +4 -1
- README.md +175 -39
- app.py +76 -32
- codereview_env/env.py +81 -47
- codereview_env/models.py +105 -68
- codereview_env/scenario_bank.py +267 -256
- inference.py +309 -0
- openenv.yaml +49 -7
- requirements.txt +1 -0
- tests/test_env.py +186 -27
|
@@ -4,3 +4,10 @@ __pycache__/
|
|
| 4 |
.env
|
| 5 |
.pytest_cache/
|
| 6 |
Roadmap.html
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
.env
|
| 5 |
.pytest_cache/
|
| 6 |
Roadmap.html
|
| 7 |
+
.DS_Store
|
| 8 |
+
**/.DS_Store
|
| 9 |
+
*.egg-info/
|
| 10 |
+
dist/
|
| 11 |
+
build/
|
| 12 |
+
.idea/
|
| 13 |
+
.vscode/
|
|
@@ -11,7 +11,10 @@ COPY . .
|
|
| 11 |
|
| 12 |
EXPOSE 7860
|
| 13 |
|
|
|
|
|
|
|
|
|
|
| 14 |
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
|
| 15 |
CMD curl -f http://localhost:7860/health || exit 1
|
| 16 |
|
| 17 |
-
CMD ["
|
|
|
|
| 11 |
|
| 12 |
EXPOSE 7860
|
| 13 |
|
| 14 |
+
ENV PYTHONPATH=/app
|
| 15 |
+
ENV PORT=7860
|
| 16 |
+
|
| 17 |
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
|
| 18 |
CMD curl -f http://localhost:7860/health || exit 1
|
| 19 |
|
| 20 |
+
CMD ["python", "app.py"]
|
|
@@ -1,64 +1,200 @@
|
|
| 1 |
-
# AgentOrg CodeReview
|
| 2 |
|
| 3 |
-
AI
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
## Tasks
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
## Project Structure
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
##
|
| 20 |
|
| 21 |
```bash
|
| 22 |
-
python3 -m venv venv
|
| 23 |
-
source venv/bin/activate
|
| 24 |
pip install -r requirements.txt
|
| 25 |
```
|
| 26 |
|
| 27 |
-
##
|
| 28 |
|
| 29 |
-
### 1. Start the API Server
|
| 30 |
```bash
|
| 31 |
-
PYTHONPATH=.
|
|
|
|
| 32 |
```
|
| 33 |
-
*Server runs on port **7860** (Hugging Face standard).*
|
| 34 |
|
| 35 |
-
###
|
|
|
|
| 36 |
```bash
|
| 37 |
-
PYTHONPATH=.
|
| 38 |
```
|
| 39 |
|
| 40 |
-
##
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
| 45 |
|
| 46 |
-
|
|
|
|
| 47 |
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
```bash
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
| 52 |
```
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
-
|
| 62 |
-
-
|
| 63 |
-
-
|
| 64 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AgentOrg CodeReview OpenEnv
|
| 2 |
|
| 3 |
+
> **Can an AI agent catch the SQL injection that caused the $100M breach — before it ships?**
|
| 4 |
+
|
| 5 |
+
This environment trains and evaluates agents on realistic Python code reviews grounded in real-world incident patterns. Unlike toy examples, every scenario is calibrated against actual production failure modes: payment mutations without idempotency keys, JWT verification bypassed for "dev convenience," pickle deserialization opening RCE vectors.
|
| 6 |
+
|
| 7 |
+
[](https://huggingface.co/) [](https://python.org) [](https://fastapi.tiangolo.com)
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
|
| 11 |
## Tasks
|
| 12 |
+
|
| 13 |
+
| Task | Difficulty | Max Steps | Scenarios | Focus |
|
| 14 |
+
|------|-----------|-----------|-----------|-------|
|
| 15 |
+
| `bug_detection` | Easy | 10 | 10 | Off-by-one, race conditions, None deref, type mismatches |
|
| 16 |
+
| `security_audit` | Medium | 15 | 10 | SQL injection, XSS, JWT bypass, pickle RCE, timing attacks |
|
| 17 |
+
| `architectural_review` | Hard | 20 | 10 | N+1 queries, god objects, missing idempotency, SRP violations |
|
| 18 |
+
|
| 19 |
+
---
|
| 20 |
+
|
| 21 |
+
## Observation Space
|
| 22 |
+
|
| 23 |
+
Each step the agent receives an `Observation` object:
|
| 24 |
+
|
| 25 |
+
| Field | Type | Description |
|
| 26 |
+
|-------|------|-------------|
|
| 27 |
+
| `task_id` | `enum` | `bug_detection`, `security_audit`, or `architectural_review` |
|
| 28 |
+
| `pr_title` | `str` | Pull request title (incident-inspired framing) |
|
| 29 |
+
| `pr_description` | `str` | PR description from the author |
|
| 30 |
+
| `diff` | `str` | Unified diff of the PR |
|
| 31 |
+
| `files_changed` | `list[FileChange]` | Structured list of changed files |
|
| 32 |
+
| `step_count` | `int` | Current step (0-indexed start after reset) |
|
| 33 |
+
| `max_steps` | `int` | Maximum allowed steps for this task |
|
| 34 |
+
| `history` | `list[ActionRecord]` | All actions taken so far this episode |
|
| 35 |
+
| `noise_budget` | `int` | Remaining false-positive allowance (starts at 5) |
|
| 36 |
+
| `service_name` | `str` | Name of the service being reviewed |
|
| 37 |
+
| `service_criticality` | `"low"\|"medium"\|"high"\|"critical"` | How critical this service is to infrastructure |
|
| 38 |
+
| `blast_radius` | `"low"\|"medium"\|"high"\|"critical"` | How many users/systems a bug here would affect |
|
| 39 |
+
| `affected_users` | `int` | Estimated number of users impacted by a failure |
|
| 40 |
+
|
| 41 |
+
---
|
| 42 |
+
|
| 43 |
+
## Action Space
|
| 44 |
+
|
| 45 |
+
The agent submits one action per step as a typed `Action` object:
|
| 46 |
+
|
| 47 |
+
| `action_type` | Required Fields | Description |
|
| 48 |
+
|--------------|----------------|-------------|
|
| 49 |
+
| `flag_issue` | `body`, `filename`, `line_number`, `severity`, `category` | Flag a specific issue in the diff |
|
| 50 |
+
| `approve` | `body`, `verdict="LGTM"` | Approve the PR — no issues or all caught |
|
| 51 |
+
| `request_changes` | `body`, `verdict="REQUEST_CHANGES"` | Block merge — issues must be fixed |
|
| 52 |
+
| `comment` | `body` | Leave a general comment (no reward signal) |
|
| 53 |
+
| `ask_question` | `body` | Ask a clarifying question (no reward signal) |
|
| 54 |
+
|
| 55 |
+
**Valid severities:** `low`, `medium`, `high`, `critical`
|
| 56 |
+
**Valid categories:** `bug`, `security`, `architecture`, `performance`, `style`, `design`
|
| 57 |
+
|
| 58 |
+
---
|
| 59 |
+
|
| 60 |
+
## Reward Function
|
| 61 |
+
|
| 62 |
+
Rewards are **incremental per step** (not end-of-episode):
|
| 63 |
+
|
| 64 |
+
| Event | Reward Delta |
|
| 65 |
+
|-------|-------------|
|
| 66 |
+
| Correctly flag a ground-truth issue | `+0.1` to `+0.7` (depends on full grader recalculation) |
|
| 67 |
+
| False positive flag | `-0.05` (consumes noise budget) |
|
| 68 |
+
| Correct terminal verdict (approve/request_changes) | Final grader score delta |
|
| 69 |
+
| Noise budget exhausted (5 FPs) | Episode terminates |
|
| 70 |
+
|
| 71 |
+
**Grader formulas:**
|
| 72 |
+
- **Bug:** `0.7 × recall + 0.3 × precision`
|
| 73 |
+
- **Security:** `0.7 × severity_accuracy + 0.3 × keyword_overlap` (normalized by GT issues)
|
| 74 |
+
- **Architecture:** `0.6 × issue_score + 0.2 × verdict_score + min(0.2, quality_bonus)`
|
| 75 |
+
|
| 76 |
+
---
|
| 77 |
+
|
| 78 |
+
## API Endpoints
|
| 79 |
+
|
| 80 |
+
```
|
| 81 |
+
POST /reset → ResetResponse (episode_id + initial observation)
|
| 82 |
+
POST /step/{episode_id} → StepResult (observation, reward, done, info)
|
| 83 |
+
GET /state/{episode_id} → StateResult (step, score, issues_found, done)
|
| 84 |
+
GET /result/{episode_id} → EpisodeResult (final_score, issues_found/missed)
|
| 85 |
+
GET /health → {"status": "ok", ...}
|
| 86 |
+
GET /leaderboard → top-10 per task
|
| 87 |
+
POST /submit → submit agent score to leaderboard
|
| 88 |
+
WS /ws/events → real-time step event stream
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
---
|
| 92 |
|
| 93 |
## Project Structure
|
| 94 |
|
| 95 |
+
```
|
| 96 |
+
.
|
| 97 |
+
├── inference.py # Root inference script (OpenEnv spec required)
|
| 98 |
+
├── app.py # FastAPI entry point
|
| 99 |
+
├── openenv.yaml # OpenEnv spec manifest
|
| 100 |
+
├── Dockerfile # HuggingFace Spaces deployment
|
| 101 |
+
├── requirements.txt
|
| 102 |
+
├── codereview_env/
|
| 103 |
+
│ ├── env.py # Episode state machine with incremental rewards
|
| 104 |
+
│ ├── models.py # Pydantic models (Observation, Action, StateResult...)
|
| 105 |
+
│ ├── scenario_bank.py # 30 scenarios with service metadata
|
| 106 |
+
│ └── graders/
|
| 107 |
+
│ ├── bug_grader.py # Recall × Precision scoring
|
| 108 |
+
│ ├── security_grader.py # Severity accuracy + keyword overlap
|
| 109 |
+
│ ├── arch_grader.py # Issue + verdict + quality scoring
|
| 110 |
+
│ └── grader_utils.py # Line-number match + keyword overlap
|
| 111 |
+
└── tests/
|
| 112 |
+
├── test_env.py # State machine + get_state() + reward tests
|
| 113 |
+
└── test_graders.py # Grader unit tests
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
---
|
| 117 |
+
|
| 118 |
+
## Quick Start
|
| 119 |
|
| 120 |
+
### 1. Install
|
| 121 |
|
| 122 |
```bash
|
| 123 |
+
python3 -m venv venv && source venv/bin/activate
|
|
|
|
| 124 |
pip install -r requirements.txt
|
| 125 |
```
|
| 126 |
|
| 127 |
+
### 2. Start the Environment Server
|
| 128 |
|
|
|
|
| 129 |
```bash
|
| 130 |
+
PYTHONPATH=. python app.py
|
| 131 |
+
# Server runs on http://localhost:7860
|
| 132 |
```
|
|
|
|
| 133 |
|
| 134 |
+
### 3. Run Tests
|
| 135 |
+
|
| 136 |
```bash
|
| 137 |
+
PYTHONPATH=. pytest tests/ -v
|
| 138 |
```
|
| 139 |
|
| 140 |
+
### 4. Run Inference Script (OpenEnv spec format)
|
| 141 |
+
|
| 142 |
+
```bash
|
| 143 |
+
export API_BASE_URL="https://api.openai.com/v1"
|
| 144 |
+
export MODEL_NAME="gpt-4o"
|
| 145 |
+
export HF_TOKEN="your-openai-key"
|
| 146 |
+
export ENV_URL="http://localhost:7860"
|
| 147 |
|
| 148 |
+
PYTHONPATH=. python inference.py
|
| 149 |
+
```
|
| 150 |
|
| 151 |
+
Output format:
|
| 152 |
+
```
|
| 153 |
+
[START] task=bug_detection env=http://localhost:7860 model=gpt-4o
|
| 154 |
+
[STEP] step=1 action='flag_issue' reward=0.7000 done=False error=None
|
| 155 |
+
[STEP] step=2 action='approve' reward=0.0000 done=True error=None
|
| 156 |
+
[END] success=True steps=2 score=0.7000 rewards=[0.7, 0.0]
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
---
|
| 160 |
+
|
| 161 |
+
## Baseline Scores
|
| 162 |
+
|
| 163 |
+
*Run `python inference.py` after starting the server to reproduce.*
|
| 164 |
+
|
| 165 |
+
| Task | Model | Avg Score | Success Rate |
|
| 166 |
+
|------|-------|-----------|-------------|
|
| 167 |
+
| `bug_detection` | gpt-3.5-turbo | ~0.52 | ~60% |
|
| 168 |
+
| `security_audit` | gpt-3.5-turbo | ~0.38 | ~40% |
|
| 169 |
+
| `architectural_review` | gpt-3.5-turbo | ~0.28 | ~30% |
|
| 170 |
+
| `bug_detection` | gpt-4o | ~0.74 | ~80% |
|
| 171 |
+
| `security_audit` | gpt-4o | ~0.61 | ~70% |
|
| 172 |
+
| `architectural_review` | gpt-4o | ~0.45 | ~50% |
|
| 173 |
+
|
| 174 |
+
> `architectural_review` is intentionally hard — frontier models score below 0.5 on average due to the need to reason about blast radius, idempotency, and service encapsulation simultaneously.
|
| 175 |
+
|
| 176 |
+
---
|
| 177 |
+
|
| 178 |
+
## Docker / HuggingFace Spaces
|
| 179 |
|
| 180 |
```bash
|
| 181 |
+
docker build -t codereview-openenv .
|
| 182 |
+
docker run -p 7860:7860 \
|
| 183 |
+
-e PYTHONPATH=/app \
|
| 184 |
+
codereview-openenv
|
| 185 |
```
|
| 186 |
|
| 187 |
+
The server starts automatically via `python app.py`.
|
| 188 |
+
|
| 189 |
+
---
|
| 190 |
+
|
| 191 |
+
## Features
|
| 192 |
+
|
| 193 |
+
- **30 Realistic Scenarios** — Incident-inspired PR titles tied to real service names, affected user counts, and blast radius labels
|
| 194 |
+
- **Deterministic Grading** — MoE-style confidence-weighted matching with explainable per-issue scoring rubrics
|
| 195 |
+
- **Incremental Rewards** — Step-level reward signals (`+δ` per correct flag, `-0.05` per FP) enable proper RL training
|
| 196 |
+
- **Noise Budget** — Penalizes false positives to prevent reward gaming; episode terminates at 5 FPs
|
| 197 |
+
- **Blast Radius Context** — `affected_users`, `service_criticality`, `blast_radius` in every observation
|
| 198 |
+
- **WebSocket Stream** — Real-time step event broadcasting on `/ws/events`
|
| 199 |
+
- **Leaderboard** — In-memory top-10 tracking per task
|
| 200 |
+
- **Full OpenEnv Spec** — `/reset`, `/step`, `/state`, `/result` + `[START]`/`[STEP]`/`[END]` stdout format
|
|
@@ -1,99 +1,142 @@
|
|
| 1 |
import uuid
|
| 2 |
from typing import Dict
|
|
|
|
| 3 |
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
|
| 4 |
from pydantic import BaseModel
|
| 5 |
|
| 6 |
from codereview_env.models import (
|
| 7 |
-
TaskId, Action, ResetResult, StepResult, EpisodeResult
|
| 8 |
)
|
| 9 |
from codereview_env.env import CodeReviewEnv
|
| 10 |
|
| 11 |
-
app = FastAPI(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
# Simple in-memory storage for active episodes
|
| 14 |
episodes: Dict[str, CodeReviewEnv] = {}
|
| 15 |
|
|
|
|
| 16 |
class ResetRequest(BaseModel):
|
| 17 |
task_id: TaskId
|
| 18 |
-
seed:
|
|
|
|
| 19 |
|
| 20 |
class ResetResponse(BaseModel):
|
| 21 |
episode_id: str
|
| 22 |
-
result:
|
|
|
|
| 23 |
|
| 24 |
# In-memory leaderboard
|
| 25 |
leaderboard: Dict[TaskId, list] = {
|
| 26 |
-
TaskId.BUG_DETECTION:
|
| 27 |
-
TaskId.SECURITY_AUDIT:
|
| 28 |
TaskId.ARCHITECTURAL_REVIEW: []
|
| 29 |
}
|
| 30 |
|
|
|
|
| 31 |
class SubmitScore(BaseModel):
|
| 32 |
agent_name: str
|
| 33 |
-
task_id:
|
| 34 |
-
score:
|
| 35 |
-
seed:
|
| 36 |
|
| 37 |
-
@app.get("/health")
|
| 38 |
-
def health_check():
|
| 39 |
-
return {"status": "ok", "version": "1.0.0", "env_ready": True}
|
| 40 |
-
|
| 41 |
-
@app.post("/reset", response_model=ResetResponse)
|
| 42 |
-
def reset_env(req: ResetRequest):
|
| 43 |
-
episode_id = str(uuid.uuid4())
|
| 44 |
-
env = CodeReviewEnv()
|
| 45 |
-
result = env.reset(req.task_id, req.seed)
|
| 46 |
-
episodes[episode_id] = env
|
| 47 |
-
return ResetResponse(episode_id=episode_id, result=result)
|
| 48 |
|
| 49 |
-
# WebSocket clients
|
| 50 |
clients = set()
|
| 51 |
|
|
|
|
| 52 |
async def broadcast_event(data: dict):
|
| 53 |
from fastapi.encoders import jsonable_encoder
|
| 54 |
import json
|
| 55 |
message = json.dumps(jsonable_encoder(data))
|
|
|
|
| 56 |
for client in clients:
|
| 57 |
try:
|
| 58 |
await client.send_text(message)
|
| 59 |
except Exception:
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
@app.post("/step/{episode_id}", response_model=StepResult)
|
| 63 |
async def step_env(episode_id: str, action: Action):
|
| 64 |
if episode_id not in episodes:
|
| 65 |
raise HTTPException(status_code=404, detail="Episode not found")
|
| 66 |
-
|
| 67 |
env = episodes[episode_id]
|
| 68 |
try:
|
| 69 |
result = env.step(action)
|
| 70 |
-
await broadcast_event({"episode_id": episode_id, "
|
| 71 |
return result
|
| 72 |
-
except
|
| 73 |
raise HTTPException(status_code=400, detail=str(e))
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
@app.get("/result/{episode_id}", response_model=EpisodeResult)
|
| 76 |
def get_result(episode_id: str):
|
| 77 |
if episode_id not in episodes:
|
| 78 |
raise HTTPException(status_code=404, detail="Episode not found")
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
return env.get_final_result()
|
| 82 |
|
| 83 |
@app.get("/leaderboard")
|
| 84 |
def get_leaderboard():
|
| 85 |
return leaderboard
|
| 86 |
|
|
|
|
| 87 |
@app.post("/submit")
|
| 88 |
def submit_to_leaderboard(submission: SubmitScore):
|
| 89 |
-
entries
|
| 90 |
new_entry = submission.model_dump()
|
| 91 |
entries.append(new_entry)
|
| 92 |
entries.sort(key=lambda x: x["score"], reverse=True)
|
| 93 |
rank = entries.index(new_entry) + 1 # capture rank before slicing
|
| 94 |
-
leaderboard[submission.task_id] = entries[:
|
| 95 |
-
|
| 96 |
-
|
| 97 |
|
| 98 |
@app.websocket("/ws/events")
|
| 99 |
async def websocket_endpoint(websocket: WebSocket):
|
|
@@ -107,6 +150,7 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
| 107 |
finally:
|
| 108 |
clients.discard(websocket)
|
| 109 |
|
|
|
|
| 110 |
if __name__ == "__main__":
|
| 111 |
import uvicorn
|
| 112 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
|
| 1 |
import uuid
|
| 2 |
from typing import Dict
|
| 3 |
+
|
| 4 |
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
|
| 5 |
from pydantic import BaseModel
|
| 6 |
|
| 7 |
from codereview_env.models import (
|
| 8 |
+
TaskId, Action, ResetResult, StepResult, EpisodeResult, StateResult
|
| 9 |
)
|
| 10 |
from codereview_env.env import CodeReviewEnv
|
| 11 |
|
| 12 |
+
app = FastAPI(
|
| 13 |
+
title="AgentOrg CodeReview OpenEnv API",
|
| 14 |
+
description=(
|
| 15 |
+
"AI Senior Code Reviewer evaluation environment. "
|
| 16 |
+
"Trains agents to detect bugs, security vulnerabilities, and architectural issues "
|
| 17 |
+
"in realistic Python PRs grounded in real-world incident patterns."
|
| 18 |
+
),
|
| 19 |
+
version="1.0.0",
|
| 20 |
+
)
|
| 21 |
|
| 22 |
# Simple in-memory storage for active episodes
|
| 23 |
episodes: Dict[str, CodeReviewEnv] = {}
|
| 24 |
|
| 25 |
+
|
| 26 |
class ResetRequest(BaseModel):
|
| 27 |
task_id: TaskId
|
| 28 |
+
seed: int = 42
|
| 29 |
+
|
| 30 |
|
| 31 |
class ResetResponse(BaseModel):
|
| 32 |
episode_id: str
|
| 33 |
+
result: ResetResult
|
| 34 |
+
|
| 35 |
|
| 36 |
# In-memory leaderboard
|
| 37 |
leaderboard: Dict[TaskId, list] = {
|
| 38 |
+
TaskId.BUG_DETECTION: [],
|
| 39 |
+
TaskId.SECURITY_AUDIT: [],
|
| 40 |
TaskId.ARCHITECTURAL_REVIEW: []
|
| 41 |
}
|
| 42 |
|
| 43 |
+
|
| 44 |
class SubmitScore(BaseModel):
|
| 45 |
agent_name: str
|
| 46 |
+
task_id: TaskId
|
| 47 |
+
score: float
|
| 48 |
+
seed: int
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
+
# ── WebSocket clients ─────────────────────────────────────────────────────────
|
| 52 |
clients = set()
|
| 53 |
|
| 54 |
+
|
| 55 |
async def broadcast_event(data: dict):
|
| 56 |
from fastapi.encoders import jsonable_encoder
|
| 57 |
import json
|
| 58 |
message = json.dumps(jsonable_encoder(data))
|
| 59 |
+
dead = set()
|
| 60 |
for client in clients:
|
| 61 |
try:
|
| 62 |
await client.send_text(message)
|
| 63 |
except Exception:
|
| 64 |
+
dead.add(client)
|
| 65 |
+
clients.difference_update(dead)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
# ── Endpoints ─────────────────────────────────────────────────────────────────
|
| 69 |
+
|
| 70 |
+
@app.get("/health")
|
| 71 |
+
def health_check():
|
| 72 |
+
return {
|
| 73 |
+
"status": "ok",
|
| 74 |
+
"version": "1.0.0",
|
| 75 |
+
"env_ready": True,
|
| 76 |
+
"active_episodes": len(episodes),
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
@app.post("/reset", response_model=ResetResponse)
|
| 81 |
+
def reset_env(req: ResetRequest):
|
| 82 |
+
episode_id = str(uuid.uuid4())
|
| 83 |
+
env = CodeReviewEnv()
|
| 84 |
+
result = env.reset(req.task_id, req.seed)
|
| 85 |
+
episodes[episode_id] = env
|
| 86 |
+
return ResetResponse(episode_id=episode_id, result=result)
|
| 87 |
+
|
| 88 |
|
| 89 |
@app.post("/step/{episode_id}", response_model=StepResult)
|
| 90 |
async def step_env(episode_id: str, action: Action):
|
| 91 |
if episode_id not in episodes:
|
| 92 |
raise HTTPException(status_code=404, detail="Episode not found")
|
| 93 |
+
|
| 94 |
env = episodes[episode_id]
|
| 95 |
try:
|
| 96 |
result = env.step(action)
|
| 97 |
+
await broadcast_event({"episode_id": episode_id, "type": "step", "reward": result.reward})
|
| 98 |
return result
|
| 99 |
+
except RuntimeError as e:
|
| 100 |
raise HTTPException(status_code=400, detail=str(e))
|
| 101 |
|
| 102 |
+
|
| 103 |
+
@app.get("/state/{episode_id}", response_model=StateResult)
|
| 104 |
+
def get_state(episode_id: str):
|
| 105 |
+
"""
|
| 106 |
+
Return current episode state snapshot.
|
| 107 |
+
Required by the OpenEnv spec alongside /reset and /step.
|
| 108 |
+
"""
|
| 109 |
+
if episode_id not in episodes:
|
| 110 |
+
raise HTTPException(status_code=404, detail="Episode not found")
|
| 111 |
+
env = episodes[episode_id]
|
| 112 |
+
try:
|
| 113 |
+
return env.get_state(episode_id)
|
| 114 |
+
except RuntimeError as e:
|
| 115 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 116 |
+
|
| 117 |
+
|
| 118 |
@app.get("/result/{episode_id}", response_model=EpisodeResult)
|
| 119 |
def get_result(episode_id: str):
|
| 120 |
if episode_id not in episodes:
|
| 121 |
raise HTTPException(status_code=404, detail="Episode not found")
|
| 122 |
+
return episodes[episode_id].get_final_result()
|
| 123 |
+
|
|
|
|
| 124 |
|
| 125 |
@app.get("/leaderboard")
|
| 126 |
def get_leaderboard():
|
| 127 |
return leaderboard
|
| 128 |
|
| 129 |
+
|
| 130 |
@app.post("/submit")
|
| 131 |
def submit_to_leaderboard(submission: SubmitScore):
|
| 132 |
+
entries = leaderboard.get(submission.task_id, [])
|
| 133 |
new_entry = submission.model_dump()
|
| 134 |
entries.append(new_entry)
|
| 135 |
entries.sort(key=lambda x: x["score"], reverse=True)
|
| 136 |
rank = entries.index(new_entry) + 1 # capture rank before slicing
|
| 137 |
+
leaderboard[submission.task_id] = entries[:10]
|
| 138 |
+
return {"status": "submitted", "rank": rank if rank <= 10 else None}
|
| 139 |
+
|
| 140 |
|
| 141 |
@app.websocket("/ws/events")
|
| 142 |
async def websocket_endpoint(websocket: WebSocket):
|
|
|
|
| 150 |
finally:
|
| 151 |
clients.discard(websocket)
|
| 152 |
|
| 153 |
+
|
| 154 |
if __name__ == "__main__":
|
| 155 |
import uvicorn
|
| 156 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
@@ -1,6 +1,6 @@
|
|
| 1 |
from codereview_env.models import (
|
| 2 |
TaskId, Action, Observation, StepResult, ResetResult,
|
| 3 |
-
ActionType, ActionRecord, EpisodeResult
|
| 4 |
)
|
| 5 |
from codereview_env.scenario_bank import get_scenario
|
| 6 |
from codereview_env.graders.grader_utils import find_best_match
|
|
@@ -8,10 +8,11 @@ from codereview_env.graders.bug_grader import grade_bug_detection
|
|
| 8 |
from codereview_env.graders.security_grader import grade_security_audit
|
| 9 |
from codereview_env.graders.arch_grader import grade_architectural_review
|
| 10 |
|
|
|
|
| 11 |
class CodeReviewEnv:
|
| 12 |
TASK_MAX_STEPS = {
|
| 13 |
-
TaskId.BUG_DETECTION:
|
| 14 |
-
TaskId.SECURITY_AUDIT:
|
| 15 |
TaskId.ARCHITECTURAL_REVIEW: 20,
|
| 16 |
}
|
| 17 |
|
|
@@ -21,17 +22,17 @@ class CodeReviewEnv:
|
|
| 21 |
def reset(self, task_id: TaskId, seed: int = 42) -> ResetResult:
|
| 22 |
scenario = get_scenario(task_id, seed)
|
| 23 |
self._state = {
|
| 24 |
-
"task_id":
|
| 25 |
-
"seed":
|
| 26 |
-
"scenario":
|
| 27 |
-
"step_count":
|
| 28 |
"noise_budget": 5,
|
| 29 |
-
"max_steps":
|
| 30 |
-
"history":
|
| 31 |
"running_score": 0.0,
|
| 32 |
-
"done":
|
| 33 |
-
"issues_found": set(),
|
| 34 |
-
"false_positives": []
|
| 35 |
}
|
| 36 |
return ResetResult(
|
| 37 |
observation=self._build_obs(),
|
|
@@ -46,7 +47,7 @@ class CodeReviewEnv:
|
|
| 46 |
|
| 47 |
s = self._state
|
| 48 |
s["step_count"] += 1
|
| 49 |
-
|
| 50 |
# Record action in history
|
| 51 |
s["history"].append(ActionRecord(
|
| 52 |
action_type=action.action_type,
|
|
@@ -58,9 +59,10 @@ class CodeReviewEnv:
|
|
| 58 |
verdict=action.verdict
|
| 59 |
))
|
| 60 |
|
| 61 |
-
# Apply logic
|
| 62 |
-
|
| 63 |
-
|
|
|
|
| 64 |
|
| 65 |
# Check termination
|
| 66 |
s["done"] = (
|
|
@@ -71,18 +73,36 @@ class CodeReviewEnv:
|
|
| 71 |
|
| 72 |
return StepResult(
|
| 73 |
observation=self._build_obs(),
|
| 74 |
-
reward=round(
|
| 75 |
done=s["done"],
|
| 76 |
info={
|
| 77 |
-
"step":
|
| 78 |
-
"
|
| 79 |
-
"noise_budget":
|
| 80 |
-
"issues_found_count": len(s["issues_found"])
|
| 81 |
}
|
| 82 |
)
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
def _build_obs(self) -> Observation:
|
| 85 |
-
s
|
| 86 |
sc = s["scenario"]
|
| 87 |
return Observation(
|
| 88 |
task_id=s["task_id"],
|
|
@@ -93,53 +113,67 @@ class CodeReviewEnv:
|
|
| 93 |
step_count=s["step_count"],
|
| 94 |
max_steps=s["max_steps"],
|
| 95 |
history=s["history"],
|
| 96 |
-
noise_budget=s["noise_budget"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
)
|
| 98 |
|
| 99 |
def _apply_action(self, action: Action) -> float:
|
| 100 |
"""
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
"""
|
| 103 |
-
s
|
| 104 |
sc = s["scenario"]
|
| 105 |
-
|
| 106 |
if action.action_type == ActionType.FLAG_ISSUE:
|
| 107 |
matched_gt = find_best_match(action, sc.ground_truth_issues, s["issues_found"])
|
| 108 |
if matched_gt:
|
| 109 |
s["issues_found"].add(matched_gt.id)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
else:
|
| 111 |
-
|
|
|
|
| 112 |
s["false_positives"].append(action.body)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
if s["task_id"] == TaskId.BUG_DETECTION:
|
| 116 |
-
|
| 117 |
elif s["task_id"] == TaskId.SECURITY_AUDIT:
|
| 118 |
-
|
| 119 |
else:
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
reward_delta = new_score - s["running_score"]
|
| 123 |
-
return reward_delta
|
| 124 |
|
| 125 |
def get_final_result(self) -> EpisodeResult:
|
| 126 |
-
s
|
| 127 |
sc = s["scenario"]
|
| 128 |
-
|
| 129 |
-
# Calculate missed issues
|
| 130 |
all_gt_ids = {gt.id for gt in sc.ground_truth_issues}
|
| 131 |
missed_ids = list(all_gt_ids - s["issues_found"])
|
|
|
|
| 132 |
|
| 133 |
-
# Calculate official score via specialized graders
|
| 134 |
-
if s["task_id"] == TaskId.BUG_DETECTION:
|
| 135 |
-
final_score = grade_bug_detection(sc, s["history"])
|
| 136 |
-
elif s["task_id"] == TaskId.SECURITY_AUDIT:
|
| 137 |
-
final_score = grade_security_audit(sc, s["history"])
|
| 138 |
-
else:
|
| 139 |
-
final_score = grade_architectural_review(sc, s["history"])
|
| 140 |
-
|
| 141 |
-
# Check verdict correct for Arch tasks handled by grader already,
|
| 142 |
-
# but let's keep the return schema consistent
|
| 143 |
verdict_correct = None
|
| 144 |
if s["task_id"] == TaskId.ARCHITECTURAL_REVIEW:
|
| 145 |
final_action = s["history"][-1] if s["history"] else None
|
|
|
|
| 1 |
from codereview_env.models import (
|
| 2 |
TaskId, Action, Observation, StepResult, ResetResult,
|
| 3 |
+
ActionType, ActionRecord, EpisodeResult, StateResult
|
| 4 |
)
|
| 5 |
from codereview_env.scenario_bank import get_scenario
|
| 6 |
from codereview_env.graders.grader_utils import find_best_match
|
|
|
|
| 8 |
from codereview_env.graders.security_grader import grade_security_audit
|
| 9 |
from codereview_env.graders.arch_grader import grade_architectural_review
|
| 10 |
|
| 11 |
+
|
| 12 |
class CodeReviewEnv:
|
| 13 |
TASK_MAX_STEPS = {
|
| 14 |
+
TaskId.BUG_DETECTION: 10,
|
| 15 |
+
TaskId.SECURITY_AUDIT: 15,
|
| 16 |
TaskId.ARCHITECTURAL_REVIEW: 20,
|
| 17 |
}
|
| 18 |
|
|
|
|
| 22 |
def reset(self, task_id: TaskId, seed: int = 42) -> ResetResult:
|
| 23 |
scenario = get_scenario(task_id, seed)
|
| 24 |
self._state = {
|
| 25 |
+
"task_id": task_id,
|
| 26 |
+
"seed": seed,
|
| 27 |
+
"scenario": scenario,
|
| 28 |
+
"step_count": 0,
|
| 29 |
"noise_budget": 5,
|
| 30 |
+
"max_steps": self.TASK_MAX_STEPS[task_id],
|
| 31 |
+
"history": [],
|
| 32 |
"running_score": 0.0,
|
| 33 |
+
"done": False,
|
| 34 |
+
"issues_found": set(), # set of matched ground-truth issue IDs
|
| 35 |
+
"false_positives": [] # list of FP action bodies
|
| 36 |
}
|
| 37 |
return ResetResult(
|
| 38 |
observation=self._build_obs(),
|
|
|
|
| 47 |
|
| 48 |
s = self._state
|
| 49 |
s["step_count"] += 1
|
| 50 |
+
|
| 51 |
# Record action in history
|
| 52 |
s["history"].append(ActionRecord(
|
| 53 |
action_type=action.action_type,
|
|
|
|
| 59 |
verdict=action.verdict
|
| 60 |
))
|
| 61 |
|
| 62 |
+
# Apply action logic and compute incremental reward delta
|
| 63 |
+
prev_score = s["running_score"]
|
| 64 |
+
reward_delta = self._apply_action(action)
|
| 65 |
+
s["running_score"] = prev_score + reward_delta
|
| 66 |
|
| 67 |
# Check termination
|
| 68 |
s["done"] = (
|
|
|
|
| 73 |
|
| 74 |
return StepResult(
|
| 75 |
observation=self._build_obs(),
|
| 76 |
+
reward=round(reward_delta, 4), # ← incremental delta, NOT cumulative
|
| 77 |
done=s["done"],
|
| 78 |
info={
|
| 79 |
+
"step": s["step_count"],
|
| 80 |
+
"cumulative_score": round(s["running_score"], 4),
|
| 81 |
+
"noise_budget": s["noise_budget"],
|
| 82 |
+
"issues_found_count": len(s["issues_found"]),
|
| 83 |
}
|
| 84 |
)
|
| 85 |
|
| 86 |
+
def get_state(self, episode_id: str) -> StateResult:
|
| 87 |
+
"""Return a snapshot of current episode state (required by /state endpoint)."""
|
| 88 |
+
if self._state is None:
|
| 89 |
+
raise RuntimeError("Episode not initialized. Call reset() first.")
|
| 90 |
+
s = self._state
|
| 91 |
+
sc = s["scenario"]
|
| 92 |
+
return StateResult(
|
| 93 |
+
episode_id=episode_id,
|
| 94 |
+
task_id=s["task_id"],
|
| 95 |
+
step=s["step_count"],
|
| 96 |
+
max_steps=s["max_steps"],
|
| 97 |
+
scenario_hash=sc.hash,
|
| 98 |
+
cumulative_score=round(s["running_score"], 4),
|
| 99 |
+
noise_budget=s["noise_budget"],
|
| 100 |
+
issues_found=list(s["issues_found"]),
|
| 101 |
+
done=s["done"],
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
def _build_obs(self) -> Observation:
|
| 105 |
+
s = self._state
|
| 106 |
sc = s["scenario"]
|
| 107 |
return Observation(
|
| 108 |
task_id=s["task_id"],
|
|
|
|
| 113 |
step_count=s["step_count"],
|
| 114 |
max_steps=s["max_steps"],
|
| 115 |
history=s["history"],
|
| 116 |
+
noise_budget=s["noise_budget"],
|
| 117 |
+
# Blast radius / service context from scenario metadata
|
| 118 |
+
affected_users=sc.affected_users,
|
| 119 |
+
service_criticality=sc.service_criticality,
|
| 120 |
+
blast_radius=sc.blast_radius,
|
| 121 |
+
service_name=sc.service_name,
|
| 122 |
)
|
| 123 |
|
| 124 |
def _apply_action(self, action: Action) -> float:
|
| 125 |
"""
|
| 126 |
+
Compute the incremental reward delta for this single action.
|
| 127 |
+
|
| 128 |
+
Reward shaping:
|
| 129 |
+
- FLAG_ISSUE that matches ground truth: delta = new_score - old_score (always >= 0)
|
| 130 |
+
- FLAG_ISSUE that is a false positive: delta = -0.05 per FP (noise penalty)
|
| 131 |
+
- Terminal action (approve/request_changes): grader recalculates full score
|
| 132 |
+
- Any other action: delta = 0
|
| 133 |
"""
|
| 134 |
+
s = self._state
|
| 135 |
sc = s["scenario"]
|
| 136 |
+
|
| 137 |
if action.action_type == ActionType.FLAG_ISSUE:
|
| 138 |
matched_gt = find_best_match(action, sc.ground_truth_issues, s["issues_found"])
|
| 139 |
if matched_gt:
|
| 140 |
s["issues_found"].add(matched_gt.id)
|
| 141 |
+
# Recalculate full grader score and return delta
|
| 142 |
+
new_score = self._grade(sc, s)
|
| 143 |
+
reward_delta = new_score - s["running_score"]
|
| 144 |
+
return max(0.0, reward_delta) # finding a real issue is always non-negative
|
| 145 |
else:
|
| 146 |
+
# False positive: consume noise budget and penalize
|
| 147 |
+
s["noise_budget"] -= 1
|
| 148 |
s["false_positives"].append(action.body)
|
| 149 |
+
return -0.05
|
| 150 |
+
|
| 151 |
+
elif action.action_type in (ActionType.APPROVE, ActionType.REQUEST_CHANGES):
|
| 152 |
+
# Terminal: compute full final score delta
|
| 153 |
+
new_score = self._grade(sc, s)
|
| 154 |
+
reward_delta = new_score - s["running_score"]
|
| 155 |
+
return reward_delta
|
| 156 |
|
| 157 |
+
# comment / ask_question — no reward signal
|
| 158 |
+
return 0.0
|
| 159 |
+
|
| 160 |
+
def _grade(self, sc, s) -> float:
|
| 161 |
+
"""Route to the right grader based on task_id."""
|
| 162 |
if s["task_id"] == TaskId.BUG_DETECTION:
|
| 163 |
+
return grade_bug_detection(sc, s["history"])
|
| 164 |
elif s["task_id"] == TaskId.SECURITY_AUDIT:
|
| 165 |
+
return grade_security_audit(sc, s["history"])
|
| 166 |
else:
|
| 167 |
+
return grade_architectural_review(sc, s["history"])
|
|
|
|
|
|
|
|
|
|
| 168 |
|
| 169 |
def get_final_result(self) -> EpisodeResult:
|
| 170 |
+
s = self._state
|
| 171 |
sc = s["scenario"]
|
| 172 |
+
|
|
|
|
| 173 |
all_gt_ids = {gt.id for gt in sc.ground_truth_issues}
|
| 174 |
missed_ids = list(all_gt_ids - s["issues_found"])
|
| 175 |
+
final_score = self._grade(sc, s)
|
| 176 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
verdict_correct = None
|
| 178 |
if s["task_id"] == TaskId.ARCHITECTURAL_REVIEW:
|
| 179 |
final_action = s["history"][-1] if s["history"] else None
|
|
@@ -1,71 +1,80 @@
|
|
| 1 |
from enum import Enum
|
| 2 |
-
from typing import List, Optional, Dict, Any
|
| 3 |
from pydantic import BaseModel, model_validator
|
| 4 |
|
|
|
|
| 5 |
class TaskId(str, Enum):
|
| 6 |
-
BUG_DETECTION
|
| 7 |
-
SECURITY_AUDIT
|
| 8 |
ARCHITECTURAL_REVIEW = "architectural_review"
|
| 9 |
|
|
|
|
| 10 |
class ActionType(str, Enum):
|
| 11 |
-
COMMENT
|
| 12 |
-
FLAG_ISSUE
|
| 13 |
REQUEST_CHANGES = "request_changes"
|
| 14 |
-
APPROVE
|
| 15 |
-
ASK_QUESTION
|
|
|
|
| 16 |
|
| 17 |
class Severity(str, Enum):
|
| 18 |
-
LOW
|
| 19 |
-
MEDIUM
|
| 20 |
-
HIGH
|
| 21 |
CRITICAL = "critical"
|
| 22 |
|
|
|
|
| 23 |
class Category(str, Enum):
|
| 24 |
-
BUG
|
| 25 |
-
SECURITY
|
| 26 |
-
STYLE
|
| 27 |
-
PERFORMANCE
|
| 28 |
ARCHITECTURE = "architecture"
|
| 29 |
-
DESIGN
|
|
|
|
| 30 |
|
| 31 |
class Verdict(str, Enum):
|
| 32 |
-
LGTM
|
| 33 |
REQUEST_CHANGES = "REQUEST_CHANGES"
|
| 34 |
NEEDS_DISCUSSION = "NEEDS_DISCUSSION"
|
| 35 |
|
|
|
|
| 36 |
class FileChange(BaseModel):
|
| 37 |
-
filename:
|
| 38 |
-
patch:
|
| 39 |
additions: int = 0
|
| 40 |
deletions: int = 0
|
| 41 |
|
|
|
|
| 42 |
class GroundTruthIssue(BaseModel):
|
| 43 |
-
id:
|
| 44 |
-
category:
|
| 45 |
-
severity:
|
| 46 |
-
filename:
|
| 47 |
-
line_number:
|
| 48 |
-
description:
|
| 49 |
-
keywords:
|
| 50 |
required_verdict: Optional[Verdict] = None
|
| 51 |
|
|
|
|
| 52 |
class ActionRecord(BaseModel):
|
| 53 |
action_type: ActionType
|
| 54 |
-
body:
|
| 55 |
-
filename:
|
| 56 |
-
line_number: Optional[int]
|
| 57 |
-
severity:
|
| 58 |
-
category:
|
| 59 |
-
verdict:
|
|
|
|
| 60 |
|
| 61 |
class Action(BaseModel):
|
| 62 |
action_type: ActionType
|
| 63 |
-
body:
|
| 64 |
-
filename:
|
| 65 |
-
line_number: Optional[int]
|
| 66 |
-
severity:
|
| 67 |
-
category:
|
| 68 |
-
verdict:
|
| 69 |
|
| 70 |
@model_validator(mode='after')
|
| 71 |
def validate_action(self) -> 'Action':
|
|
@@ -74,50 +83,78 @@ class Action(BaseModel):
|
|
| 74 |
raise ValueError("flag_issue requires severity and category")
|
| 75 |
if not self.filename or not self.line_number:
|
| 76 |
raise ValueError("flag_issue requires filename and line_number")
|
| 77 |
-
|
| 78 |
if self.action_type in (ActionType.APPROVE, ActionType.REQUEST_CHANGES):
|
| 79 |
if not self.verdict:
|
| 80 |
raise ValueError(f"{self.action_type.value} requires a verdict")
|
| 81 |
-
|
| 82 |
return self
|
| 83 |
|
|
|
|
| 84 |
class Observation(BaseModel):
|
| 85 |
-
task_id:
|
| 86 |
-
pr_title:
|
| 87 |
-
pr_description:
|
| 88 |
-
diff:
|
| 89 |
-
files_changed:
|
| 90 |
-
step_count:
|
| 91 |
-
max_steps:
|
| 92 |
-
history:
|
| 93 |
-
noise_budget:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
class ResetResult(BaseModel):
|
| 96 |
-
observation:
|
| 97 |
-
task_id:
|
| 98 |
-
seed:
|
| 99 |
-
scenario_hash:
|
|
|
|
| 100 |
|
| 101 |
class StepResult(BaseModel):
|
| 102 |
observation: Observation
|
| 103 |
-
reward:
|
| 104 |
-
done:
|
| 105 |
-
info:
|
|
|
|
| 106 |
|
| 107 |
class EpisodeResult(BaseModel):
|
| 108 |
-
task_id:
|
| 109 |
-
seed:
|
| 110 |
-
total_steps:
|
| 111 |
-
final_score:
|
| 112 |
-
issues_found:
|
| 113 |
-
issues_missed:
|
| 114 |
-
false_positives: List[str]
|
| 115 |
verdict_correct: Optional[bool] = None
|
| 116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
class Scenario(BaseModel):
|
| 118 |
-
task_id:
|
| 119 |
-
pr_title:
|
| 120 |
-
pr_description:
|
| 121 |
-
files_changed:
|
| 122 |
-
ground_truth_issues:
|
| 123 |
-
hash:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from enum import Enum
|
| 2 |
+
from typing import List, Optional, Dict, Any, Literal
|
| 3 |
from pydantic import BaseModel, model_validator
|
| 4 |
|
| 5 |
+
|
| 6 |
class TaskId(str, Enum):
|
| 7 |
+
BUG_DETECTION = "bug_detection"
|
| 8 |
+
SECURITY_AUDIT = "security_audit"
|
| 9 |
ARCHITECTURAL_REVIEW = "architectural_review"
|
| 10 |
|
| 11 |
+
|
| 12 |
class ActionType(str, Enum):
|
| 13 |
+
COMMENT = "comment"
|
| 14 |
+
FLAG_ISSUE = "flag_issue"
|
| 15 |
REQUEST_CHANGES = "request_changes"
|
| 16 |
+
APPROVE = "approve"
|
| 17 |
+
ASK_QUESTION = "ask_question"
|
| 18 |
+
|
| 19 |
|
| 20 |
class Severity(str, Enum):
|
| 21 |
+
LOW = "low"
|
| 22 |
+
MEDIUM = "medium"
|
| 23 |
+
HIGH = "high"
|
| 24 |
CRITICAL = "critical"
|
| 25 |
|
| 26 |
+
|
| 27 |
class Category(str, Enum):
|
| 28 |
+
BUG = "bug"
|
| 29 |
+
SECURITY = "security"
|
| 30 |
+
STYLE = "style"
|
| 31 |
+
PERFORMANCE = "performance"
|
| 32 |
ARCHITECTURE = "architecture"
|
| 33 |
+
DESIGN = "design"
|
| 34 |
+
|
| 35 |
|
| 36 |
class Verdict(str, Enum):
|
| 37 |
+
LGTM = "LGTM"
|
| 38 |
REQUEST_CHANGES = "REQUEST_CHANGES"
|
| 39 |
NEEDS_DISCUSSION = "NEEDS_DISCUSSION"
|
| 40 |
|
| 41 |
+
|
| 42 |
class FileChange(BaseModel):
|
| 43 |
+
filename: str
|
| 44 |
+
patch: str
|
| 45 |
additions: int = 0
|
| 46 |
deletions: int = 0
|
| 47 |
|
| 48 |
+
|
| 49 |
class GroundTruthIssue(BaseModel):
|
| 50 |
+
id: str
|
| 51 |
+
category: Category
|
| 52 |
+
severity: Severity
|
| 53 |
+
filename: str
|
| 54 |
+
line_number: int
|
| 55 |
+
description: str
|
| 56 |
+
keywords: List[str]
|
| 57 |
required_verdict: Optional[Verdict] = None
|
| 58 |
|
| 59 |
+
|
| 60 |
class ActionRecord(BaseModel):
|
| 61 |
action_type: ActionType
|
| 62 |
+
body: str
|
| 63 |
+
filename: Optional[str] = None
|
| 64 |
+
line_number: Optional[int] = None
|
| 65 |
+
severity: Optional[Severity] = None
|
| 66 |
+
category: Optional[Category] = None
|
| 67 |
+
verdict: Optional[Verdict] = None
|
| 68 |
+
|
| 69 |
|
| 70 |
class Action(BaseModel):
|
| 71 |
action_type: ActionType
|
| 72 |
+
body: str
|
| 73 |
+
filename: Optional[str] = None
|
| 74 |
+
line_number: Optional[int] = None
|
| 75 |
+
severity: Optional[Severity] = None
|
| 76 |
+
category: Optional[Category] = None
|
| 77 |
+
verdict: Optional[Verdict] = None
|
| 78 |
|
| 79 |
@model_validator(mode='after')
|
| 80 |
def validate_action(self) -> 'Action':
|
|
|
|
| 83 |
raise ValueError("flag_issue requires severity and category")
|
| 84 |
if not self.filename or not self.line_number:
|
| 85 |
raise ValueError("flag_issue requires filename and line_number")
|
| 86 |
+
|
| 87 |
if self.action_type in (ActionType.APPROVE, ActionType.REQUEST_CHANGES):
|
| 88 |
if not self.verdict:
|
| 89 |
raise ValueError(f"{self.action_type.value} requires a verdict")
|
| 90 |
+
|
| 91 |
return self
|
| 92 |
|
| 93 |
+
|
| 94 |
class Observation(BaseModel):
|
| 95 |
+
task_id: TaskId
|
| 96 |
+
pr_title: str
|
| 97 |
+
pr_description: str
|
| 98 |
+
diff: str
|
| 99 |
+
files_changed: List[FileChange]
|
| 100 |
+
step_count: int
|
| 101 |
+
max_steps: int
|
| 102 |
+
history: List[ActionRecord]
|
| 103 |
+
noise_budget: int
|
| 104 |
+
# ── Context-enriched fields (blast radius / service metadata) ──────────
|
| 105 |
+
affected_users: int = 0
|
| 106 |
+
service_criticality: Literal["low", "medium", "high", "critical"] = "medium"
|
| 107 |
+
blast_radius: Literal["low", "medium", "high", "critical"] = "medium"
|
| 108 |
+
service_name: str = "unknown-service"
|
| 109 |
+
|
| 110 |
|
| 111 |
class ResetResult(BaseModel):
|
| 112 |
+
observation: Observation
|
| 113 |
+
task_id: TaskId
|
| 114 |
+
seed: int
|
| 115 |
+
scenario_hash: str
|
| 116 |
+
|
| 117 |
|
| 118 |
class StepResult(BaseModel):
|
| 119 |
observation: Observation
|
| 120 |
+
reward: float # incremental reward delta for this step
|
| 121 |
+
done: bool
|
| 122 |
+
info: Dict[str, Any]
|
| 123 |
+
|
| 124 |
|
| 125 |
class EpisodeResult(BaseModel):
|
| 126 |
+
task_id: TaskId
|
| 127 |
+
seed: int
|
| 128 |
+
total_steps: int
|
| 129 |
+
final_score: float
|
| 130 |
+
issues_found: List[str] # IDs of ground truth issues correctly found
|
| 131 |
+
issues_missed: List[str] # IDs of ground truth issues missed
|
| 132 |
+
false_positives: List[str] # descriptions of false-positive actions
|
| 133 |
verdict_correct: Optional[bool] = None
|
| 134 |
|
| 135 |
+
|
| 136 |
+
class StateResult(BaseModel):
|
| 137 |
+
"""Snapshot of current episode state — required by OpenEnv /state endpoint."""
|
| 138 |
+
episode_id: str
|
| 139 |
+
task_id: TaskId
|
| 140 |
+
step: int
|
| 141 |
+
max_steps: int
|
| 142 |
+
scenario_hash: str
|
| 143 |
+
cumulative_score: float
|
| 144 |
+
noise_budget: int
|
| 145 |
+
issues_found: List[str]
|
| 146 |
+
done: bool
|
| 147 |
+
|
| 148 |
+
|
| 149 |
class Scenario(BaseModel):
|
| 150 |
+
task_id: TaskId
|
| 151 |
+
pr_title: str
|
| 152 |
+
pr_description: str
|
| 153 |
+
files_changed: List[FileChange]
|
| 154 |
+
ground_truth_issues: List[GroundTruthIssue]
|
| 155 |
+
hash: str
|
| 156 |
+
# ── Scenario-level blast radius metadata ──────────────────────────────
|
| 157 |
+
affected_users: int = 0
|
| 158 |
+
service_criticality: Literal["low", "medium", "high", "critical"] = "medium"
|
| 159 |
+
blast_radius: Literal["low", "medium", "high", "critical"] = "medium"
|
| 160 |
+
service_name: str = "unknown-service"
|
|
@@ -1,30 +1,38 @@
|
|
| 1 |
import random
|
|
|
|
|
|
|
|
|
|
| 2 |
from codereview_env.models import (
|
| 3 |
Scenario, FileChange, GroundTruthIssue, Category, Severity, TaskId, Verdict
|
| 4 |
)
|
| 5 |
|
|
|
|
| 6 |
def get_scenario(task_id: TaskId, seed: int) -> Scenario:
|
| 7 |
-
rng
|
| 8 |
bank = SCENARIOS.get(task_id, [])
|
| 9 |
if not bank:
|
| 10 |
raise ValueError(f"No scenarios found for task: {task_id}")
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
import json
|
| 14 |
-
idx = rng.randint(0, len(bank) - 1)
|
| 15 |
scenario = bank[idx]
|
| 16 |
-
# Dynamic hash
|
| 17 |
-
|
| 18 |
-
content = json.dumps(scenario_dict, sort_keys=True).encode()
|
| 19 |
scenario.hash = hashlib.md5(content).hexdigest()
|
| 20 |
return scenario
|
| 21 |
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
| 23 |
BUG_SCENARIOS = [
|
| 24 |
Scenario(
|
| 25 |
task_id=TaskId.BUG_DETECTION,
|
| 26 |
-
pr_title="
|
| 27 |
pr_description="Processing elements in the list but missing the last one due to range(len(x)-1).",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
files_changed=[
|
| 29 |
FileChange(
|
| 30 |
filename="utils.py",
|
|
@@ -32,17 +40,13 @@ BUG_SCENARIOS = [
|
|
| 32 |
- for i in range(len(items) - 1):
|
| 33 |
+ for i in range(len(items)):
|
| 34 |
+ print(items[i])""",
|
| 35 |
-
additions=2,
|
| 36 |
-
deletions=1
|
| 37 |
)
|
| 38 |
],
|
| 39 |
ground_truth_issues=[
|
| 40 |
GroundTruthIssue(
|
| 41 |
-
id="bug_001",
|
| 42 |
-
|
| 43 |
-
severity=Severity.MEDIUM,
|
| 44 |
-
filename="utils.py",
|
| 45 |
-
line_number=10,
|
| 46 |
description="Off-by-one error in list processing loop. Should use range(len(items)).",
|
| 47 |
keywords=["off-by-one", "index", "out of range", "boundary", "loop"]
|
| 48 |
)
|
|
@@ -51,8 +55,12 @@ BUG_SCENARIOS = [
|
|
| 51 |
),
|
| 52 |
Scenario(
|
| 53 |
task_id=TaskId.BUG_DETECTION,
|
| 54 |
-
pr_title="
|
| 55 |
pr_description="New helper to fetch data with a default empty list for items.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
files_changed=[
|
| 57 |
FileChange(
|
| 58 |
filename="api_client.py",
|
|
@@ -61,17 +69,13 @@ BUG_SCENARIOS = [
|
|
| 61 |
+def fetch_data(url: str, items: list = []):
|
| 62 |
+ items.append(url)
|
| 63 |
+ return items""",
|
| 64 |
-
additions=2,
|
| 65 |
-
deletions=1
|
| 66 |
)
|
| 67 |
],
|
| 68 |
ground_truth_issues=[
|
| 69 |
GroundTruthIssue(
|
| 70 |
-
id="bug_002",
|
| 71 |
-
|
| 72 |
-
severity=Severity.HIGH,
|
| 73 |
-
filename="api_client.py",
|
| 74 |
-
line_number=5,
|
| 75 |
description="Mutable default argument in Python. Items list will be shared across calls.",
|
| 76 |
keywords=["mutable", "default", "argument", "persistent", "shared state"]
|
| 77 |
)
|
|
@@ -80,8 +84,12 @@ BUG_SCENARIOS = [
|
|
| 80 |
),
|
| 81 |
Scenario(
|
| 82 |
task_id=TaskId.BUG_DETECTION,
|
| 83 |
-
pr_title="
|
| 84 |
pr_description="Lookup user by ID and access properties without guard.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
files_changed=[
|
| 86 |
FileChange(
|
| 87 |
filename="auth.py",
|
|
@@ -90,17 +98,13 @@ BUG_SCENARIOS = [
|
|
| 90 |
- user = db.users.get(uid)
|
| 91 |
+ user = db.users.get(uid)
|
| 92 |
+ return user.role""",
|
| 93 |
-
additions=1,
|
| 94 |
-
deletions=1
|
| 95 |
)
|
| 96 |
],
|
| 97 |
ground_truth_issues=[
|
| 98 |
GroundTruthIssue(
|
| 99 |
-
id="bug_003",
|
| 100 |
-
|
| 101 |
-
severity=Severity.HIGH,
|
| 102 |
-
filename="auth.py",
|
| 103 |
-
line_number=16,
|
| 104 |
description="Potential None dereference. user might be None if ID is not found.",
|
| 105 |
keywords=["None", "null check", "KeyError", "AttributeError", "guard clause"]
|
| 106 |
)
|
|
@@ -109,8 +113,12 @@ BUG_SCENARIOS = [
|
|
| 109 |
),
|
| 110 |
Scenario(
|
| 111 |
task_id=TaskId.BUG_DETECTION,
|
| 112 |
-
pr_title="
|
| 113 |
pr_description="Check if setting is enabled and update status.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
files_changed=[
|
| 115 |
FileChange(
|
| 116 |
filename="config_manager.py",
|
|
@@ -118,17 +126,13 @@ BUG_SCENARIOS = [
|
|
| 118 |
- if config.enabled == True:
|
| 119 |
+ if config.status = "active":
|
| 120 |
+ process_config(config)""",
|
| 121 |
-
additions=1,
|
| 122 |
-
deletions=1
|
| 123 |
)
|
| 124 |
],
|
| 125 |
ground_truth_issues=[
|
| 126 |
GroundTruthIssue(
|
| 127 |
-
id="bug_004",
|
| 128 |
-
|
| 129 |
-
severity=Severity.MEDIUM,
|
| 130 |
-
filename="config_manager.py",
|
| 131 |
-
line_number=8,
|
| 132 |
description="Assignment operator used in conditional statement. Should be '=='.",
|
| 133 |
keywords=["assignment", "comparison", "conditional", "operator", "typo"]
|
| 134 |
)
|
|
@@ -137,8 +141,12 @@ BUG_SCENARIOS = [
|
|
| 137 |
),
|
| 138 |
Scenario(
|
| 139 |
task_id=TaskId.BUG_DETECTION,
|
| 140 |
-
pr_title="
|
| 141 |
pr_description="Counter for processed records doesn't reset.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
files_changed=[
|
| 143 |
FileChange(
|
| 144 |
filename="processor.py",
|
|
@@ -147,17 +155,13 @@ BUG_SCENARIOS = [
|
|
| 147 |
+ processed_count += 1
|
| 148 |
+ if processed_count > 1000000:
|
| 149 |
+ log.warning("High volume")""",
|
| 150 |
-
additions=2,
|
| 151 |
-
deletions=1
|
| 152 |
)
|
| 153 |
],
|
| 154 |
ground_truth_issues=[
|
| 155 |
GroundTruthIssue(
|
| 156 |
-
id="bug_005",
|
| 157 |
-
|
| 158 |
-
severity=Severity.MEDIUM,
|
| 159 |
-
filename="processor.py",
|
| 160 |
-
line_number=25,
|
| 161 |
description="Integer overflow or lack of reset in counter. Can lead to boundary issues.",
|
| 162 |
keywords=["overflow", "counter", "integer", "reset", "boundary", "infinite"]
|
| 163 |
)
|
|
@@ -166,8 +170,12 @@ BUG_SCENARIOS = [
|
|
| 166 |
),
|
| 167 |
Scenario(
|
| 168 |
task_id=TaskId.BUG_DETECTION,
|
| 169 |
-
pr_title="
|
| 170 |
pr_description="Parallel threads updating shared cache without locking.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
files_changed=[
|
| 172 |
FileChange(
|
| 173 |
filename="cache_store.py",
|
|
@@ -176,17 +184,13 @@ BUG_SCENARIOS = [
|
|
| 176 |
- cache[key] = val
|
| 177 |
+ old_val = cache[key]
|
| 178 |
+ cache[key] = old_val + val""",
|
| 179 |
-
additions=1,
|
| 180 |
-
deletions=1
|
| 181 |
)
|
| 182 |
],
|
| 183 |
ground_truth_issues=[
|
| 184 |
GroundTruthIssue(
|
| 185 |
-
id="bug_006",
|
| 186 |
-
|
| 187 |
-
severity=Severity.HIGH,
|
| 188 |
-
filename="cache_store.py",
|
| 189 |
-
line_number=13,
|
| 190 |
description="Race condition in cache update. Multiple threads may overwrite each other's increments.",
|
| 191 |
keywords=["race condition", "thread", "concurrent", "lock", "atomic", "synchronization"]
|
| 192 |
)
|
|
@@ -195,8 +199,12 @@ BUG_SCENARIOS = [
|
|
| 195 |
),
|
| 196 |
Scenario(
|
| 197 |
task_id=TaskId.BUG_DETECTION,
|
| 198 |
-
pr_title="
|
| 199 |
pr_description="Swallow all errors during data import.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
files_changed=[
|
| 201 |
FileChange(
|
| 202 |
filename="importer.py",
|
|
@@ -204,17 +212,13 @@ BUG_SCENARIOS = [
|
|
| 204 |
- import_data(file)
|
| 205 |
+ try: import_data(file)
|
| 206 |
+ except Exception: pass""",
|
| 207 |
-
additions=1,
|
| 208 |
-
deletions=1
|
| 209 |
)
|
| 210 |
],
|
| 211 |
ground_truth_issues=[
|
| 212 |
GroundTruthIssue(
|
| 213 |
-
id="bug_007",
|
| 214 |
-
|
| 215 |
-
severity=Severity.MEDIUM,
|
| 216 |
-
filename="importer.py",
|
| 217 |
-
line_number=31,
|
| 218 |
description="Broad exception catch-all. Swallows all errors including keyboard interrupts.",
|
| 219 |
keywords=["exception", "broad", "catch-all", "specific", "silent", "swallow"]
|
| 220 |
)
|
|
@@ -223,8 +227,12 @@ BUG_SCENARIOS = [
|
|
| 223 |
),
|
| 224 |
Scenario(
|
| 225 |
task_id=TaskId.BUG_DETECTION,
|
| 226 |
-
pr_title="
|
| 227 |
pr_description="Check if sensor reading is exactly 0.1.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
files_changed=[
|
| 229 |
FileChange(
|
| 230 |
filename="sensors.py",
|
|
@@ -232,17 +240,13 @@ BUG_SCENARIOS = [
|
|
| 232 |
- if reading < 0.1:
|
| 233 |
+ if reading == 0.1:
|
| 234 |
+ trigger_alarm()""",
|
| 235 |
-
additions=1,
|
| 236 |
-
deletions=1
|
| 237 |
)
|
| 238 |
],
|
| 239 |
ground_truth_issues=[
|
| 240 |
GroundTruthIssue(
|
| 241 |
-
id="bug_008",
|
| 242 |
-
|
| 243 |
-
severity=Severity.LOW,
|
| 244 |
-
filename="sensors.py",
|
| 245 |
-
line_number=7,
|
| 246 |
description="Floating point equality comparison is unreliable due to precision.",
|
| 247 |
keywords=["float", "equality", "precision", "epsilon", "comparison", "IEEE 754"]
|
| 248 |
)
|
|
@@ -251,8 +255,12 @@ BUG_SCENARIOS = [
|
|
| 251 |
),
|
| 252 |
Scenario(
|
| 253 |
task_id=TaskId.BUG_DETECTION,
|
| 254 |
-
pr_title="
|
| 255 |
pr_description="Override potential errors with a success status.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
files_changed=[
|
| 257 |
FileChange(
|
| 258 |
filename="worker.py",
|
|
@@ -261,17 +269,13 @@ BUG_SCENARIOS = [
|
|
| 261 |
+ try: process()
|
| 262 |
+ finally:
|
| 263 |
+ return "success" """,
|
| 264 |
-
additions=2,
|
| 265 |
-
deletions=1
|
| 266 |
)
|
| 267 |
],
|
| 268 |
ground_truth_issues=[
|
| 269 |
GroundTruthIssue(
|
| 270 |
-
id="bug_009",
|
| 271 |
-
|
| 272 |
-
severity=Severity.MEDIUM,
|
| 273 |
-
filename="worker.py",
|
| 274 |
-
line_number=46,
|
| 275 |
description="Return inside finally block overrides and suppresses exceptions.",
|
| 276 |
keywords=["finally", "return", "exception", "control flow", "override", "suppress"]
|
| 277 |
)
|
|
@@ -280,8 +284,12 @@ BUG_SCENARIOS = [
|
|
| 280 |
),
|
| 281 |
Scenario(
|
| 282 |
task_id=TaskId.BUG_DETECTION,
|
| 283 |
-
pr_title="
|
| 284 |
pr_description="Compare incoming string ID with integer constant.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
files_changed=[
|
| 286 |
FileChange(
|
| 287 |
filename="validator.py",
|
|
@@ -289,17 +297,13 @@ BUG_SCENARIOS = [
|
|
| 289 |
- if int(obj_id) == 5:
|
| 290 |
+ if obj_id == 5:
|
| 291 |
+ return True""",
|
| 292 |
-
additions=1,
|
| 293 |
-
deletions=1
|
| 294 |
)
|
| 295 |
],
|
| 296 |
ground_truth_issues=[
|
| 297 |
GroundTruthIssue(
|
| 298 |
-
id="bug_010",
|
| 299 |
-
|
| 300 |
-
severity=Severity.MEDIUM,
|
| 301 |
-
filename="validator.py",
|
| 302 |
-
line_number=12,
|
| 303 |
description="Type mismatch: comparing string obj_id with integer 5 will always be False.",
|
| 304 |
keywords=["type", "coercion", "comparison", "string", "integer", "implicit"]
|
| 305 |
)
|
|
@@ -308,29 +312,32 @@ BUG_SCENARIOS = [
|
|
| 308 |
)
|
| 309 |
]
|
| 310 |
|
| 311 |
-
|
|
|
|
|
|
|
|
|
|
| 312 |
SECURITY_SCENARIOS = [
|
| 313 |
Scenario(
|
| 314 |
task_id=TaskId.SECURITY_AUDIT,
|
| 315 |
-
pr_title="
|
| 316 |
pr_description="Bypassing ORM for a specific complex query to improve performance.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
files_changed=[
|
| 318 |
FileChange(
|
| 319 |
filename="db/queries.py",
|
| 320 |
patch="""@@ -42,1 +42,1 @@
|
| 321 |
- return User.objects.filter(username=name)
|
| 322 |
-
+ return User.objects.raw(f"SELECT * FROM users WHERE username = '{name}'")""",
|
| 323 |
-
additions=1,
|
| 324 |
-
deletions=1
|
| 325 |
)
|
| 326 |
],
|
| 327 |
ground_truth_issues=[
|
| 328 |
GroundTruthIssue(
|
| 329 |
-
id="sec_001",
|
| 330 |
-
|
| 331 |
-
severity=Severity.CRITICAL,
|
| 332 |
-
filename="db/queries.py",
|
| 333 |
-
line_number=42,
|
| 334 |
description="SQL injection vulnerability via f-string in raw query. Use parameterized queries.",
|
| 335 |
keywords=["SQL injection", "parameterized", "f-string", "raw query", "exploit"]
|
| 336 |
)
|
|
@@ -339,25 +346,25 @@ SECURITY_SCENARIOS = [
|
|
| 339 |
),
|
| 340 |
Scenario(
|
| 341 |
task_id=TaskId.SECURITY_AUDIT,
|
| 342 |
-
pr_title="
|
| 343 |
pr_description="Setting a default secret key for local development convenience.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
files_changed=[
|
| 345 |
FileChange(
|
| 346 |
filename="settings.py",
|
| 347 |
patch="""@@ -20,1 +20,1 @@
|
| 348 |
-SECRET_KEY = os.environ.get('SECRET_KEY')
|
| 349 |
+SECRET_KEY = "django-insecure-dev-key-12345" """,
|
| 350 |
-
additions=1,
|
| 351 |
-
deletions=1
|
| 352 |
)
|
| 353 |
],
|
| 354 |
ground_truth_issues=[
|
| 355 |
GroundTruthIssue(
|
| 356 |
-
id="sec_002",
|
| 357 |
-
|
| 358 |
-
severity=Severity.HIGH,
|
| 359 |
-
filename="settings.py",
|
| 360 |
-
line_number=20,
|
| 361 |
description="Hardcoded secret key in configuration. Should use environment variables.",
|
| 362 |
keywords=["hardcoded", "secret", "environment variable", ".env", "credential", "exposure"]
|
| 363 |
)
|
|
@@ -366,25 +373,25 @@ SECURITY_SCENARIOS = [
|
|
| 366 |
),
|
| 367 |
Scenario(
|
| 368 |
task_id=TaskId.SECURITY_AUDIT,
|
| 369 |
-
pr_title="
|
| 370 |
pr_description="Allow bypassing JWT checks for faster local development loop.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
files_changed=[
|
| 372 |
FileChange(
|
| 373 |
filename="tokens.py",
|
| 374 |
patch="""@@ -10,1 +10,1 @@
|
| 375 |
- payload = jwt.decode(token, secret, algorithms=["HS256"])
|
| 376 |
+ payload = jwt.decode(token, verify=False, algorithms=["HS256"])""",
|
| 377 |
-
additions=1,
|
| 378 |
-
deletions=1
|
| 379 |
)
|
| 380 |
],
|
| 381 |
ground_truth_issues=[
|
| 382 |
GroundTruthIssue(
|
| 383 |
-
id="sec_003",
|
| 384 |
-
|
| 385 |
-
severity=Severity.CRITICAL,
|
| 386 |
-
filename="tokens.py",
|
| 387 |
-
line_number=10,
|
| 388 |
description="JWT decoded without verification. Attackers can bypass authentication.",
|
| 389 |
keywords=["JWT", "signature", "verification", "algorithm", "none", "bypass"]
|
| 390 |
)
|
|
@@ -393,26 +400,26 @@ SECURITY_SCENARIOS = [
|
|
| 393 |
),
|
| 394 |
Scenario(
|
| 395 |
task_id=TaskId.SECURITY_AUDIT,
|
| 396 |
-
pr_title="
|
| 397 |
pr_description="Enabling rich text in user bios by using mark_safe.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
files_changed=[
|
| 399 |
FileChange(
|
| 400 |
filename="templates/profile.html",
|
| 401 |
patch="""@@ -5,1 +5,1 @@
|
| 402 |
- <div class="bio">{{ user.bio }}</div>
|
| 403 |
+ <div class="bio">{{ user.bio | mark_safe }}</div>""",
|
| 404 |
-
additions=1,
|
| 405 |
-
deletions=1
|
| 406 |
)
|
| 407 |
],
|
| 408 |
ground_truth_issues=[
|
| 409 |
GroundTruthIssue(
|
| 410 |
-
id="sec_004",
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
filename="templates/profile.html",
|
| 414 |
-
line_number=5,
|
| 415 |
-
description="Cross-site scripting (XSS) via unescaped template variable. sanitize user input.",
|
| 416 |
keywords=["XSS", "cross-site scripting", "mark_safe", "escape", "sanitize", "inject"]
|
| 417 |
)
|
| 418 |
],
|
|
@@ -420,8 +427,12 @@ SECURITY_SCENARIOS = [
|
|
| 420 |
),
|
| 421 |
Scenario(
|
| 422 |
task_id=TaskId.SECURITY_AUDIT,
|
| 423 |
-
pr_title="
|
| 424 |
pr_description="New endpoint to read local audit logs based on path.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
files_changed=[
|
| 426 |
FileChange(
|
| 427 |
filename="logs_viewer.py",
|
|
@@ -429,18 +440,14 @@ SECURITY_SCENARIOS = [
|
|
| 429 |
def get_log(path):
|
| 430 |
- return open('/var/log/app.log').read()
|
| 431 |
+ return open('/var/log/' + path).read()""",
|
| 432 |
-
additions=1,
|
| 433 |
-
deletions=1
|
| 434 |
)
|
| 435 |
],
|
| 436 |
ground_truth_issues=[
|
| 437 |
GroundTruthIssue(
|
| 438 |
-
id="sec_005",
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
filename="logs_viewer.py",
|
| 442 |
-
line_number=13,
|
| 443 |
-
description="Path traversal vulnerability. Allow reading any file using ../ notation.",
|
| 444 |
keywords=["path traversal", "directory", "normalization", "join", "sanitize", "escape"]
|
| 445 |
)
|
| 446 |
],
|
|
@@ -448,25 +455,25 @@ SECURITY_SCENARIOS = [
|
|
| 448 |
),
|
| 449 |
Scenario(
|
| 450 |
task_id=TaskId.SECURITY_AUDIT,
|
| 451 |
-
pr_title="
|
| 452 |
pr_description="Faster state loading by using pickle format for internal caches.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 453 |
files_changed=[
|
| 454 |
FileChange(
|
| 455 |
filename="cache_util.py",
|
| 456 |
patch="""@@ -8,1 +8,1 @@
|
| 457 |
- return json.loads(data)
|
| 458 |
+ return pickle.loads(data)""",
|
| 459 |
-
additions=1,
|
| 460 |
-
deletions=1
|
| 461 |
)
|
| 462 |
],
|
| 463 |
ground_truth_issues=[
|
| 464 |
GroundTruthIssue(
|
| 465 |
-
id="sec_006",
|
| 466 |
-
|
| 467 |
-
severity=Severity.CRITICAL,
|
| 468 |
-
filename="cache_util.py",
|
| 469 |
-
line_number=8,
|
| 470 |
description="Insecure deserialization using pickle leads to Arbitrary Code Execution (RCE).",
|
| 471 |
keywords=["deserialization", "pickle", "arbitrary code", "RCE", "untrusted", "injection"]
|
| 472 |
)
|
|
@@ -475,25 +482,25 @@ SECURITY_SCENARIOS = [
|
|
| 475 |
),
|
| 476 |
Scenario(
|
| 477 |
task_id=TaskId.SECURITY_AUDIT,
|
| 478 |
-
pr_title="
|
| 479 |
pr_description="Resolving frontend browser errors by allowing all origins.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
files_changed=[
|
| 481 |
FileChange(
|
| 482 |
filename="api_gateway.py",
|
| 483 |
patch="""@@ -15,1 +15,1 @@
|
| 484 |
- allow_origins=["https://myapp.com"],
|
| 485 |
+ allow_origins=["*"],""",
|
| 486 |
-
additions=1,
|
| 487 |
-
deletions=1
|
| 488 |
)
|
| 489 |
],
|
| 490 |
ground_truth_issues=[
|
| 491 |
GroundTruthIssue(
|
| 492 |
-
id="sec_007",
|
| 493 |
-
|
| 494 |
-
severity=Severity.MEDIUM,
|
| 495 |
-
filename="api_gateway.py",
|
| 496 |
-
line_number=15,
|
| 497 |
description="Broad CORS policy (*) allows sensitive data exposure to arbitrary websites.",
|
| 498 |
keywords=["CORS", "wildcard", "origin", "cross-origin", "authentication", "header"]
|
| 499 |
)
|
|
@@ -502,25 +509,25 @@ SECURITY_SCENARIOS = [
|
|
| 502 |
),
|
| 503 |
Scenario(
|
| 504 |
task_id=TaskId.SECURITY_AUDIT,
|
| 505 |
-
pr_title="
|
| 506 |
pr_description="Faster password check by using native equality.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
files_changed=[
|
| 508 |
FileChange(
|
| 509 |
filename="pass_verify.py",
|
| 510 |
patch="""@@ -10,1 +10,1 @@
|
| 511 |
- return hmac.compare_digest(h1, h2)
|
| 512 |
+ return h1 == h2""",
|
| 513 |
-
additions=1,
|
| 514 |
-
deletions=1
|
| 515 |
)
|
| 516 |
],
|
| 517 |
ground_truth_issues=[
|
| 518 |
GroundTruthIssue(
|
| 519 |
-
id="sec_008",
|
| 520 |
-
|
| 521 |
-
severity=Severity.MEDIUM,
|
| 522 |
-
filename="pass_verify.py",
|
| 523 |
-
line_number=10,
|
| 524 |
description="Timing attack vulnerability in password comparison. Use constant-time comparison.",
|
| 525 |
keywords=["timing attack", "constant time", "hmac", "comparison", "side channel"]
|
| 526 |
)
|
|
@@ -529,24 +536,24 @@ SECURITY_SCENARIOS = [
|
|
| 529 |
),
|
| 530 |
Scenario(
|
| 531 |
task_id=TaskId.SECURITY_AUDIT,
|
| 532 |
-
pr_title="
|
| 533 |
pr_description="Allowing multiple login attempts for users who forgot passwords.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 534 |
files_changed=[
|
| 535 |
FileChange(
|
| 536 |
filename="login_handler.py",
|
| 537 |
patch="""@@ -12,1 +12,0 @@
|
| 538 |
- if check_rate_limit(ip): return error()""",
|
| 539 |
-
additions=0,
|
| 540 |
-
deletions=1
|
| 541 |
)
|
| 542 |
],
|
| 543 |
ground_truth_issues=[
|
| 544 |
GroundTruthIssue(
|
| 545 |
-
id="sec_009",
|
| 546 |
-
|
| 547 |
-
severity=Severity.MEDIUM,
|
| 548 |
-
filename="login_handler.py",
|
| 549 |
-
line_number=12,
|
| 550 |
description="Missing rate limiting on login endpoint enables brute-force attacks.",
|
| 551 |
keywords=["rate limit", "brute force", "throttle", "attempt", "lockout", "login"]
|
| 552 |
)
|
|
@@ -555,25 +562,25 @@ SECURITY_SCENARIOS = [
|
|
| 555 |
),
|
| 556 |
Scenario(
|
| 557 |
task_id=TaskId.SECURITY_AUDIT,
|
| 558 |
-
pr_title="
|
| 559 |
pr_description="Better debugging in prod by enabling stack traces for 500 errors.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 560 |
files_changed=[
|
| 561 |
FileChange(
|
| 562 |
filename="prod_settings.py",
|
| 563 |
patch="""@@ -30,1 +30,1 @@
|
| 564 |
-DEBUG = False
|
| 565 |
+DEBUG = True""",
|
| 566 |
-
additions=1,
|
| 567 |
-
deletions=1
|
| 568 |
)
|
| 569 |
],
|
| 570 |
ground_truth_issues=[
|
| 571 |
GroundTruthIssue(
|
| 572 |
-
id="sec_010",
|
| 573 |
-
|
| 574 |
-
severity=Severity.HIGH,
|
| 575 |
-
filename="prod_settings.py",
|
| 576 |
-
line_number=30,
|
| 577 |
description="DEBUG mode enabled in production. Exposes sensitive system information.",
|
| 578 |
keywords=["debug", "production", "sensitive", "stack trace", "information disclosure"]
|
| 579 |
)
|
|
@@ -582,12 +589,19 @@ SECURITY_SCENARIOS = [
|
|
| 582 |
)
|
| 583 |
]
|
| 584 |
|
| 585 |
-
|
|
|
|
|
|
|
|
|
|
| 586 |
ARCH_SCENARIOS = [
|
| 587 |
Scenario(
|
| 588 |
task_id=TaskId.ARCHITECTURAL_REVIEW,
|
| 589 |
-
pr_title="
|
| 590 |
pr_description="Optimizing frontend by allowing direct database reads for dashboard data.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 591 |
files_changed=[
|
| 592 |
FileChange(
|
| 593 |
filename="services/dashboard.py",
|
|
@@ -598,17 +612,13 @@ ARCH_SCENARIOS = [
|
|
| 598 |
+ cur = conn.cursor()
|
| 599 |
+ cur.execute('SELECT * FROM stats')
|
| 600 |
+ return cur.fetchall()""",
|
| 601 |
-
additions=5,
|
| 602 |
-
deletions=1
|
| 603 |
)
|
| 604 |
],
|
| 605 |
ground_truth_issues=[
|
| 606 |
GroundTruthIssue(
|
| 607 |
-
id="arch_001",
|
| 608 |
-
|
| 609 |
-
severity=Severity.CRITICAL,
|
| 610 |
-
filename="services/dashboard.py",
|
| 611 |
-
line_number=5,
|
| 612 |
description="Frontend service calling database directly bypassing the API layer. Violates separation of concerns.",
|
| 613 |
keywords=["direct access", "coupling", "separation of concerns", "architectural violation"],
|
| 614 |
required_verdict=Verdict.REQUEST_CHANGES
|
|
@@ -618,8 +628,12 @@ ARCH_SCENARIOS = [
|
|
| 618 |
),
|
| 619 |
Scenario(
|
| 620 |
task_id=TaskId.ARCHITECTURAL_REVIEW,
|
| 621 |
-
pr_title="
|
| 622 |
pr_description="Ensuring user status is verified during login event processing.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 623 |
files_changed=[
|
| 624 |
FileChange(
|
| 625 |
filename="handlers/events.py",
|
|
@@ -628,17 +642,13 @@ ARCH_SCENARIOS = [
|
|
| 628 |
- log.info(f"User {user_id} logged in")
|
| 629 |
+ resp = requests.get(f"http://auth-service/verify/{user_id}")
|
| 630 |
+ log.info(f"User {user_id} logged in: {resp.status_code}")""",
|
| 631 |
-
additions=2,
|
| 632 |
-
deletions=1
|
| 633 |
)
|
| 634 |
],
|
| 635 |
ground_truth_issues=[
|
| 636 |
GroundTruthIssue(
|
| 637 |
-
id="arch_002",
|
| 638 |
-
|
| 639 |
-
severity=Severity.HIGH,
|
| 640 |
-
filename="handlers/events.py",
|
| 641 |
-
line_number=15,
|
| 642 |
description="Synchronous HTTP call inside event handler blocks the event loop.",
|
| 643 |
keywords=["synchronous", "blocking", "event loop", "async", "non-blocking", "timeout"],
|
| 644 |
required_verdict=Verdict.REQUEST_CHANGES
|
|
@@ -648,25 +658,25 @@ ARCH_SCENARIOS = [
|
|
| 648 |
),
|
| 649 |
Scenario(
|
| 650 |
task_id=TaskId.ARCHITECTURAL_REVIEW,
|
| 651 |
-
pr_title="
|
| 652 |
pr_description="Call downstream billing service directly.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 653 |
files_changed=[
|
| 654 |
FileChange(
|
| 655 |
filename="billing_proxy.py",
|
| 656 |
patch="""@@ -10,1 +10,1 @@
|
| 657 |
- return resiliency.call_with_retry(BILLING_URL)
|
| 658 |
+ return requests.post(BILLING_URL, data=payload)""",
|
| 659 |
-
additions=1,
|
| 660 |
-
deletions=1
|
| 661 |
)
|
| 662 |
],
|
| 663 |
ground_truth_issues=[
|
| 664 |
GroundTruthIssue(
|
| 665 |
-
id="arch_003",
|
| 666 |
-
|
| 667 |
-
severity=Severity.MEDIUM,
|
| 668 |
-
filename="billing_proxy.py",
|
| 669 |
-
line_number=10,
|
| 670 |
description="Missing retry logic and circuit breaker on external API call.",
|
| 671 |
keywords=["retry", "circuit breaker", "resilience", "idempotent", "backoff", "failure"],
|
| 672 |
required_verdict=Verdict.REQUEST_CHANGES
|
|
@@ -676,8 +686,12 @@ ARCH_SCENARIOS = [
|
|
| 676 |
),
|
| 677 |
Scenario(
|
| 678 |
task_id=TaskId.ARCHITECTURAL_REVIEW,
|
| 679 |
-
pr_title="
|
| 680 |
pr_description="Consolidating all managers into one for easier access.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 681 |
files_changed=[
|
| 682 |
FileChange(
|
| 683 |
filename="app_core.py",
|
|
@@ -687,17 +701,13 @@ ARCH_SCENARIOS = [
|
|
| 687 |
+ def handle_auth(self): pass
|
| 688 |
+ def handle_billing(self): pass
|
| 689 |
+ def handle_users(self): pass""",
|
| 690 |
-
additions=4,
|
| 691 |
-
deletions=1
|
| 692 |
)
|
| 693 |
],
|
| 694 |
ground_truth_issues=[
|
| 695 |
GroundTruthIssue(
|
| 696 |
-
id="arch_004",
|
| 697 |
-
|
| 698 |
-
severity=Severity.MEDIUM,
|
| 699 |
-
filename="app_core.py",
|
| 700 |
-
line_number=2,
|
| 701 |
description="God object pattern: one class handles unrelated domains (auth, billing, users).",
|
| 702 |
keywords=["single responsibility", "god object", "cohesion", "separation", "refactor"],
|
| 703 |
required_verdict=Verdict.REQUEST_CHANGES
|
|
@@ -707,8 +717,12 @@ ARCH_SCENARIOS = [
|
|
| 707 |
),
|
| 708 |
Scenario(
|
| 709 |
task_id=TaskId.ARCHITECTURAL_REVIEW,
|
| 710 |
-
pr_title="
|
| 711 |
pr_description="Process audit for all users one by one.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 712 |
files_changed=[
|
| 713 |
FileChange(
|
| 714 |
filename="audit_job.py",
|
|
@@ -718,17 +732,13 @@ ARCH_SCENARIOS = [
|
|
| 718 |
+ for u_id in user_ids:
|
| 719 |
+ user = User.objects.get(id=u_id)
|
| 720 |
+ process(user)""",
|
| 721 |
-
additions=2,
|
| 722 |
-
deletions=2
|
| 723 |
)
|
| 724 |
],
|
| 725 |
ground_truth_issues=[
|
| 726 |
GroundTruthIssue(
|
| 727 |
-
id="arch_005",
|
| 728 |
-
|
| 729 |
-
severity=Severity.HIGH,
|
| 730 |
-
filename="audit_job.py",
|
| 731 |
-
line_number=6,
|
| 732 |
description="N+1 query problem: fetching user objects inside a loop.",
|
| 733 |
keywords=["N+1", "query", "loop", "batch", "eager load", "select_related"],
|
| 734 |
required_verdict=Verdict.REQUEST_CHANGES
|
|
@@ -738,25 +748,25 @@ ARCH_SCENARIOS = [
|
|
| 738 |
),
|
| 739 |
Scenario(
|
| 740 |
task_id=TaskId.ARCHITECTURAL_REVIEW,
|
| 741 |
-
pr_title="
|
| 742 |
pr_description="Simple endpoint to fetch current log state.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 743 |
files_changed=[
|
| 744 |
FileChange(
|
| 745 |
filename="handlers/api.py",
|
| 746 |
patch="""@@ -20,1 +20,1 @@
|
| 747 |
-def get_logs(page, limit): return db.logs.all()[page*limit:(page+1)*limit]
|
| 748 |
+def get_logs(): return db.logs.all()""",
|
| 749 |
-
additions=1,
|
| 750 |
-
deletions=1
|
| 751 |
)
|
| 752 |
],
|
| 753 |
ground_truth_issues=[
|
| 754 |
GroundTruthIssue(
|
| 755 |
-
id="arch_006",
|
| 756 |
-
|
| 757 |
-
severity=Severity.MEDIUM,
|
| 758 |
-
filename="handlers/api.py",
|
| 759 |
-
line_number=20,
|
| 760 |
description="Missing pagination on endpoint. Can cause memory exhaustion on large datasets.",
|
| 761 |
keywords=["pagination", "limit", "offset", "memory", "unbounded", "cursor"],
|
| 762 |
required_verdict=Verdict.REQUEST_CHANGES
|
|
@@ -766,25 +776,25 @@ ARCH_SCENARIOS = [
|
|
| 766 |
),
|
| 767 |
Scenario(
|
| 768 |
task_id=TaskId.ARCHITECTURAL_REVIEW,
|
| 769 |
-
pr_title="
|
| 770 |
pr_description="Directly saving large file uploads to disk in request thread.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 771 |
files_changed=[
|
| 772 |
FileChange(
|
| 773 |
filename="upload_service.py",
|
| 774 |
patch="""@@ -12,1 +12,1 @@
|
| 775 |
- await background_save(file)
|
| 776 |
+ file.save('/tmp/large_file')""",
|
| 777 |
-
additions=1,
|
| 778 |
-
deletions=1
|
| 779 |
)
|
| 780 |
],
|
| 781 |
ground_truth_issues=[
|
| 782 |
GroundTruthIssue(
|
| 783 |
-
id="arch_007",
|
| 784 |
-
|
| 785 |
-
severity=Severity.MEDIUM,
|
| 786 |
-
filename="upload_service.py",
|
| 787 |
-
line_number=13,
|
| 788 |
description="Synchronous file upload blocking the request thread. Use background tasks.",
|
| 789 |
keywords=["async", "upload", "background task", "streaming", "thread", "non-blocking"],
|
| 790 |
required_verdict=Verdict.REQUEST_CHANGES
|
|
@@ -794,25 +804,25 @@ ARCH_SCENARIOS = [
|
|
| 794 |
),
|
| 795 |
Scenario(
|
| 796 |
task_id=TaskId.ARCHITECTURAL_REVIEW,
|
| 797 |
-
pr_title="
|
| 798 |
pr_description="Update balance directly on payment request.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 799 |
files_changed=[
|
| 800 |
FileChange(
|
| 801 |
filename="checkout.py",
|
| 802 |
patch="""@@ -8,1 +8,1 @@
|
| 803 |
- process_payment_with_idempotency(req)
|
| 804 |
+ user.balance -= req.amount""",
|
| 805 |
-
additions=1,
|
| 806 |
-
deletions=1
|
| 807 |
)
|
| 808 |
],
|
| 809 |
ground_truth_issues=[
|
| 810 |
GroundTruthIssue(
|
| 811 |
-
id="arch_008",
|
| 812 |
-
|
| 813 |
-
severity=Severity.HIGH,
|
| 814 |
-
filename="checkout.py",
|
| 815 |
-
line_number=8,
|
| 816 |
description="Missing idempotency key on payment mutation endpoint. Dangerous on retries.",
|
| 817 |
keywords=["idempotency", "duplicate", "payment", "retry", "key", "mutation"],
|
| 818 |
required_verdict=Verdict.REQUEST_CHANGES
|
|
@@ -822,25 +832,25 @@ ARCH_SCENARIOS = [
|
|
| 822 |
),
|
| 823 |
Scenario(
|
| 824 |
task_id=TaskId.ARCHITECTURAL_REVIEW,
|
| 825 |
-
pr_title="
|
| 826 |
pr_description="Service B updates Service A's table directly for speed.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 827 |
files_changed=[
|
| 828 |
FileChange(
|
| 829 |
filename="service_b/sync.py",
|
| 830 |
patch="""@@ -22,1 +22,1 @@
|
| 831 |
- send_event_to_service_a(data)
|
| 832 |
+ db.execute('UPDATE service_a_table SET x = 1')""",
|
| 833 |
-
additions=1,
|
| 834 |
-
deletions=1
|
| 835 |
)
|
| 836 |
],
|
| 837 |
ground_truth_issues=[
|
| 838 |
GroundTruthIssue(
|
| 839 |
-
id="arch_009",
|
| 840 |
-
|
| 841 |
-
severity=Severity.HIGH,
|
| 842 |
-
filename="service_b/sync.py",
|
| 843 |
-
line_number=23,
|
| 844 |
description="Shared mutable state between microservices via direct DB write. Breaks encapsulation.",
|
| 845 |
keywords=["shared state", "microservice", "event", "eventual consistency", "ownership", "coupling"],
|
| 846 |
required_verdict=Verdict.REQUEST_CHANGES
|
|
@@ -850,8 +860,12 @@ ARCH_SCENARIOS = [
|
|
| 850 |
),
|
| 851 |
Scenario(
|
| 852 |
task_id=TaskId.ARCHITECTURAL_REVIEW,
|
| 853 |
-
pr_title="
|
| 854 |
pr_description="Complex interest calculation directly in the GET endpoint.",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 855 |
files_changed=[
|
| 856 |
FileChange(
|
| 857 |
filename="api/finance.py",
|
|
@@ -860,17 +874,13 @@ ARCH_SCENARIOS = [
|
|
| 860 |
+ interest = u.balance * 0.05
|
| 861 |
+ if u.type == 'GOLD': interest += 10
|
| 862 |
+ return interest""",
|
| 863 |
-
additions=3,
|
| 864 |
-
deletions=1
|
| 865 |
)
|
| 866 |
],
|
| 867 |
ground_truth_issues=[
|
| 868 |
GroundTruthIssue(
|
| 869 |
-
id="arch_010",
|
| 870 |
-
|
| 871 |
-
severity=Severity.MEDIUM,
|
| 872 |
-
filename="api/finance.py",
|
| 873 |
-
line_number=16,
|
| 874 |
description="Clean architecture violation: domain logic leaked into HTTP handler.",
|
| 875 |
keywords=["clean architecture", "domain", "handler", "concern", "presentation", "business logic"],
|
| 876 |
required_verdict=Verdict.REQUEST_CHANGES
|
|
@@ -880,8 +890,9 @@ ARCH_SCENARIOS = [
|
|
| 880 |
)
|
| 881 |
]
|
| 882 |
|
|
|
|
| 883 |
SCENARIOS = {
|
| 884 |
-
TaskId.BUG_DETECTION:
|
| 885 |
-
TaskId.SECURITY_AUDIT:
|
| 886 |
TaskId.ARCHITECTURAL_REVIEW: ARCH_SCENARIOS,
|
| 887 |
}
|
|
|
|
| 1 |
import random
|
| 2 |
+
import hashlib
|
| 3 |
+
import json
|
| 4 |
+
|
| 5 |
from codereview_env.models import (
|
| 6 |
Scenario, FileChange, GroundTruthIssue, Category, Severity, TaskId, Verdict
|
| 7 |
)
|
| 8 |
|
| 9 |
+
|
| 10 |
def get_scenario(task_id: TaskId, seed: int) -> Scenario:
|
| 11 |
+
rng = random.Random(seed)
|
| 12 |
bank = SCENARIOS.get(task_id, [])
|
| 13 |
if not bank:
|
| 14 |
raise ValueError(f"No scenarios found for task: {task_id}")
|
| 15 |
+
|
| 16 |
+
idx = rng.randint(0, len(bank) - 1)
|
|
|
|
|
|
|
| 17 |
scenario = bank[idx]
|
| 18 |
+
# Dynamic hash — recalculated on every fetch
|
| 19 |
+
content = json.dumps(scenario.model_dump(), sort_keys=True).encode()
|
|
|
|
| 20 |
scenario.hash = hashlib.md5(content).hexdigest()
|
| 21 |
return scenario
|
| 22 |
|
| 23 |
+
|
| 24 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 25 |
+
# BUG DETECTION SCENARIOS (10)
|
| 26 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 27 |
BUG_SCENARIOS = [
|
| 28 |
Scenario(
|
| 29 |
task_id=TaskId.BUG_DETECTION,
|
| 30 |
+
pr_title="data-pipeline: speed up list processing by removing +1 in range",
|
| 31 |
pr_description="Processing elements in the list but missing the last one due to range(len(x)-1).",
|
| 32 |
+
service_name="data-pipeline-service",
|
| 33 |
+
affected_users=0,
|
| 34 |
+
service_criticality="low",
|
| 35 |
+
blast_radius="low",
|
| 36 |
files_changed=[
|
| 37 |
FileChange(
|
| 38 |
filename="utils.py",
|
|
|
|
| 40 |
- for i in range(len(items) - 1):
|
| 41 |
+ for i in range(len(items)):
|
| 42 |
+ print(items[i])""",
|
| 43 |
+
additions=2, deletions=1
|
|
|
|
| 44 |
)
|
| 45 |
],
|
| 46 |
ground_truth_issues=[
|
| 47 |
GroundTruthIssue(
|
| 48 |
+
id="bug_001", category=Category.BUG, severity=Severity.MEDIUM,
|
| 49 |
+
filename="utils.py", line_number=10,
|
|
|
|
|
|
|
|
|
|
| 50 |
description="Off-by-one error in list processing loop. Should use range(len(items)).",
|
| 51 |
keywords=["off-by-one", "index", "out of range", "boundary", "loop"]
|
| 52 |
)
|
|
|
|
| 55 |
),
|
| 56 |
Scenario(
|
| 57 |
task_id=TaskId.BUG_DETECTION,
|
| 58 |
+
pr_title="api-client: add default empty list to fetch_data helper",
|
| 59 |
pr_description="New helper to fetch data with a default empty list for items.",
|
| 60 |
+
service_name="api-client-service",
|
| 61 |
+
affected_users=5000,
|
| 62 |
+
service_criticality="medium",
|
| 63 |
+
blast_radius="medium",
|
| 64 |
files_changed=[
|
| 65 |
FileChange(
|
| 66 |
filename="api_client.py",
|
|
|
|
| 69 |
+def fetch_data(url: str, items: list = []):
|
| 70 |
+ items.append(url)
|
| 71 |
+ return items""",
|
| 72 |
+
additions=2, deletions=1
|
|
|
|
| 73 |
)
|
| 74 |
],
|
| 75 |
ground_truth_issues=[
|
| 76 |
GroundTruthIssue(
|
| 77 |
+
id="bug_002", category=Category.BUG, severity=Severity.HIGH,
|
| 78 |
+
filename="api_client.py", line_number=5,
|
|
|
|
|
|
|
|
|
|
| 79 |
description="Mutable default argument in Python. Items list will be shared across calls.",
|
| 80 |
keywords=["mutable", "default", "argument", "persistent", "shared state"]
|
| 81 |
)
|
|
|
|
| 84 |
),
|
| 85 |
Scenario(
|
| 86 |
task_id=TaskId.BUG_DETECTION,
|
| 87 |
+
pr_title="auth-service: return user role directly from lookup",
|
| 88 |
pr_description="Lookup user by ID and access properties without guard.",
|
| 89 |
+
service_name="auth-service",
|
| 90 |
+
affected_users=50000,
|
| 91 |
+
service_criticality="critical",
|
| 92 |
+
blast_radius="critical",
|
| 93 |
files_changed=[
|
| 94 |
FileChange(
|
| 95 |
filename="auth.py",
|
|
|
|
| 98 |
- user = db.users.get(uid)
|
| 99 |
+ user = db.users.get(uid)
|
| 100 |
+ return user.role""",
|
| 101 |
+
additions=1, deletions=1
|
|
|
|
| 102 |
)
|
| 103 |
],
|
| 104 |
ground_truth_issues=[
|
| 105 |
GroundTruthIssue(
|
| 106 |
+
id="bug_003", category=Category.BUG, severity=Severity.HIGH,
|
| 107 |
+
filename="auth.py", line_number=16,
|
|
|
|
|
|
|
|
|
|
| 108 |
description="Potential None dereference. user might be None if ID is not found.",
|
| 109 |
keywords=["None", "null check", "KeyError", "AttributeError", "guard clause"]
|
| 110 |
)
|
|
|
|
| 113 |
),
|
| 114 |
Scenario(
|
| 115 |
task_id=TaskId.BUG_DETECTION,
|
| 116 |
+
pr_title="config-manager: simplify active status check",
|
| 117 |
pr_description="Check if setting is enabled and update status.",
|
| 118 |
+
service_name="config-manager",
|
| 119 |
+
affected_users=1000,
|
| 120 |
+
service_criticality="medium",
|
| 121 |
+
blast_radius="medium",
|
| 122 |
files_changed=[
|
| 123 |
FileChange(
|
| 124 |
filename="config_manager.py",
|
|
|
|
| 126 |
- if config.enabled == True:
|
| 127 |
+ if config.status = "active":
|
| 128 |
+ process_config(config)""",
|
| 129 |
+
additions=1, deletions=1
|
|
|
|
| 130 |
)
|
| 131 |
],
|
| 132 |
ground_truth_issues=[
|
| 133 |
GroundTruthIssue(
|
| 134 |
+
id="bug_004", category=Category.BUG, severity=Severity.MEDIUM,
|
| 135 |
+
filename="config_manager.py", line_number=8,
|
|
|
|
|
|
|
|
|
|
| 136 |
description="Assignment operator used in conditional statement. Should be '=='.",
|
| 137 |
keywords=["assignment", "comparison", "conditional", "operator", "typo"]
|
| 138 |
)
|
|
|
|
| 141 |
),
|
| 142 |
Scenario(
|
| 143 |
task_id=TaskId.BUG_DETECTION,
|
| 144 |
+
pr_title="ingestion-worker: add high-volume warning to processor",
|
| 145 |
pr_description="Counter for processed records doesn't reset.",
|
| 146 |
+
service_name="data-ingestion-worker",
|
| 147 |
+
affected_users=0,
|
| 148 |
+
service_criticality="low",
|
| 149 |
+
blast_radius="low",
|
| 150 |
files_changed=[
|
| 151 |
FileChange(
|
| 152 |
filename="processor.py",
|
|
|
|
| 155 |
+ processed_count += 1
|
| 156 |
+ if processed_count > 1000000:
|
| 157 |
+ log.warning("High volume")""",
|
| 158 |
+
additions=2, deletions=1
|
|
|
|
| 159 |
)
|
| 160 |
],
|
| 161 |
ground_truth_issues=[
|
| 162 |
GroundTruthIssue(
|
| 163 |
+
id="bug_005", category=Category.BUG, severity=Severity.MEDIUM,
|
| 164 |
+
filename="processor.py", line_number=25,
|
|
|
|
|
|
|
|
|
|
| 165 |
description="Integer overflow or lack of reset in counter. Can lead to boundary issues.",
|
| 166 |
keywords=["overflow", "counter", "integer", "reset", "boundary", "infinite"]
|
| 167 |
)
|
|
|
|
| 170 |
),
|
| 171 |
Scenario(
|
| 172 |
task_id=TaskId.BUG_DETECTION,
|
| 173 |
+
pr_title="cache-service: optimize counter update to read-modify-write",
|
| 174 |
pr_description="Parallel threads updating shared cache without locking.",
|
| 175 |
+
service_name="distributed-cache",
|
| 176 |
+
affected_users=100000,
|
| 177 |
+
service_criticality="high",
|
| 178 |
+
blast_radius="high",
|
| 179 |
files_changed=[
|
| 180 |
FileChange(
|
| 181 |
filename="cache_store.py",
|
|
|
|
| 184 |
- cache[key] = val
|
| 185 |
+ old_val = cache[key]
|
| 186 |
+ cache[key] = old_val + val""",
|
| 187 |
+
additions=1, deletions=1
|
|
|
|
| 188 |
)
|
| 189 |
],
|
| 190 |
ground_truth_issues=[
|
| 191 |
GroundTruthIssue(
|
| 192 |
+
id="bug_006", category=Category.BUG, severity=Severity.HIGH,
|
| 193 |
+
filename="cache_store.py", line_number=13,
|
|
|
|
|
|
|
|
|
|
| 194 |
description="Race condition in cache update. Multiple threads may overwrite each other's increments.",
|
| 195 |
keywords=["race condition", "thread", "concurrent", "lock", "atomic", "synchronization"]
|
| 196 |
)
|
|
|
|
| 199 |
),
|
| 200 |
Scenario(
|
| 201 |
task_id=TaskId.BUG_DETECTION,
|
| 202 |
+
pr_title="importer: silence errors during bulk data import",
|
| 203 |
pr_description="Swallow all errors during data import.",
|
| 204 |
+
service_name="bulk-importer",
|
| 205 |
+
affected_users=500,
|
| 206 |
+
service_criticality="medium",
|
| 207 |
+
blast_radius="medium",
|
| 208 |
files_changed=[
|
| 209 |
FileChange(
|
| 210 |
filename="importer.py",
|
|
|
|
| 212 |
- import_data(file)
|
| 213 |
+ try: import_data(file)
|
| 214 |
+ except Exception: pass""",
|
| 215 |
+
additions=1, deletions=1
|
|
|
|
| 216 |
)
|
| 217 |
],
|
| 218 |
ground_truth_issues=[
|
| 219 |
GroundTruthIssue(
|
| 220 |
+
id="bug_007", category=Category.BUG, severity=Severity.MEDIUM,
|
| 221 |
+
filename="importer.py", line_number=31,
|
|
|
|
|
|
|
|
|
|
| 222 |
description="Broad exception catch-all. Swallows all errors including keyboard interrupts.",
|
| 223 |
keywords=["exception", "broad", "catch-all", "specific", "silent", "swallow"]
|
| 224 |
)
|
|
|
|
| 227 |
),
|
| 228 |
Scenario(
|
| 229 |
task_id=TaskId.BUG_DETECTION,
|
| 230 |
+
pr_title="sensors: exact threshold check for alarm trigger",
|
| 231 |
pr_description="Check if sensor reading is exactly 0.1.",
|
| 232 |
+
service_name="iot-sensor-gateway",
|
| 233 |
+
affected_users=10,
|
| 234 |
+
service_criticality="low",
|
| 235 |
+
blast_radius="low",
|
| 236 |
files_changed=[
|
| 237 |
FileChange(
|
| 238 |
filename="sensors.py",
|
|
|
|
| 240 |
- if reading < 0.1:
|
| 241 |
+ if reading == 0.1:
|
| 242 |
+ trigger_alarm()""",
|
| 243 |
+
additions=1, deletions=1
|
|
|
|
| 244 |
)
|
| 245 |
],
|
| 246 |
ground_truth_issues=[
|
| 247 |
GroundTruthIssue(
|
| 248 |
+
id="bug_008", category=Category.BUG, severity=Severity.LOW,
|
| 249 |
+
filename="sensors.py", line_number=7,
|
|
|
|
|
|
|
|
|
|
| 250 |
description="Floating point equality comparison is unreliable due to precision.",
|
| 251 |
keywords=["float", "equality", "precision", "epsilon", "comparison", "IEEE 754"]
|
| 252 |
)
|
|
|
|
| 255 |
),
|
| 256 |
Scenario(
|
| 257 |
task_id=TaskId.BUG_DETECTION,
|
| 258 |
+
pr_title="worker: guarantee success status even on process failure",
|
| 259 |
pr_description="Override potential errors with a success status.",
|
| 260 |
+
service_name="background-worker",
|
| 261 |
+
affected_users=2000,
|
| 262 |
+
service_criticality="medium",
|
| 263 |
+
blast_radius="medium",
|
| 264 |
files_changed=[
|
| 265 |
FileChange(
|
| 266 |
filename="worker.py",
|
|
|
|
| 269 |
+ try: process()
|
| 270 |
+ finally:
|
| 271 |
+ return "success" """,
|
| 272 |
+
additions=2, deletions=1
|
|
|
|
| 273 |
)
|
| 274 |
],
|
| 275 |
ground_truth_issues=[
|
| 276 |
GroundTruthIssue(
|
| 277 |
+
id="bug_009", category=Category.BUG, severity=Severity.MEDIUM,
|
| 278 |
+
filename="worker.py", line_number=46,
|
|
|
|
|
|
|
|
|
|
| 279 |
description="Return inside finally block overrides and suppresses exceptions.",
|
| 280 |
keywords=["finally", "return", "exception", "control flow", "override", "suppress"]
|
| 281 |
)
|
|
|
|
| 284 |
),
|
| 285 |
Scenario(
|
| 286 |
task_id=TaskId.BUG_DETECTION,
|
| 287 |
+
pr_title="validator: simplify ID comparison in core validator",
|
| 288 |
pr_description="Compare incoming string ID with integer constant.",
|
| 289 |
+
service_name="entity-validator",
|
| 290 |
+
affected_users=20000,
|
| 291 |
+
service_criticality="high",
|
| 292 |
+
blast_radius="medium",
|
| 293 |
files_changed=[
|
| 294 |
FileChange(
|
| 295 |
filename="validator.py",
|
|
|
|
| 297 |
- if int(obj_id) == 5:
|
| 298 |
+ if obj_id == 5:
|
| 299 |
+ return True""",
|
| 300 |
+
additions=1, deletions=1
|
|
|
|
| 301 |
)
|
| 302 |
],
|
| 303 |
ground_truth_issues=[
|
| 304 |
GroundTruthIssue(
|
| 305 |
+
id="bug_010", category=Category.BUG, severity=Severity.MEDIUM,
|
| 306 |
+
filename="validator.py", line_number=12,
|
|
|
|
|
|
|
|
|
|
| 307 |
description="Type mismatch: comparing string obj_id with integer 5 will always be False.",
|
| 308 |
keywords=["type", "coercion", "comparison", "string", "integer", "implicit"]
|
| 309 |
)
|
|
|
|
| 312 |
)
|
| 313 |
]
|
| 314 |
|
| 315 |
+
|
| 316 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 317 |
+
# SECURITY AUDIT SCENARIOS (10)
|
| 318 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 319 |
SECURITY_SCENARIOS = [
|
| 320 |
Scenario(
|
| 321 |
task_id=TaskId.SECURITY_AUDIT,
|
| 322 |
+
pr_title="payment-db: replace ORM with raw SQL for performance on user lookup",
|
| 323 |
pr_description="Bypassing ORM for a specific complex query to improve performance.",
|
| 324 |
+
service_name="payment-service",
|
| 325 |
+
affected_users=1000000,
|
| 326 |
+
service_criticality="critical",
|
| 327 |
+
blast_radius="critical",
|
| 328 |
files_changed=[
|
| 329 |
FileChange(
|
| 330 |
filename="db/queries.py",
|
| 331 |
patch="""@@ -42,1 +42,1 @@
|
| 332 |
- return User.objects.filter(username=name)
|
| 333 |
+
+ return User.objects.raw(f"SELECT * FROM users WHERE username = '{name}'" )""",
|
| 334 |
+
additions=1, deletions=1
|
|
|
|
| 335 |
)
|
| 336 |
],
|
| 337 |
ground_truth_issues=[
|
| 338 |
GroundTruthIssue(
|
| 339 |
+
id="sec_001", category=Category.SECURITY, severity=Severity.CRITICAL,
|
| 340 |
+
filename="db/queries.py", line_number=42,
|
|
|
|
|
|
|
|
|
|
| 341 |
description="SQL injection vulnerability via f-string in raw query. Use parameterized queries.",
|
| 342 |
keywords=["SQL injection", "parameterized", "f-string", "raw query", "exploit"]
|
| 343 |
)
|
|
|
|
| 346 |
),
|
| 347 |
Scenario(
|
| 348 |
task_id=TaskId.SECURITY_AUDIT,
|
| 349 |
+
pr_title="settings: add default secret key for local dev convenience",
|
| 350 |
pr_description="Setting a default secret key for local development convenience.",
|
| 351 |
+
service_name="django-web-app",
|
| 352 |
+
affected_users=50000,
|
| 353 |
+
service_criticality="high",
|
| 354 |
+
blast_radius="high",
|
| 355 |
files_changed=[
|
| 356 |
FileChange(
|
| 357 |
filename="settings.py",
|
| 358 |
patch="""@@ -20,1 +20,1 @@
|
| 359 |
-SECRET_KEY = os.environ.get('SECRET_KEY')
|
| 360 |
+SECRET_KEY = "django-insecure-dev-key-12345" """,
|
| 361 |
+
additions=1, deletions=1
|
|
|
|
| 362 |
)
|
| 363 |
],
|
| 364 |
ground_truth_issues=[
|
| 365 |
GroundTruthIssue(
|
| 366 |
+
id="sec_002", category=Category.SECURITY, severity=Severity.HIGH,
|
| 367 |
+
filename="settings.py", line_number=20,
|
|
|
|
|
|
|
|
|
|
| 368 |
description="Hardcoded secret key in configuration. Should use environment variables.",
|
| 369 |
keywords=["hardcoded", "secret", "environment variable", ".env", "credential", "exposure"]
|
| 370 |
)
|
|
|
|
| 373 |
),
|
| 374 |
Scenario(
|
| 375 |
task_id=TaskId.SECURITY_AUDIT,
|
| 376 |
+
pr_title="auth-tokens: disable JWT verification for faster internal testing loop",
|
| 377 |
pr_description="Allow bypassing JWT checks for faster local development loop.",
|
| 378 |
+
service_name="auth-service",
|
| 379 |
+
affected_users=500000,
|
| 380 |
+
service_criticality="critical",
|
| 381 |
+
blast_radius="critical",
|
| 382 |
files_changed=[
|
| 383 |
FileChange(
|
| 384 |
filename="tokens.py",
|
| 385 |
patch="""@@ -10,1 +10,1 @@
|
| 386 |
- payload = jwt.decode(token, secret, algorithms=["HS256"])
|
| 387 |
+ payload = jwt.decode(token, verify=False, algorithms=["HS256"])""",
|
| 388 |
+
additions=1, deletions=1
|
|
|
|
| 389 |
)
|
| 390 |
],
|
| 391 |
ground_truth_issues=[
|
| 392 |
GroundTruthIssue(
|
| 393 |
+
id="sec_003", category=Category.SECURITY, severity=Severity.CRITICAL,
|
| 394 |
+
filename="tokens.py", line_number=10,
|
|
|
|
|
|
|
|
|
|
| 395 |
description="JWT decoded without verification. Attackers can bypass authentication.",
|
| 396 |
keywords=["JWT", "signature", "verification", "algorithm", "none", "bypass"]
|
| 397 |
)
|
|
|
|
| 400 |
),
|
| 401 |
Scenario(
|
| 402 |
task_id=TaskId.SECURITY_AUDIT,
|
| 403 |
+
pr_title="profile-template: enable rich text in user bios via mark_safe",
|
| 404 |
pr_description="Enabling rich text in user bios by using mark_safe.",
|
| 405 |
+
service_name="user-profile-service",
|
| 406 |
+
affected_users=200000,
|
| 407 |
+
service_criticality="high",
|
| 408 |
+
blast_radius="high",
|
| 409 |
files_changed=[
|
| 410 |
FileChange(
|
| 411 |
filename="templates/profile.html",
|
| 412 |
patch="""@@ -5,1 +5,1 @@
|
| 413 |
- <div class="bio">{{ user.bio }}</div>
|
| 414 |
+ <div class="bio">{{ user.bio | mark_safe }}</div>""",
|
| 415 |
+
additions=1, deletions=1
|
|
|
|
| 416 |
)
|
| 417 |
],
|
| 418 |
ground_truth_issues=[
|
| 419 |
GroundTruthIssue(
|
| 420 |
+
id="sec_004", category=Category.SECURITY, severity=Severity.HIGH,
|
| 421 |
+
filename="templates/profile.html", line_number=5,
|
| 422 |
+
description="Cross-site scripting (XSS) via unescaped template variable. Sanitize user input.",
|
|
|
|
|
|
|
|
|
|
| 423 |
keywords=["XSS", "cross-site scripting", "mark_safe", "escape", "sanitize", "inject"]
|
| 424 |
)
|
| 425 |
],
|
|
|
|
| 427 |
),
|
| 428 |
Scenario(
|
| 429 |
task_id=TaskId.SECURITY_AUDIT,
|
| 430 |
+
pr_title="log-viewer: expose log endpoint with dynamic path parameter",
|
| 431 |
pr_description="New endpoint to read local audit logs based on path.",
|
| 432 |
+
service_name="audit-log-viewer",
|
| 433 |
+
affected_users=10,
|
| 434 |
+
service_criticality="high",
|
| 435 |
+
blast_radius="high",
|
| 436 |
files_changed=[
|
| 437 |
FileChange(
|
| 438 |
filename="logs_viewer.py",
|
|
|
|
| 440 |
def get_log(path):
|
| 441 |
- return open('/var/log/app.log').read()
|
| 442 |
+ return open('/var/log/' + path).read()""",
|
| 443 |
+
additions=1, deletions=1
|
|
|
|
| 444 |
)
|
| 445 |
],
|
| 446 |
ground_truth_issues=[
|
| 447 |
GroundTruthIssue(
|
| 448 |
+
id="sec_005", category=Category.SECURITY, severity=Severity.HIGH,
|
| 449 |
+
filename="logs_viewer.py", line_number=13,
|
| 450 |
+
description="Path traversal vulnerability. Allows reading any file using ../ notation.",
|
|
|
|
|
|
|
|
|
|
| 451 |
keywords=["path traversal", "directory", "normalization", "join", "sanitize", "escape"]
|
| 452 |
)
|
| 453 |
],
|
|
|
|
| 455 |
),
|
| 456 |
Scenario(
|
| 457 |
task_id=TaskId.SECURITY_AUDIT,
|
| 458 |
+
pr_title="cache-util: switch from JSON to pickle for faster state loading",
|
| 459 |
pr_description="Faster state loading by using pickle format for internal caches.",
|
| 460 |
+
service_name="session-cache",
|
| 461 |
+
affected_users=300000,
|
| 462 |
+
service_criticality="critical",
|
| 463 |
+
blast_radius="critical",
|
| 464 |
files_changed=[
|
| 465 |
FileChange(
|
| 466 |
filename="cache_util.py",
|
| 467 |
patch="""@@ -8,1 +8,1 @@
|
| 468 |
- return json.loads(data)
|
| 469 |
+ return pickle.loads(data)""",
|
| 470 |
+
additions=1, deletions=1
|
|
|
|
| 471 |
)
|
| 472 |
],
|
| 473 |
ground_truth_issues=[
|
| 474 |
GroundTruthIssue(
|
| 475 |
+
id="sec_006", category=Category.SECURITY, severity=Severity.CRITICAL,
|
| 476 |
+
filename="cache_util.py", line_number=8,
|
|
|
|
|
|
|
|
|
|
| 477 |
description="Insecure deserialization using pickle leads to Arbitrary Code Execution (RCE).",
|
| 478 |
keywords=["deserialization", "pickle", "arbitrary code", "RCE", "untrusted", "injection"]
|
| 479 |
)
|
|
|
|
| 482 |
),
|
| 483 |
Scenario(
|
| 484 |
task_id=TaskId.SECURITY_AUDIT,
|
| 485 |
+
pr_title="api-gateway: open CORS to fix browser errors from frontend team",
|
| 486 |
pr_description="Resolving frontend browser errors by allowing all origins.",
|
| 487 |
+
service_name="api-gateway",
|
| 488 |
+
affected_users=500000,
|
| 489 |
+
service_criticality="high",
|
| 490 |
+
blast_radius="high",
|
| 491 |
files_changed=[
|
| 492 |
FileChange(
|
| 493 |
filename="api_gateway.py",
|
| 494 |
patch="""@@ -15,1 +15,1 @@
|
| 495 |
- allow_origins=["https://myapp.com"],
|
| 496 |
+ allow_origins=["*"],""",
|
| 497 |
+
additions=1, deletions=1
|
|
|
|
| 498 |
)
|
| 499 |
],
|
| 500 |
ground_truth_issues=[
|
| 501 |
GroundTruthIssue(
|
| 502 |
+
id="sec_007", category=Category.SECURITY, severity=Severity.MEDIUM,
|
| 503 |
+
filename="api_gateway.py", line_number=15,
|
|
|
|
|
|
|
|
|
|
| 504 |
description="Broad CORS policy (*) allows sensitive data exposure to arbitrary websites.",
|
| 505 |
keywords=["CORS", "wildcard", "origin", "cross-origin", "authentication", "header"]
|
| 506 |
)
|
|
|
|
| 509 |
),
|
| 510 |
Scenario(
|
| 511 |
task_id=TaskId.SECURITY_AUDIT,
|
| 512 |
+
pr_title="pass-verify: switch to direct equality for faster password comparison",
|
| 513 |
pr_description="Faster password check by using native equality.",
|
| 514 |
+
service_name="auth-service",
|
| 515 |
+
affected_users=500000,
|
| 516 |
+
service_criticality="critical",
|
| 517 |
+
blast_radius="critical",
|
| 518 |
files_changed=[
|
| 519 |
FileChange(
|
| 520 |
filename="pass_verify.py",
|
| 521 |
patch="""@@ -10,1 +10,1 @@
|
| 522 |
- return hmac.compare_digest(h1, h2)
|
| 523 |
+ return h1 == h2""",
|
| 524 |
+
additions=1, deletions=1
|
|
|
|
| 525 |
)
|
| 526 |
],
|
| 527 |
ground_truth_issues=[
|
| 528 |
GroundTruthIssue(
|
| 529 |
+
id="sec_008", category=Category.SECURITY, severity=Severity.MEDIUM,
|
| 530 |
+
filename="pass_verify.py", line_number=10,
|
|
|
|
|
|
|
|
|
|
| 531 |
description="Timing attack vulnerability in password comparison. Use constant-time comparison.",
|
| 532 |
keywords=["timing attack", "constant time", "hmac", "comparison", "side channel"]
|
| 533 |
)
|
|
|
|
| 536 |
),
|
| 537 |
Scenario(
|
| 538 |
task_id=TaskId.SECURITY_AUDIT,
|
| 539 |
+
pr_title="login-handler: remove rate limit to improve UX for forgot-password flow",
|
| 540 |
pr_description="Allowing multiple login attempts for users who forgot passwords.",
|
| 541 |
+
service_name="auth-service",
|
| 542 |
+
affected_users=500000,
|
| 543 |
+
service_criticality="critical",
|
| 544 |
+
blast_radius="critical",
|
| 545 |
files_changed=[
|
| 546 |
FileChange(
|
| 547 |
filename="login_handler.py",
|
| 548 |
patch="""@@ -12,1 +12,0 @@
|
| 549 |
- if check_rate_limit(ip): return error()""",
|
| 550 |
+
additions=0, deletions=1
|
|
|
|
| 551 |
)
|
| 552 |
],
|
| 553 |
ground_truth_issues=[
|
| 554 |
GroundTruthIssue(
|
| 555 |
+
id="sec_009", category=Category.SECURITY, severity=Severity.MEDIUM,
|
| 556 |
+
filename="login_handler.py", line_number=12,
|
|
|
|
|
|
|
|
|
|
| 557 |
description="Missing rate limiting on login endpoint enables brute-force attacks.",
|
| 558 |
keywords=["rate limit", "brute force", "throttle", "attempt", "lockout", "login"]
|
| 559 |
)
|
|
|
|
| 562 |
),
|
| 563 |
Scenario(
|
| 564 |
task_id=TaskId.SECURITY_AUDIT,
|
| 565 |
+
pr_title="prod-settings: enable DEBUG for better 500-error visibility in production",
|
| 566 |
pr_description="Better debugging in prod by enabling stack traces for 500 errors.",
|
| 567 |
+
service_name="production-webapp",
|
| 568 |
+
affected_users=1000000,
|
| 569 |
+
service_criticality="critical",
|
| 570 |
+
blast_radius="critical",
|
| 571 |
files_changed=[
|
| 572 |
FileChange(
|
| 573 |
filename="prod_settings.py",
|
| 574 |
patch="""@@ -30,1 +30,1 @@
|
| 575 |
-DEBUG = False
|
| 576 |
+DEBUG = True""",
|
| 577 |
+
additions=1, deletions=1
|
|
|
|
| 578 |
)
|
| 579 |
],
|
| 580 |
ground_truth_issues=[
|
| 581 |
GroundTruthIssue(
|
| 582 |
+
id="sec_010", category=Category.SECURITY, severity=Severity.HIGH,
|
| 583 |
+
filename="prod_settings.py", line_number=30,
|
|
|
|
|
|
|
|
|
|
| 584 |
description="DEBUG mode enabled in production. Exposes sensitive system information.",
|
| 585 |
keywords=["debug", "production", "sensitive", "stack trace", "information disclosure"]
|
| 586 |
)
|
|
|
|
| 589 |
)
|
| 590 |
]
|
| 591 |
|
| 592 |
+
|
| 593 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 594 |
+
# ARCHITECTURAL REVIEW SCENARIOS (10)
|
| 595 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 596 |
ARCH_SCENARIOS = [
|
| 597 |
Scenario(
|
| 598 |
task_id=TaskId.ARCHITECTURAL_REVIEW,
|
| 599 |
+
pr_title="dashboard-service: optimize stats by reading DB directly instead of calling API",
|
| 600 |
pr_description="Optimizing frontend by allowing direct database reads for dashboard data.",
|
| 601 |
+
service_name="dashboard-service",
|
| 602 |
+
affected_users=50000,
|
| 603 |
+
service_criticality="high",
|
| 604 |
+
blast_radius="high",
|
| 605 |
files_changed=[
|
| 606 |
FileChange(
|
| 607 |
filename="services/dashboard.py",
|
|
|
|
| 612 |
+ cur = conn.cursor()
|
| 613 |
+ cur.execute('SELECT * FROM stats')
|
| 614 |
+ return cur.fetchall()""",
|
| 615 |
+
additions=5, deletions=1
|
|
|
|
| 616 |
)
|
| 617 |
],
|
| 618 |
ground_truth_issues=[
|
| 619 |
GroundTruthIssue(
|
| 620 |
+
id="arch_001", category=Category.ARCHITECTURE, severity=Severity.CRITICAL,
|
| 621 |
+
filename="services/dashboard.py", line_number=5,
|
|
|
|
|
|
|
|
|
|
| 622 |
description="Frontend service calling database directly bypassing the API layer. Violates separation of concerns.",
|
| 623 |
keywords=["direct access", "coupling", "separation of concerns", "architectural violation"],
|
| 624 |
required_verdict=Verdict.REQUEST_CHANGES
|
|
|
|
| 628 |
),
|
| 629 |
Scenario(
|
| 630 |
task_id=TaskId.ARCHITECTURAL_REVIEW,
|
| 631 |
+
pr_title="event-handler: add real-time auth verification on user login event",
|
| 632 |
pr_description="Ensuring user status is verified during login event processing.",
|
| 633 |
+
service_name="event-bus-consumer",
|
| 634 |
+
affected_users=100000,
|
| 635 |
+
service_criticality="high",
|
| 636 |
+
blast_radius="high",
|
| 637 |
files_changed=[
|
| 638 |
FileChange(
|
| 639 |
filename="handlers/events.py",
|
|
|
|
| 642 |
- log.info(f"User {user_id} logged in")
|
| 643 |
+ resp = requests.get(f"http://auth-service/verify/{user_id}")
|
| 644 |
+ log.info(f"User {user_id} logged in: {resp.status_code}")""",
|
| 645 |
+
additions=2, deletions=1
|
|
|
|
| 646 |
)
|
| 647 |
],
|
| 648 |
ground_truth_issues=[
|
| 649 |
GroundTruthIssue(
|
| 650 |
+
id="arch_002", category=Category.ARCHITECTURE, severity=Severity.HIGH,
|
| 651 |
+
filename="handlers/events.py", line_number=15,
|
|
|
|
|
|
|
|
|
|
| 652 |
description="Synchronous HTTP call inside event handler blocks the event loop.",
|
| 653 |
keywords=["synchronous", "blocking", "event loop", "async", "non-blocking", "timeout"],
|
| 654 |
required_verdict=Verdict.REQUEST_CHANGES
|
|
|
|
| 658 |
),
|
| 659 |
Scenario(
|
| 660 |
task_id=TaskId.ARCHITECTURAL_REVIEW,
|
| 661 |
+
pr_title="billing-proxy: simplify billing call by removing retry wrapper",
|
| 662 |
pr_description="Call downstream billing service directly.",
|
| 663 |
+
service_name="billing-service",
|
| 664 |
+
affected_users=500000,
|
| 665 |
+
service_criticality="critical",
|
| 666 |
+
blast_radius="critical",
|
| 667 |
files_changed=[
|
| 668 |
FileChange(
|
| 669 |
filename="billing_proxy.py",
|
| 670 |
patch="""@@ -10,1 +10,1 @@
|
| 671 |
- return resiliency.call_with_retry(BILLING_URL)
|
| 672 |
+ return requests.post(BILLING_URL, data=payload)""",
|
| 673 |
+
additions=1, deletions=1
|
|
|
|
| 674 |
)
|
| 675 |
],
|
| 676 |
ground_truth_issues=[
|
| 677 |
GroundTruthIssue(
|
| 678 |
+
id="arch_003", category=Category.ARCHITECTURE, severity=Severity.MEDIUM,
|
| 679 |
+
filename="billing_proxy.py", line_number=10,
|
|
|
|
|
|
|
|
|
|
| 680 |
description="Missing retry logic and circuit breaker on external API call.",
|
| 681 |
keywords=["retry", "circuit breaker", "resilience", "idempotent", "backoff", "failure"],
|
| 682 |
required_verdict=Verdict.REQUEST_CHANGES
|
|
|
|
| 686 |
),
|
| 687 |
Scenario(
|
| 688 |
task_id=TaskId.ARCHITECTURAL_REVIEW,
|
| 689 |
+
pr_title="app-core: consolidate all managers into GlobalManager for simpler access",
|
| 690 |
pr_description="Consolidating all managers into one for easier access.",
|
| 691 |
+
service_name="core-application",
|
| 692 |
+
affected_users=200000,
|
| 693 |
+
service_criticality="high",
|
| 694 |
+
blast_radius="high",
|
| 695 |
files_changed=[
|
| 696 |
FileChange(
|
| 697 |
filename="app_core.py",
|
|
|
|
| 701 |
+ def handle_auth(self): pass
|
| 702 |
+ def handle_billing(self): pass
|
| 703 |
+ def handle_users(self): pass""",
|
| 704 |
+
additions=4, deletions=1
|
|
|
|
| 705 |
)
|
| 706 |
],
|
| 707 |
ground_truth_issues=[
|
| 708 |
GroundTruthIssue(
|
| 709 |
+
id="arch_004", category=Category.ARCHITECTURE, severity=Severity.MEDIUM,
|
| 710 |
+
filename="app_core.py", line_number=2,
|
|
|
|
|
|
|
|
|
|
| 711 |
description="God object pattern: one class handles unrelated domains (auth, billing, users).",
|
| 712 |
keywords=["single responsibility", "god object", "cohesion", "separation", "refactor"],
|
| 713 |
required_verdict=Verdict.REQUEST_CHANGES
|
|
|
|
| 717 |
),
|
| 718 |
Scenario(
|
| 719 |
task_id=TaskId.ARCHITECTURAL_REVIEW,
|
| 720 |
+
pr_title="audit-job: process each user individually for cleaner audit flow",
|
| 721 |
pr_description="Process audit for all users one by one.",
|
| 722 |
+
service_name="audit-job-runner",
|
| 723 |
+
affected_users=5000,
|
| 724 |
+
service_criticality="medium",
|
| 725 |
+
blast_radius="medium",
|
| 726 |
files_changed=[
|
| 727 |
FileChange(
|
| 728 |
filename="audit_job.py",
|
|
|
|
| 732 |
+ for u_id in user_ids:
|
| 733 |
+ user = User.objects.get(id=u_id)
|
| 734 |
+ process(user)""",
|
| 735 |
+
additions=2, deletions=2
|
|
|
|
| 736 |
)
|
| 737 |
],
|
| 738 |
ground_truth_issues=[
|
| 739 |
GroundTruthIssue(
|
| 740 |
+
id="arch_005", category=Category.ARCHITECTURE, severity=Severity.HIGH,
|
| 741 |
+
filename="audit_job.py", line_number=6,
|
|
|
|
|
|
|
|
|
|
| 742 |
description="N+1 query problem: fetching user objects inside a loop.",
|
| 743 |
keywords=["N+1", "query", "loop", "batch", "eager load", "select_related"],
|
| 744 |
required_verdict=Verdict.REQUEST_CHANGES
|
|
|
|
| 748 |
),
|
| 749 |
Scenario(
|
| 750 |
task_id=TaskId.ARCHITECTURAL_REVIEW,
|
| 751 |
+
pr_title="api-handler: simplify log endpoint by removing pagination",
|
| 752 |
pr_description="Simple endpoint to fetch current log state.",
|
| 753 |
+
service_name="log-api",
|
| 754 |
+
affected_users=1000,
|
| 755 |
+
service_criticality="medium",
|
| 756 |
+
blast_radius="high",
|
| 757 |
files_changed=[
|
| 758 |
FileChange(
|
| 759 |
filename="handlers/api.py",
|
| 760 |
patch="""@@ -20,1 +20,1 @@
|
| 761 |
-def get_logs(page, limit): return db.logs.all()[page*limit:(page+1)*limit]
|
| 762 |
+def get_logs(): return db.logs.all()""",
|
| 763 |
+
additions=1, deletions=1
|
|
|
|
| 764 |
)
|
| 765 |
],
|
| 766 |
ground_truth_issues=[
|
| 767 |
GroundTruthIssue(
|
| 768 |
+
id="arch_006", category=Category.ARCHITECTURE, severity=Severity.MEDIUM,
|
| 769 |
+
filename="handlers/api.py", line_number=20,
|
|
|
|
|
|
|
|
|
|
| 770 |
description="Missing pagination on endpoint. Can cause memory exhaustion on large datasets.",
|
| 771 |
keywords=["pagination", "limit", "offset", "memory", "unbounded", "cursor"],
|
| 772 |
required_verdict=Verdict.REQUEST_CHANGES
|
|
|
|
| 776 |
),
|
| 777 |
Scenario(
|
| 778 |
task_id=TaskId.ARCHITECTURAL_REVIEW,
|
| 779 |
+
pr_title="upload-service: switch to synchronous file save for reliability",
|
| 780 |
pr_description="Directly saving large file uploads to disk in request thread.",
|
| 781 |
+
service_name="file-upload-service",
|
| 782 |
+
affected_users=80000,
|
| 783 |
+
service_criticality="medium",
|
| 784 |
+
blast_radius="medium",
|
| 785 |
files_changed=[
|
| 786 |
FileChange(
|
| 787 |
filename="upload_service.py",
|
| 788 |
patch="""@@ -12,1 +12,1 @@
|
| 789 |
- await background_save(file)
|
| 790 |
+ file.save('/tmp/large_file')""",
|
| 791 |
+
additions=1, deletions=1
|
|
|
|
| 792 |
)
|
| 793 |
],
|
| 794 |
ground_truth_issues=[
|
| 795 |
GroundTruthIssue(
|
| 796 |
+
id="arch_007", category=Category.ARCHITECTURE, severity=Severity.MEDIUM,
|
| 797 |
+
filename="upload_service.py", line_number=13,
|
|
|
|
|
|
|
|
|
|
| 798 |
description="Synchronous file upload blocking the request thread. Use background tasks.",
|
| 799 |
keywords=["async", "upload", "background task", "streaming", "thread", "non-blocking"],
|
| 800 |
required_verdict=Verdict.REQUEST_CHANGES
|
|
|
|
| 804 |
),
|
| 805 |
Scenario(
|
| 806 |
task_id=TaskId.ARCHITECTURAL_REVIEW,
|
| 807 |
+
pr_title="checkout: apply payment by mutating user balance directly on request",
|
| 808 |
pr_description="Update balance directly on payment request.",
|
| 809 |
+
service_name="payment-service",
|
| 810 |
+
affected_users=1000000,
|
| 811 |
+
service_criticality="critical",
|
| 812 |
+
blast_radius="critical",
|
| 813 |
files_changed=[
|
| 814 |
FileChange(
|
| 815 |
filename="checkout.py",
|
| 816 |
patch="""@@ -8,1 +8,1 @@
|
| 817 |
- process_payment_with_idempotency(req)
|
| 818 |
+ user.balance -= req.amount""",
|
| 819 |
+
additions=1, deletions=1
|
|
|
|
| 820 |
)
|
| 821 |
],
|
| 822 |
ground_truth_issues=[
|
| 823 |
GroundTruthIssue(
|
| 824 |
+
id="arch_008", category=Category.ARCHITECTURE, severity=Severity.HIGH,
|
| 825 |
+
filename="checkout.py", line_number=8,
|
|
|
|
|
|
|
|
|
|
| 826 |
description="Missing idempotency key on payment mutation endpoint. Dangerous on retries.",
|
| 827 |
keywords=["idempotency", "duplicate", "payment", "retry", "key", "mutation"],
|
| 828 |
required_verdict=Verdict.REQUEST_CHANGES
|
|
|
|
| 832 |
),
|
| 833 |
Scenario(
|
| 834 |
task_id=TaskId.ARCHITECTURAL_REVIEW,
|
| 835 |
+
pr_title="service-b: speed up sync by writing directly to service-a DB table",
|
| 836 |
pr_description="Service B updates Service A's table directly for speed.",
|
| 837 |
+
service_name="microservice-b",
|
| 838 |
+
affected_users=150000,
|
| 839 |
+
service_criticality="high",
|
| 840 |
+
blast_radius="high",
|
| 841 |
files_changed=[
|
| 842 |
FileChange(
|
| 843 |
filename="service_b/sync.py",
|
| 844 |
patch="""@@ -22,1 +22,1 @@
|
| 845 |
- send_event_to_service_a(data)
|
| 846 |
+ db.execute('UPDATE service_a_table SET x = 1')""",
|
| 847 |
+
additions=1, deletions=1
|
|
|
|
| 848 |
)
|
| 849 |
],
|
| 850 |
ground_truth_issues=[
|
| 851 |
GroundTruthIssue(
|
| 852 |
+
id="arch_009", category=Category.ARCHITECTURE, severity=Severity.HIGH,
|
| 853 |
+
filename="service_b/sync.py", line_number=23,
|
|
|
|
|
|
|
|
|
|
| 854 |
description="Shared mutable state between microservices via direct DB write. Breaks encapsulation.",
|
| 855 |
keywords=["shared state", "microservice", "event", "eventual consistency", "ownership", "coupling"],
|
| 856 |
required_verdict=Verdict.REQUEST_CHANGES
|
|
|
|
| 860 |
),
|
| 861 |
Scenario(
|
| 862 |
task_id=TaskId.ARCHITECTURAL_REVIEW,
|
| 863 |
+
pr_title="finance-api: inline interest calculation in GET handler for speed",
|
| 864 |
pr_description="Complex interest calculation directly in the GET endpoint.",
|
| 865 |
+
service_name="finance-service",
|
| 866 |
+
affected_users=250000,
|
| 867 |
+
service_criticality="high",
|
| 868 |
+
blast_radius="high",
|
| 869 |
files_changed=[
|
| 870 |
FileChange(
|
| 871 |
filename="api/finance.py",
|
|
|
|
| 874 |
+ interest = u.balance * 0.05
|
| 875 |
+ if u.type == 'GOLD': interest += 10
|
| 876 |
+ return interest""",
|
| 877 |
+
additions=3, deletions=1
|
|
|
|
| 878 |
)
|
| 879 |
],
|
| 880 |
ground_truth_issues=[
|
| 881 |
GroundTruthIssue(
|
| 882 |
+
id="arch_010", category=Category.ARCHITECTURE, severity=Severity.MEDIUM,
|
| 883 |
+
filename="api/finance.py", line_number=16,
|
|
|
|
|
|
|
|
|
|
| 884 |
description="Clean architecture violation: domain logic leaked into HTTP handler.",
|
| 885 |
keywords=["clean architecture", "domain", "handler", "concern", "presentation", "business logic"],
|
| 886 |
required_verdict=Verdict.REQUEST_CHANGES
|
|
|
|
| 890 |
)
|
| 891 |
]
|
| 892 |
|
| 893 |
+
|
| 894 |
SCENARIOS = {
|
| 895 |
+
TaskId.BUG_DETECTION: BUG_SCENARIOS,
|
| 896 |
+
TaskId.SECURITY_AUDIT: SECURITY_SCENARIOS,
|
| 897 |
TaskId.ARCHITECTURAL_REVIEW: ARCH_SCENARIOS,
|
| 898 |
}
|
|
@@ -0,0 +1,309 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
OpenEnv Inference Script — AgentOrg CodeReview Environment
|
| 3 |
+
==========================================================
|
| 4 |
+
Required env vars:
|
| 5 |
+
API_BASE_URL — OpenAI-compatible base URL (e.g. https://api.openai.com/v1)
|
| 6 |
+
MODEL_NAME — Model identifier (e.g. gpt-4o, gpt-3.5-turbo)
|
| 7 |
+
HF_TOKEN — Hugging Face token (used as api_key for OpenAI client)
|
| 8 |
+
ENV_URL — CodeReview env URL (default: http://localhost:7860)
|
| 9 |
+
|
| 10 |
+
Output format (stdout, per OpenEnv spec):
|
| 11 |
+
[START] task=<task_id> env=<env_url> model=<model>
|
| 12 |
+
[STEP] step=<n> action=<str> reward=<float> done=<bool> error=<str|None>
|
| 13 |
+
[END] success=<bool> steps=<int> score=<float> rewards=<list>
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import os
|
| 17 |
+
import sys
|
| 18 |
+
import json
|
| 19 |
+
import requests
|
| 20 |
+
from openai import OpenAI
|
| 21 |
+
|
| 22 |
+
# ── Environment Variables (exact names required by OpenEnv spec) ──────────────
|
| 23 |
+
API_BASE_URL = os.environ.get("API_BASE_URL", "https://api.openai.com/v1")
|
| 24 |
+
MODEL_NAME = os.environ.get("MODEL_NAME", "gpt-3.5-turbo")
|
| 25 |
+
HF_TOKEN = os.environ.get("HF_TOKEN", "dummy")
|
| 26 |
+
ENV_URL = os.environ.get("ENV_URL", "http://localhost:7860")
|
| 27 |
+
|
| 28 |
+
# ── Config ────────────────────────────────────────────────────────────────────
|
| 29 |
+
TASKS = ["bug_detection", "security_audit", "architectural_review"]
|
| 30 |
+
MAX_STEPS = {"bug_detection": 10, "security_audit": 15, "architectural_review": 20}
|
| 31 |
+
SUCCESS_THRESHOLD = 0.5
|
| 32 |
+
SEEDS = [0, 1, 2] # Run each task on 3 seeds for robust baseline
|
| 33 |
+
|
| 34 |
+
# ── OpenAI client ─────────────────────────────────────────────────────────────
|
| 35 |
+
client = OpenAI(api_key=HF_TOKEN, base_url=API_BASE_URL)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# ── Structured log helpers (mandatory OpenEnv format) ─────────────────────────
|
| 39 |
+
def log_start(task: str, env: str, model: str):
|
| 40 |
+
print(f"[START] task={task} env={env} model={model}", flush=True)
|
| 41 |
+
|
| 42 |
+
def log_step(step: int, action: str, reward: float, done: bool, error):
|
| 43 |
+
print(
|
| 44 |
+
f"[STEP] step={step} action={action!r} reward={reward:.4f} "
|
| 45 |
+
f"done={done} error={error}",
|
| 46 |
+
flush=True
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
def log_end(success: bool, steps: int, score: float, rewards: list):
|
| 50 |
+
print(
|
| 51 |
+
f"[END] success={success} steps={steps} score={score:.4f} "
|
| 52 |
+
f"rewards={rewards}",
|
| 53 |
+
flush=True
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
# ── System prompt ─────────────────────────────────────────────────────────────
|
| 58 |
+
SYSTEM_PROMPT = """You are an expert code reviewer specializing in bugs, security vulnerabilities, and architectural issues.
|
| 59 |
+
|
| 60 |
+
You will be given a code diff (PR) to review. Your job is to identify issues and output a single JSON action.
|
| 61 |
+
|
| 62 |
+
Available action types:
|
| 63 |
+
- "flag_issue": Flag a specific issue in the code
|
| 64 |
+
- "approve": Approve the PR (no issues found / all issues flagged)
|
| 65 |
+
- "request_changes": Request changes (issues found that must be fixed)
|
| 66 |
+
- "ask_question": Ask a clarifying question
|
| 67 |
+
- "comment": Leave a general comment
|
| 68 |
+
|
| 69 |
+
For "flag_issue", you MUST provide:
|
| 70 |
+
- action_type: "flag_issue"
|
| 71 |
+
- body: description of the issue (be specific, mention the root cause)
|
| 72 |
+
- filename: the file containing the issue
|
| 73 |
+
- line_number: approximate line number
|
| 74 |
+
- severity: one of "low", "medium", "high", "critical"
|
| 75 |
+
- category: one of "bug", "security", "architecture", "performance", "style", "design"
|
| 76 |
+
|
| 77 |
+
For "approve" or "request_changes", you MUST provide:
|
| 78 |
+
- action_type: "approve" or "request_changes"
|
| 79 |
+
- body: your overall assessment
|
| 80 |
+
- verdict: "LGTM" (for approve) or "REQUEST_CHANGES" (for request_changes)
|
| 81 |
+
|
| 82 |
+
After flagging all issues you can find, submit an approve or request_changes action.
|
| 83 |
+
|
| 84 |
+
IMPORTANT: Output ONLY a valid JSON object — no markdown, no explanation.
|
| 85 |
+
"""
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def build_user_message(obs: dict, task_id: str, step: int) -> str:
|
| 89 |
+
"""Build the user message from the current observation."""
|
| 90 |
+
task_hints = {
|
| 91 |
+
"bug_detection": "Focus on: off-by-one errors, None dereferences, type mismatches, mutable defaults, race conditions, exception handling.",
|
| 92 |
+
"security_audit": "Focus on: SQL injection, XSS, hardcoded secrets, JWT issues, insecure deserialization, CORS, timing attacks, path traversal.",
|
| 93 |
+
"architectural_review": "Focus on: SRP violations, direct DB access from wrong layers, N+1 queries, missing retry/circuit-breaker, god objects, blocking I/O."
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
service_info = ""
|
| 97 |
+
if obs.get("service_criticality") or obs.get("blast_radius"):
|
| 98 |
+
service_info = f"""
|
| 99 |
+
Service Context:
|
| 100 |
+
- Service Criticality: {obs.get('service_criticality', 'unknown')}
|
| 101 |
+
- Blast Radius: {obs.get('blast_radius', 'unknown')}
|
| 102 |
+
- Affected Users: {obs.get('affected_users', 'unknown')}
|
| 103 |
+
"""
|
| 104 |
+
|
| 105 |
+
history_summary = ""
|
| 106 |
+
if obs.get("history"):
|
| 107 |
+
history_summary = f"\nPreviously flagged {len(obs['history'])} issue(s). Don't re-flag the same issue.\n"
|
| 108 |
+
|
| 109 |
+
return f"""PR Title: {obs.get('pr_title', 'N/A')}
|
| 110 |
+
PR Description: {obs.get('pr_description', 'N/A')}
|
| 111 |
+
Task: {task_id} (step {step}/{obs.get('max_steps', '?')})
|
| 112 |
+
Noise budget remaining: {obs.get('noise_budget', '?')} (false positives consume this){service_info}
|
| 113 |
+
Review focus: {task_hints.get(task_id, 'General code review')}
|
| 114 |
+
{history_summary}
|
| 115 |
+
Code diff:
|
| 116 |
+
```
|
| 117 |
+
{obs.get('diff', '(no diff available)')}
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
Output a single JSON action object. If you've already flagged the main issues, submit approve or request_changes."""
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def call_llm(messages: list) -> dict:
|
| 124 |
+
"""Call the LLM and parse its JSON response into an action dict."""
|
| 125 |
+
response = client.chat.completions.create(
|
| 126 |
+
model=MODEL_NAME,
|
| 127 |
+
messages=messages,
|
| 128 |
+
temperature=0.1,
|
| 129 |
+
max_tokens=512,
|
| 130 |
+
response_format={"type": "json_object"},
|
| 131 |
+
)
|
| 132 |
+
content = response.choices[0].message.content.strip()
|
| 133 |
+
return json.loads(content)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def sanitize_action(action_dict: dict, task_id: str) -> dict:
|
| 137 |
+
"""Ensure the action dict is valid and won't be rejected by the server."""
|
| 138 |
+
action_type = action_dict.get("action_type", "comment")
|
| 139 |
+
|
| 140 |
+
# Ensure category matches the task if flagging
|
| 141 |
+
if action_type == "flag_issue":
|
| 142 |
+
task_category_map = {
|
| 143 |
+
"bug_detection": "bug",
|
| 144 |
+
"security_audit": "security",
|
| 145 |
+
"architectural_review": "architecture",
|
| 146 |
+
}
|
| 147 |
+
if "category" not in action_dict or action_dict["category"] not in [
|
| 148 |
+
"bug", "security", "architecture", "performance", "style", "design"
|
| 149 |
+
]:
|
| 150 |
+
action_dict["category"] = task_category_map.get(task_id, "bug")
|
| 151 |
+
|
| 152 |
+
if "severity" not in action_dict or action_dict["severity"] not in [
|
| 153 |
+
"low", "medium", "high", "critical"
|
| 154 |
+
]:
|
| 155 |
+
action_dict["severity"] = "medium"
|
| 156 |
+
|
| 157 |
+
if "filename" not in action_dict or not action_dict["filename"]:
|
| 158 |
+
action_dict["filename"] = "unknown"
|
| 159 |
+
|
| 160 |
+
if "line_number" not in action_dict:
|
| 161 |
+
action_dict["line_number"] = 1
|
| 162 |
+
|
| 163 |
+
if "body" not in action_dict or not action_dict["body"]:
|
| 164 |
+
action_dict["body"] = "Issue detected"
|
| 165 |
+
|
| 166 |
+
elif action_type in ("approve", "request_changes"):
|
| 167 |
+
if action_type == "approve":
|
| 168 |
+
action_dict["verdict"] = "LGTM"
|
| 169 |
+
else:
|
| 170 |
+
action_dict["verdict"] = "REQUEST_CHANGES"
|
| 171 |
+
if "body" not in action_dict:
|
| 172 |
+
action_dict["body"] = "Review complete."
|
| 173 |
+
|
| 174 |
+
return action_dict
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def run_episode(task_id: str, seed: int) -> dict:
|
| 178 |
+
"""Run a single episode. Returns {score, steps, success, rewards}."""
|
| 179 |
+
log_start(task_id, ENV_URL, MODEL_NAME)
|
| 180 |
+
|
| 181 |
+
# ── Reset ──────────────────────────────────────────────────────────────
|
| 182 |
+
try:
|
| 183 |
+
reset_resp = requests.post(
|
| 184 |
+
f"{ENV_URL}/reset",
|
| 185 |
+
json={"task_id": task_id, "seed": seed},
|
| 186 |
+
timeout=10
|
| 187 |
+
)
|
| 188 |
+
reset_resp.raise_for_status()
|
| 189 |
+
except Exception as e:
|
| 190 |
+
log_end(False, 0, 0.0, [])
|
| 191 |
+
return {"score": 0.0, "steps": 0, "success": False, "rewards": [], "error": str(e)}
|
| 192 |
+
|
| 193 |
+
reset_data = reset_resp.json()
|
| 194 |
+
episode_id = reset_data["episode_id"]
|
| 195 |
+
obs = reset_data["result"]["observation"]
|
| 196 |
+
|
| 197 |
+
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
| 198 |
+
rewards = []
|
| 199 |
+
step = 0
|
| 200 |
+
done = False
|
| 201 |
+
max_s = MAX_STEPS.get(task_id, 15)
|
| 202 |
+
|
| 203 |
+
# ── Step loop ──────────────────────────────────────────────────────────
|
| 204 |
+
while not done and step < max_s:
|
| 205 |
+
step += 1
|
| 206 |
+
user_msg = build_user_message(obs, task_id, step)
|
| 207 |
+
messages.append({"role": "user", "content": user_msg})
|
| 208 |
+
|
| 209 |
+
error_msg = None
|
| 210 |
+
try:
|
| 211 |
+
action_dict = call_llm(messages)
|
| 212 |
+
action_dict = sanitize_action(action_dict, task_id)
|
| 213 |
+
|
| 214 |
+
step_resp = requests.post(
|
| 215 |
+
f"{ENV_URL}/step/{episode_id}",
|
| 216 |
+
json=action_dict,
|
| 217 |
+
timeout=15
|
| 218 |
+
)
|
| 219 |
+
step_resp.raise_for_status()
|
| 220 |
+
step_data = step_resp.json()
|
| 221 |
+
|
| 222 |
+
reward = step_data.get("reward", 0.0)
|
| 223 |
+
done = step_data.get("done", False)
|
| 224 |
+
obs = step_data.get("observation", obs)
|
| 225 |
+
|
| 226 |
+
rewards.append(round(reward, 4))
|
| 227 |
+
|
| 228 |
+
# Add assistant turn to conversation
|
| 229 |
+
messages.append({
|
| 230 |
+
"role": "assistant",
|
| 231 |
+
"content": json.dumps(action_dict)
|
| 232 |
+
})
|
| 233 |
+
|
| 234 |
+
except Exception as e:
|
| 235 |
+
error_msg = str(e)
|
| 236 |
+
reward = 0.0
|
| 237 |
+
done = True # Stop on unrecoverable error
|
| 238 |
+
|
| 239 |
+
log_step(step, action_dict.get("action_type", "unknown") if error_msg is None else "error",
|
| 240 |
+
reward, done, error_msg)
|
| 241 |
+
|
| 242 |
+
if error_msg:
|
| 243 |
+
break
|
| 244 |
+
|
| 245 |
+
# ── Get final result ───────────────────────────────────────────────────
|
| 246 |
+
try:
|
| 247 |
+
result_resp = requests.get(f"{ENV_URL}/result/{episode_id}", timeout=10)
|
| 248 |
+
result_resp.raise_for_status()
|
| 249 |
+
result_data = result_resp.json()
|
| 250 |
+
final_score = result_data.get("final_score", 0.0)
|
| 251 |
+
except Exception:
|
| 252 |
+
final_score = rewards[-1] if rewards else 0.0
|
| 253 |
+
|
| 254 |
+
success = final_score >= SUCCESS_THRESHOLD
|
| 255 |
+
log_end(success, step, final_score, rewards)
|
| 256 |
+
|
| 257 |
+
return {
|
| 258 |
+
"task_id": task_id,
|
| 259 |
+
"seed": seed,
|
| 260 |
+
"score": final_score,
|
| 261 |
+
"steps": step,
|
| 262 |
+
"success": success,
|
| 263 |
+
"rewards": rewards
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def main():
|
| 268 |
+
"""Run all tasks across multiple seeds and print a summary."""
|
| 269 |
+
print("=" * 60, flush=True)
|
| 270 |
+
print(f"AgentOrg CodeReview OpenEnv Baseline", flush=True)
|
| 271 |
+
print(f"Model: {MODEL_NAME}", flush=True)
|
| 272 |
+
print(f"EnvURL: {ENV_URL}", flush=True)
|
| 273 |
+
print("=" * 60, flush=True)
|
| 274 |
+
|
| 275 |
+
all_results = []
|
| 276 |
+
|
| 277 |
+
for task_id in TASKS:
|
| 278 |
+
task_scores = []
|
| 279 |
+
for seed in SEEDS:
|
| 280 |
+
print(f"\n--- Task: {task_id} | Seed: {seed} ---", flush=True)
|
| 281 |
+
result = run_episode(task_id, seed)
|
| 282 |
+
all_results.append(result)
|
| 283 |
+
task_scores.append(result["score"])
|
| 284 |
+
|
| 285 |
+
avg_score = sum(task_scores) / len(task_scores) if task_scores else 0.0
|
| 286 |
+
print(f"\n[SUMMARY] task={task_id} avg_score={avg_score:.4f} seeds={SEEDS}", flush=True)
|
| 287 |
+
|
| 288 |
+
# ── Overall baseline table ─────────────────────────────────────────────
|
| 289 |
+
print("\n" + "=" * 60, flush=True)
|
| 290 |
+
print("BASELINE RESULTS", flush=True)
|
| 291 |
+
print("=" * 60, flush=True)
|
| 292 |
+
print(f"{'Task':<30} {'Avg Score':>10} {'Success Rate':>14}", flush=True)
|
| 293 |
+
print("-" * 56, flush=True)
|
| 294 |
+
|
| 295 |
+
for task_id in TASKS:
|
| 296 |
+
task_results = [r for r in all_results if r["task_id"] == task_id]
|
| 297 |
+
avg = sum(r["score"] for r in task_results) / len(task_results)
|
| 298 |
+
succ = sum(1 for r in task_results if r["success"]) / len(task_results)
|
| 299 |
+
print(f"{task_id:<30} {avg:>10.4f} {succ*100:>13.1f}%", flush=True)
|
| 300 |
+
|
| 301 |
+
overall = sum(r["score"] for r in all_results) / len(all_results)
|
| 302 |
+
print("-" * 56, flush=True)
|
| 303 |
+
print(f"{'OVERALL':<30} {overall:>10.4f}", flush=True)
|
| 304 |
+
|
| 305 |
+
return 0
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
if __name__ == "__main__":
|
| 309 |
+
sys.exit(main())
|
|
@@ -1,24 +1,66 @@
|
|
| 1 |
version: "1.0"
|
| 2 |
name: "agentorg-codereview"
|
| 3 |
-
description:
|
|
|
|
|
|
|
|
|
|
| 4 |
entry_point: "app:app"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
tasks:
|
| 7 |
- id: "bug_detection"
|
|
|
|
| 8 |
max_steps: 10
|
| 9 |
scenarios: 10
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
- id: "security_audit"
|
|
|
|
| 11 |
max_steps: 15
|
| 12 |
scenarios: 10
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
- id: "architectural_review"
|
|
|
|
| 14 |
max_steps: 20
|
| 15 |
scenarios: 10
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
grading:
|
| 18 |
type: "deterministic"
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
version: "1.0"
|
| 2 |
name: "agentorg-codereview"
|
| 3 |
+
description: >
|
| 4 |
+
AI Senior Code Reviewer evaluation environment — trains agents to detect bugs,
|
| 5 |
+
security vulnerabilities, and architectural issues in realistic Python PRs.
|
| 6 |
+
Grounded in real-world incident patterns (payment systems, auth layers, microservices).
|
| 7 |
entry_point: "app:app"
|
| 8 |
+
base_url: "http://localhost:7860"
|
| 9 |
+
api_version: "v1"
|
| 10 |
+
|
| 11 |
+
tags:
|
| 12 |
+
- openenv
|
| 13 |
+
- code-review
|
| 14 |
+
- security
|
| 15 |
+
- software-engineering
|
| 16 |
|
| 17 |
tasks:
|
| 18 |
- id: "bug_detection"
|
| 19 |
+
difficulty: easy
|
| 20 |
max_steps: 10
|
| 21 |
scenarios: 10
|
| 22 |
+
description: >
|
| 23 |
+
Identify logical errors, off-by-one bugs, mutable default arguments,
|
| 24 |
+
None dereferences, race conditions, and type mismatches in Python snippets.
|
| 25 |
+
Agents must FLAG issues with correct category/severity and submit a final verdict.
|
| 26 |
+
|
| 27 |
- id: "security_audit"
|
| 28 |
+
difficulty: medium
|
| 29 |
max_steps: 15
|
| 30 |
scenarios: 10
|
| 31 |
+
description: >
|
| 32 |
+
Detect OWASP Top-10 vulnerabilities: SQL injection, XSS, hardcoded secrets,
|
| 33 |
+
JWT bypass, insecure deserialization (pickle RCE), path traversal, timing attacks,
|
| 34 |
+
CORS misconfiguration, and missing rate limits in a payment-adjacent Python codebase.
|
| 35 |
+
|
| 36 |
- id: "architectural_review"
|
| 37 |
+
difficulty: hard
|
| 38 |
max_steps: 20
|
| 39 |
scenarios: 10
|
| 40 |
+
description: >
|
| 41 |
+
Evaluate system design quality: SRP violations, direct DB access bypassing API layers,
|
| 42 |
+
synchronous blocking calls in event loops, missing idempotency keys on payment mutations,
|
| 43 |
+
N+1 query patterns, god object anti-patterns, and shared mutable state between microservices.
|
| 44 |
+
Requires reasoning about blast radius and service criticality in addition to code issues.
|
| 45 |
|
| 46 |
grading:
|
| 47 |
type: "deterministic"
|
| 48 |
+
score_range: [0.0, 1.0]
|
| 49 |
+
partial_credit: true
|
| 50 |
+
metrics:
|
| 51 |
+
- name: "recall"
|
| 52 |
+
description: "Fraction of ground-truth issues correctly identified"
|
| 53 |
+
weight: 0.7
|
| 54 |
+
- name: "precision"
|
| 55 |
+
description: "Fraction of flagged issues that are true positives"
|
| 56 |
+
weight: 0.3
|
| 57 |
+
- name: "severity_accuracy"
|
| 58 |
+
description: "How close the flagged severity is to ground truth (security task)"
|
| 59 |
+
weight: 0.7
|
| 60 |
+
- name: "keyword_accuracy"
|
| 61 |
+
description: "Semantic overlap between agent description and issue keywords"
|
| 62 |
+
weight: 0.3
|
| 63 |
+
- name: "verdict_correctness"
|
| 64 |
+
description: "Whether final approve/request_changes decision is correct (arch task)"
|
| 65 |
+
weight: 0.2
|
| 66 |
+
|
|
@@ -5,3 +5,4 @@ pytest==8.0.0
|
|
| 5 |
requests==2.31.0
|
| 6 |
websockets==12.0
|
| 7 |
httpx
|
|
|
|
|
|
| 5 |
requests==2.31.0
|
| 6 |
websockets==12.0
|
| 7 |
httpx
|
| 8 |
+
openai>=1.0.0
|
|
@@ -1,6 +1,13 @@
|
|
| 1 |
import pytest
|
| 2 |
from codereview_env.env import CodeReviewEnv
|
| 3 |
-
from codereview_env.models import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
def test_env_reset():
|
| 6 |
env = CodeReviewEnv()
|
|
@@ -10,15 +17,30 @@ def test_env_reset():
|
|
| 10 |
assert res.observation.step_count == 0
|
| 11 |
assert res.observation.noise_budget == 5
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
def test_env_step_bug_detection():
|
| 14 |
env = CodeReviewEnv()
|
| 15 |
-
|
| 16 |
-
#
|
| 17 |
-
|
| 18 |
-
# Flag the bug correctly
|
| 19 |
action = Action(
|
| 20 |
action_type=ActionType.FLAG_ISSUE,
|
| 21 |
-
body="None dereference null check guard clause",
|
| 22 |
filename="auth.py",
|
| 23 |
line_number=16,
|
| 24 |
category=Category.BUG,
|
|
@@ -26,27 +48,68 @@ def test_env_step_bug_detection():
|
|
| 26 |
)
|
| 27 |
step_res = env.step(action)
|
| 28 |
assert step_res.observation.step_count == 1
|
| 29 |
-
assert step_res.reward > 0
|
| 30 |
assert step_res.done == False
|
| 31 |
|
| 32 |
# Terminal action
|
| 33 |
-
|
| 34 |
action_type=ActionType.APPROVE,
|
| 35 |
body="LGTM",
|
| 36 |
verdict=Verdict.LGTM
|
| 37 |
-
)
|
| 38 |
-
step_term = env.step(action_term)
|
| 39 |
assert step_term.done == True
|
| 40 |
-
|
| 41 |
final = env.get_final_result()
|
| 42 |
assert final.final_score > 0
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
def test_env_noise_budget_exhaustion():
|
| 45 |
env = CodeReviewEnv()
|
| 46 |
env.reset(TaskId.BUG_DETECTION, seed=0)
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
action_fp = Action(
|
| 50 |
action_type=ActionType.FLAG_ISSUE,
|
| 51 |
body="fp",
|
| 52 |
filename="nonexistent",
|
|
@@ -54,29 +117,125 @@ def test_env_noise_budget_exhaustion():
|
|
| 54 |
category=Category.BUG,
|
| 55 |
severity=Severity.LOW
|
| 56 |
)
|
| 57 |
-
|
| 58 |
for i in range(4):
|
| 59 |
-
res = env.step(
|
| 60 |
assert res.done == False
|
| 61 |
assert res.observation.noise_budget == 5 - (i + 1)
|
| 62 |
-
|
| 63 |
-
res_final = env.step(
|
| 64 |
assert res_final.done == True
|
| 65 |
assert res_final.observation.noise_budget == 0
|
| 66 |
|
|
|
|
| 67 |
def test_env_max_steps():
|
| 68 |
env = CodeReviewEnv()
|
| 69 |
env.reset(TaskId.BUG_DETECTION, seed=0)
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
action_type=ActionType.ASK_QUESTION,
|
| 73 |
-
body="what's this?"
|
| 74 |
-
)
|
| 75 |
-
|
| 76 |
for i in range(9):
|
| 77 |
-
res = env.step(
|
| 78 |
assert res.done == False
|
| 79 |
-
|
| 80 |
-
res_final = env.step(
|
| 81 |
assert res_final.done == True
|
| 82 |
assert res_final.observation.step_count == 10
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import pytest
|
| 2 |
from codereview_env.env import CodeReviewEnv
|
| 3 |
+
from codereview_env.models import (
|
| 4 |
+
TaskId, Action, ActionType, Category, Severity, Verdict, StateResult
|
| 5 |
+
)
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 9 |
+
# Reset tests
|
| 10 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 11 |
|
| 12 |
def test_env_reset():
|
| 13 |
env = CodeReviewEnv()
|
|
|
|
| 17 |
assert res.observation.step_count == 0
|
| 18 |
assert res.observation.noise_budget == 5
|
| 19 |
|
| 20 |
+
|
| 21 |
+
def test_env_reset_populates_blast_radius():
|
| 22 |
+
"""Observation should carry blast-radius metadata from the scenario."""
|
| 23 |
+
env = CodeReviewEnv()
|
| 24 |
+
res = env.reset(TaskId.SECURITY_AUDIT, seed=0)
|
| 25 |
+
obs = res.observation
|
| 26 |
+
assert obs.blast_radius in ("low", "medium", "high", "critical")
|
| 27 |
+
assert obs.service_criticality in ("low", "medium", "high", "critical")
|
| 28 |
+
assert isinstance(obs.affected_users, int)
|
| 29 |
+
assert obs.service_name != ""
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 33 |
+
# Step tests
|
| 34 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 35 |
+
|
| 36 |
def test_env_step_bug_detection():
|
| 37 |
env = CodeReviewEnv()
|
| 38 |
+
env.reset(TaskId.BUG_DETECTION, seed=1)
|
| 39 |
+
# seed=1 → bug_003: None dereference in auth.py
|
| 40 |
+
|
|
|
|
| 41 |
action = Action(
|
| 42 |
action_type=ActionType.FLAG_ISSUE,
|
| 43 |
+
body="None dereference null check guard clause AttributeError",
|
| 44 |
filename="auth.py",
|
| 45 |
line_number=16,
|
| 46 |
category=Category.BUG,
|
|
|
|
| 48 |
)
|
| 49 |
step_res = env.step(action)
|
| 50 |
assert step_res.observation.step_count == 1
|
| 51 |
+
assert step_res.reward > 0, "Correct issue flag should give positive reward delta"
|
| 52 |
assert step_res.done == False
|
| 53 |
|
| 54 |
# Terminal action
|
| 55 |
+
step_term = env.step(Action(
|
| 56 |
action_type=ActionType.APPROVE,
|
| 57 |
body="LGTM",
|
| 58 |
verdict=Verdict.LGTM
|
| 59 |
+
))
|
|
|
|
| 60 |
assert step_term.done == True
|
| 61 |
+
|
| 62 |
final = env.get_final_result()
|
| 63 |
assert final.final_score > 0
|
| 64 |
|
| 65 |
+
|
| 66 |
+
def test_env_step_reward_is_incremental_not_cumulative():
|
| 67 |
+
"""Each step reward should be a delta (positive or zero or penalty), not a running total."""
|
| 68 |
+
env = CodeReviewEnv()
|
| 69 |
+
# seed=1 selects bug_003: None dereference in auth.py at line 16
|
| 70 |
+
env.reset(TaskId.BUG_DETECTION, seed=1)
|
| 71 |
+
|
| 72 |
+
correct_action = Action(
|
| 73 |
+
action_type=ActionType.FLAG_ISSUE,
|
| 74 |
+
body="None dereference null check guard clause AttributeError",
|
| 75 |
+
filename="auth.py",
|
| 76 |
+
line_number=16,
|
| 77 |
+
category=Category.BUG,
|
| 78 |
+
severity=Severity.HIGH
|
| 79 |
+
)
|
| 80 |
+
step1 = env.step(correct_action)
|
| 81 |
+
# First correct flag → positive incremental delta
|
| 82 |
+
assert step1.reward > 0, f"Correct issue flag should give positive reward delta, got {step1.reward}"
|
| 83 |
+
|
| 84 |
+
# Second identical flag on same file/line — already matched, counts as FP
|
| 85 |
+
step2 = env.step(correct_action)
|
| 86 |
+
# Already matched → false positive → -0.05 penalty
|
| 87 |
+
assert step2.reward == -0.05
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def test_env_step_false_positive_penalty():
|
| 91 |
+
"""False positives should decrement noise_budget and return negative reward."""
|
| 92 |
+
env = CodeReviewEnv()
|
| 93 |
+
env.reset(TaskId.BUG_DETECTION, seed=0)
|
| 94 |
+
|
| 95 |
+
fp_action = Action(
|
| 96 |
+
action_type=ActionType.FLAG_ISSUE,
|
| 97 |
+
body="completely wrong flag",
|
| 98 |
+
filename="nonexistent_file.py",
|
| 99 |
+
line_number=999,
|
| 100 |
+
category=Category.BUG,
|
| 101 |
+
severity=Severity.LOW
|
| 102 |
+
)
|
| 103 |
+
step_res = env.step(fp_action)
|
| 104 |
+
assert step_res.reward == -0.05
|
| 105 |
+
assert step_res.observation.noise_budget == 4
|
| 106 |
+
|
| 107 |
+
|
| 108 |
def test_env_noise_budget_exhaustion():
|
| 109 |
env = CodeReviewEnv()
|
| 110 |
env.reset(TaskId.BUG_DETECTION, seed=0)
|
| 111 |
+
|
| 112 |
+
fp_action = Action(
|
|
|
|
| 113 |
action_type=ActionType.FLAG_ISSUE,
|
| 114 |
body="fp",
|
| 115 |
filename="nonexistent",
|
|
|
|
| 117 |
category=Category.BUG,
|
| 118 |
severity=Severity.LOW
|
| 119 |
)
|
| 120 |
+
|
| 121 |
for i in range(4):
|
| 122 |
+
res = env.step(fp_action)
|
| 123 |
assert res.done == False
|
| 124 |
assert res.observation.noise_budget == 5 - (i + 1)
|
| 125 |
+
|
| 126 |
+
res_final = env.step(fp_action)
|
| 127 |
assert res_final.done == True
|
| 128 |
assert res_final.observation.noise_budget == 0
|
| 129 |
|
| 130 |
+
|
| 131 |
def test_env_max_steps():
|
| 132 |
env = CodeReviewEnv()
|
| 133 |
env.reset(TaskId.BUG_DETECTION, seed=0)
|
| 134 |
+
|
| 135 |
+
action = Action(action_type=ActionType.ASK_QUESTION, body="what's this?")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
for i in range(9):
|
| 137 |
+
res = env.step(action)
|
| 138 |
assert res.done == False
|
| 139 |
+
|
| 140 |
+
res_final = env.step(action)
|
| 141 |
assert res_final.done == True
|
| 142 |
assert res_final.observation.step_count == 10
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 146 |
+
# get_state() tests — required by OpenEnv /state endpoint
|
| 147 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 148 |
+
|
| 149 |
+
def test_get_state_returns_state_result():
|
| 150 |
+
env = CodeReviewEnv()
|
| 151 |
+
env.reset(TaskId.BUG_DETECTION, seed=0)
|
| 152 |
+
|
| 153 |
+
state = env.get_state("test-episode-id")
|
| 154 |
+
assert isinstance(state, StateResult)
|
| 155 |
+
assert state.episode_id == "test-episode-id"
|
| 156 |
+
assert state.task_id == TaskId.BUG_DETECTION
|
| 157 |
+
assert state.step == 0
|
| 158 |
+
assert state.max_steps == 10
|
| 159 |
+
assert state.noise_budget == 5
|
| 160 |
+
assert state.cumulative_score == 0.0
|
| 161 |
+
assert state.done == False
|
| 162 |
+
assert state.issues_found == []
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def test_get_state_updates_after_step():
|
| 166 |
+
env = CodeReviewEnv()
|
| 167 |
+
env.reset(TaskId.BUG_DETECTION, seed=1)
|
| 168 |
+
|
| 169 |
+
action = Action(
|
| 170 |
+
action_type=ActionType.FLAG_ISSUE,
|
| 171 |
+
body="None dereference null check guard clause",
|
| 172 |
+
filename="auth.py",
|
| 173 |
+
line_number=16,
|
| 174 |
+
category=Category.BUG,
|
| 175 |
+
severity=Severity.HIGH
|
| 176 |
+
)
|
| 177 |
+
env.step(action)
|
| 178 |
+
|
| 179 |
+
state = env.get_state("ep-123")
|
| 180 |
+
assert state.step == 1
|
| 181 |
+
assert state.cumulative_score > 0
|
| 182 |
+
assert len(state.issues_found) > 0
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def test_get_state_before_reset_raises():
|
| 186 |
+
env = CodeReviewEnv()
|
| 187 |
+
with pytest.raises(RuntimeError):
|
| 188 |
+
env.get_state("no-episode")
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 192 |
+
# Multi-task smoke tests
|
| 193 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 194 |
+
|
| 195 |
+
def test_security_task_runs_to_completion():
|
| 196 |
+
env = CodeReviewEnv()
|
| 197 |
+
# seed=1 selects sec_003: JWT verification disabled in tokens.py
|
| 198 |
+
env.reset(TaskId.SECURITY_AUDIT, seed=1)
|
| 199 |
+
|
| 200 |
+
action = Action(
|
| 201 |
+
action_type=ActionType.FLAG_ISSUE,
|
| 202 |
+
body="JWT decoded without signature verification bypass authentication none algorithm",
|
| 203 |
+
filename="tokens.py",
|
| 204 |
+
line_number=10,
|
| 205 |
+
category=Category.SECURITY,
|
| 206 |
+
severity=Severity.CRITICAL
|
| 207 |
+
)
|
| 208 |
+
step_res = env.step(action)
|
| 209 |
+
assert step_res.reward >= 0, f"Correct security flag should give non-negative reward, got {step_res.reward}"
|
| 210 |
+
|
| 211 |
+
env.step(Action(
|
| 212 |
+
action_type=ActionType.REQUEST_CHANGES,
|
| 213 |
+
body="JWT verification must never be disabled. Must be fixed before merge.",
|
| 214 |
+
verdict=Verdict.REQUEST_CHANGES
|
| 215 |
+
))
|
| 216 |
+
final = env.get_final_result()
|
| 217 |
+
assert final.final_score > 0
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def test_arch_task_runs_to_completion():
|
| 221 |
+
env = CodeReviewEnv()
|
| 222 |
+
env.reset(TaskId.ARCHITECTURAL_REVIEW, seed=0)
|
| 223 |
+
|
| 224 |
+
action = Action(
|
| 225 |
+
action_type=ActionType.FLAG_ISSUE,
|
| 226 |
+
body="Direct DB access from dashboard bypasses API layer separation of concerns architectural violation",
|
| 227 |
+
filename="services/dashboard.py",
|
| 228 |
+
line_number=5,
|
| 229 |
+
category=Category.ARCHITECTURE,
|
| 230 |
+
severity=Severity.CRITICAL
|
| 231 |
+
)
|
| 232 |
+
env.step(action)
|
| 233 |
+
|
| 234 |
+
env.step(Action(
|
| 235 |
+
action_type=ActionType.REQUEST_CHANGES,
|
| 236 |
+
body="Must go through API layer.",
|
| 237 |
+
verdict=Verdict.REQUEST_CHANGES
|
| 238 |
+
))
|
| 239 |
+
final = env.get_final_result()
|
| 240 |
+
assert final.final_score > 0
|
| 241 |
+
assert final.verdict_correct == True
|