Spaces:
Sleeping
Sleeping
Commit ·
3583511
0
Parent(s):
first commit
Browse files- Dockerfile +19 -0
- README.md +148 -0
- app.py +64 -0
- environment.py +309 -0
- inference.py +178 -0
- models.py +45 -0
- openenv.yaml +77 -0
- requirements.txt +5 -0
Dockerfile
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install dependencies first (layer cache)
|
| 6 |
+
COPY requirements.txt .
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
# Copy application code
|
| 10 |
+
COPY server/ ./server/
|
| 11 |
+
|
| 12 |
+
# Environment defaults
|
| 13 |
+
ENV PORT=7860
|
| 14 |
+
ENV PYTHONPATH=/app
|
| 15 |
+
ENV ENABLE_WEB_INTERFACE=false
|
| 16 |
+
|
| 17 |
+
EXPOSE 7860
|
| 18 |
+
|
| 19 |
+
CMD ["python", "-m", "uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Code Security Review — OpenEnv
|
| 2 |
+
|
| 3 |
+
> An RL environment for training AI agents to detect bugs and security
|
| 4 |
+
> vulnerabilities in Python code.
|
| 5 |
+
|
| 6 |
+
## Motivation
|
| 7 |
+
|
| 8 |
+
Code review is one of the highest-leverage tasks in software engineering, yet it
|
| 9 |
+
remains bottlenecked on human attention. This environment trains agents to catch
|
| 10 |
+
real bug categories — from simple off-by-one errors to critical SQL injection
|
| 11 |
+
vulnerabilities — using structured, deterministic reward signals.
|
| 12 |
+
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
## Action Space
|
| 16 |
+
|
| 17 |
+
| Field | Type | Description |
|
| 18 |
+
|---|---|---|
|
| 19 |
+
| `bug_identified` | bool | Whether a bug was found |
|
| 20 |
+
| `bug_location` | string | Exact location (function, expression) |
|
| 21 |
+
| `bug_type` | string | `off-by-one`, `logic-error`, `security-vulnerability`, `none` |
|
| 22 |
+
| `bug_description` | string | Explanation of the bug and its impact |
|
| 23 |
+
| `severity` | string | `none` / `low` / `medium` / `high` / `critical` |
|
| 24 |
+
| `suggested_fix` | string | Corrected code or fix description |
|
| 25 |
+
|
| 26 |
+
## Observation Space
|
| 27 |
+
|
| 28 |
+
| Field | Type | Description |
|
| 29 |
+
|---|---|---|
|
| 30 |
+
| `code_snippet` | string | The code to review |
|
| 31 |
+
| `language` | string | Programming language |
|
| 32 |
+
| `task_description` | string | What the code is supposed to do |
|
| 33 |
+
| `task_id` | string | Unique task identifier |
|
| 34 |
+
| `difficulty` | string | `easy` / `medium` / `hard` |
|
| 35 |
+
| `step_number` | int | Current step within the episode |
|
| 36 |
+
| `max_steps` | int | Maximum steps allowed (3) |
|
| 37 |
+
| `previous_feedback` | string? | Feedback from prior step |
|
| 38 |
+
|
| 39 |
+
---
|
| 40 |
+
|
| 41 |
+
## Tasks
|
| 42 |
+
|
| 43 |
+
### Easy — Off-by-one in array traversal
|
| 44 |
+
- **Code:** `sum_elements(arr)` iterates `range(1, len(arr)+1)` causing `IndexError`
|
| 45 |
+
- **Expected bug type:** `off-by-one`
|
| 46 |
+
- **Expected severity:** `high`
|
| 47 |
+
- **Baseline score:** ~0.72
|
| 48 |
+
|
| 49 |
+
### Medium — Authentication logic flaw
|
| 50 |
+
- **Code:** `authenticate_user()` uses `or` instead of `and` for admin check
|
| 51 |
+
- **Expected bug type:** `logic-error`
|
| 52 |
+
- **Expected severity:** `critical`
|
| 53 |
+
- **Baseline score:** ~0.60
|
| 54 |
+
|
| 55 |
+
### Hard — SQL injection via f-string
|
| 56 |
+
- **Code:** `fetch_records()` interpolates `user_id` and `sort_column` directly into SQL
|
| 57 |
+
- **Expected bug type:** `security-vulnerability`
|
| 58 |
+
- **Expected severity:** `critical`
|
| 59 |
+
- **Baseline score:** ~0.55
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
## Reward Function
|
| 64 |
+
|
| 65 |
+
Rewards are deterministic and provide partial progress signal:
|
| 66 |
+
|
| 67 |
+
| Component | Max Score | Description |
|
| 68 |
+
|---|---|---|
|
| 69 |
+
| Bug identified | 0.20 | Correctly flags presence/absence of bug |
|
| 70 |
+
| Bug type | 0.20 | Correct category of bug |
|
| 71 |
+
| Bug location | 0.10 | Precise location identified |
|
| 72 |
+
| Description quality | 0.25 | Keyword density in explanation |
|
| 73 |
+
| Fix quality | 0.15 | Correct fix keywords present |
|
| 74 |
+
| Severity | 0.10 | Correct severity level |
|
| 75 |
+
| **Total** | **1.00** | |
|
| 76 |
+
|
| 77 |
+
---
|
| 78 |
+
|
| 79 |
+
## Setup
|
| 80 |
+
|
| 81 |
+
### 1. Build and run Docker
|
| 82 |
+
|
| 83 |
+
```bash
|
| 84 |
+
docker build -t code-review-env .
|
| 85 |
+
docker run -p 7860:7860 code-review-env
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
### 2. Run inference baseline
|
| 89 |
+
|
| 90 |
+
```bash
|
| 91 |
+
# Set your environment variables
|
| 92 |
+
export HF_TOKEN=hf_your_token_here
|
| 93 |
+
export MODEL_NAME=Qwen/Qwen2.5-72B-Instruct
|
| 94 |
+
export API_BASE_URL=https://router.huggingface.co/v1
|
| 95 |
+
export ENV_BASE_URL=http://localhost:7860
|
| 96 |
+
|
| 97 |
+
# Install dependencies
|
| 98 |
+
pip install -r requirements.txt
|
| 99 |
+
|
| 100 |
+
# Run
|
| 101 |
+
python inference.py
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
### 3. Validate (OpenEnv CLI)
|
| 105 |
+
|
| 106 |
+
```bash
|
| 107 |
+
openenv validate
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
---
|
| 111 |
+
|
| 112 |
+
## API Endpoints
|
| 113 |
+
|
| 114 |
+
| Method | Path | Description |
|
| 115 |
+
|---|---|---|
|
| 116 |
+
| GET | `/health` | Health check |
|
| 117 |
+
| POST | `/reset?difficulty=easy` | Reset environment |
|
| 118 |
+
| POST | `/step` | Submit a review action |
|
| 119 |
+
| GET | `/state` | Current episode state |
|
| 120 |
+
|
| 121 |
+
---
|
| 122 |
+
|
| 123 |
+
## Baseline Scores
|
| 124 |
+
|
| 125 |
+
| Task | Difficulty | Reward |
|
| 126 |
+
|---|---|---|
|
| 127 |
+
| Off-by-one detection | Easy | ~0.72 |
|
| 128 |
+
| Auth logic flaw | Medium | ~0.60 |
|
| 129 |
+
| SQL injection | Hard | ~0.55 |
|
| 130 |
+
| **Average** | | **~0.62** |
|
| 131 |
+
|
| 132 |
+
---
|
| 133 |
+
|
| 134 |
+
## Project Structure
|
| 135 |
+
|
| 136 |
+
```
|
| 137 |
+
code-review-env/
|
| 138 |
+
├── Dockerfile
|
| 139 |
+
├── openenv.yaml
|
| 140 |
+
├── requirements.txt
|
| 141 |
+
├── inference.py
|
| 142 |
+
├── README.md
|
| 143 |
+
└── server/
|
| 144 |
+
├── __init__.py
|
| 145 |
+
├── app.py # FastAPI endpoints
|
| 146 |
+
├── environment.py # Tasks + grader logic
|
| 147 |
+
└── models.py # Pydantic action/observation/state
|
| 148 |
+
```
|
app.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import uvicorn
|
| 3 |
+
from fastapi import FastAPI, HTTPException, Query
|
| 4 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 5 |
+
|
| 6 |
+
from .models import CodeReviewAction, CodeReviewState, StepResponse, ResetResponse
|
| 7 |
+
from .environment import CodeReviewEnvironment
|
| 8 |
+
|
| 9 |
+
app = FastAPI(
|
| 10 |
+
title="Code Security Review — OpenEnv",
|
| 11 |
+
description=(
|
| 12 |
+
"RL environment for training AI agents to detect bugs and security "
|
| 13 |
+
"vulnerabilities in code. Compatible with the OpenEnv spec."
|
| 14 |
+
),
|
| 15 |
+
version="1.0.0",
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
app.add_middleware(
|
| 19 |
+
CORSMiddleware,
|
| 20 |
+
allow_origins=["*"],
|
| 21 |
+
allow_methods=["*"],
|
| 22 |
+
allow_headers=["*"],
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
env = CodeReviewEnvironment()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@app.get("/health")
|
| 29 |
+
def health():
|
| 30 |
+
return {"status": "ok", "env": "code-review-env", "version": "1.0.0"}
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@app.post("/reset", response_model=ResetResponse)
|
| 34 |
+
def reset(difficulty: str = Query(default="easy", description="easy | medium | hard")):
|
| 35 |
+
"""Reset the environment and return the first observation."""
|
| 36 |
+
obs = env.reset(difficulty=difficulty)
|
| 37 |
+
return ResetResponse(observation=obs)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@app.post("/step", response_model=StepResponse)
|
| 41 |
+
def step(action: CodeReviewAction):
|
| 42 |
+
"""Submit a code review action and receive a reward signal."""
|
| 43 |
+
try:
|
| 44 |
+
obs, reward, done, info = env.step(action)
|
| 45 |
+
return StepResponse(observation=obs, reward=reward, done=done, info=info)
|
| 46 |
+
except ValueError as exc:
|
| 47 |
+
raise HTTPException(status_code=400, detail=str(exc))
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
@app.get("/state", response_model=CodeReviewState)
|
| 51 |
+
def state():
|
| 52 |
+
"""Return the current environment state."""
|
| 53 |
+
return env.state()
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
if __name__ == "__main__":
|
| 57 |
+
port = int(os.environ.get("PORT", 7860))
|
| 58 |
+
enable_web = os.environ.get("ENABLE_WEB_INTERFACE", "false").lower() == "true"
|
| 59 |
+
uvicorn.run(
|
| 60 |
+
"server.app:app",
|
| 61 |
+
host="0.0.0.0",
|
| 62 |
+
port=port,
|
| 63 |
+
reload=False,
|
| 64 |
+
)
|
environment.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, Any, Tuple, Optional
|
| 2 |
+
from .models import CodeReviewAction, CodeReviewObservation, CodeReviewState
|
| 3 |
+
|
| 4 |
+
MAX_STEPS = 3
|
| 5 |
+
|
| 6 |
+
# ──────────────────────────────────────────────
|
| 7 |
+
# TASK DEFINITIONS
|
| 8 |
+
# ──────────────────────────────────────────────
|
| 9 |
+
|
| 10 |
+
TASKS: Dict[str, dict] = {
|
| 11 |
+
|
| 12 |
+
# ── EASY ─────────────────────────────────
|
| 13 |
+
"easy": {
|
| 14 |
+
"id": "task_easy_001",
|
| 15 |
+
"difficulty": "easy",
|
| 16 |
+
"language": "python",
|
| 17 |
+
"description": (
|
| 18 |
+
"This function is supposed to sum all elements in a list. "
|
| 19 |
+
"Find any bugs and suggest a fix."
|
| 20 |
+
),
|
| 21 |
+
"code": (
|
| 22 |
+
"def sum_elements(arr):\n"
|
| 23 |
+
' """Return the sum of all elements."""\n'
|
| 24 |
+
" total = 0\n"
|
| 25 |
+
" for i in range(1, len(arr) + 1): # iterates over indices\n"
|
| 26 |
+
" total += arr[i]\n"
|
| 27 |
+
" return total"
|
| 28 |
+
),
|
| 29 |
+
"ground_truth": {
|
| 30 |
+
"bug_identified": True,
|
| 31 |
+
"bug_type_keywords": [
|
| 32 |
+
"off-by-one", "off by one", "index error", "indexerror",
|
| 33 |
+
"out of bounds", "out of range", "index out",
|
| 34 |
+
],
|
| 35 |
+
"location_keywords": [
|
| 36 |
+
"range(1, len(arr) + 1)", "len(arr) + 1", "len(arr)+1",
|
| 37 |
+
"range", "loop", "index", "arr[i]",
|
| 38 |
+
],
|
| 39 |
+
"description_keywords": [
|
| 40 |
+
"index", "range", "len", "off-by-one", "off by one",
|
| 41 |
+
"IndexError", "out of bounds", "+1", "exceed", "arr[i]",
|
| 42 |
+
"zero", "start",
|
| 43 |
+
],
|
| 44 |
+
"fix_keywords": [
|
| 45 |
+
"range(len(arr))", "range(0, len(arr))",
|
| 46 |
+
"for i in range(len", "for element in arr",
|
| 47 |
+
"arr[i]" , "len(arr))",
|
| 48 |
+
],
|
| 49 |
+
"severity_valid": ["high", "medium"],
|
| 50 |
+
},
|
| 51 |
+
},
|
| 52 |
+
|
| 53 |
+
# ── MEDIUM ────────────────────────────────
|
| 54 |
+
"medium": {
|
| 55 |
+
"id": "task_medium_001",
|
| 56 |
+
"difficulty": "medium",
|
| 57 |
+
"language": "python",
|
| 58 |
+
"description": (
|
| 59 |
+
"This authentication function controls admin access. "
|
| 60 |
+
"Find the logical security bug."
|
| 61 |
+
),
|
| 62 |
+
"code": (
|
| 63 |
+
"def authenticate_user(username, password, request_admin=False):\n"
|
| 64 |
+
' """Authenticate user and return access level."""\n'
|
| 65 |
+
" user = db.find_user(username)\n"
|
| 66 |
+
" if not user or user.password_hash != hash_password(password):\n"
|
| 67 |
+
' return {"authenticated": False, "level": "none"}\n'
|
| 68 |
+
"\n"
|
| 69 |
+
" # Elevate to admin if caller requests it OR user has admin role\n"
|
| 70 |
+
" if request_admin or user.role == 'admin': # <-- review this\n"
|
| 71 |
+
' return {"authenticated": True, "level": "admin"}\n'
|
| 72 |
+
"\n"
|
| 73 |
+
' return {"authenticated": True, "level": "user"}'
|
| 74 |
+
),
|
| 75 |
+
"ground_truth": {
|
| 76 |
+
"bug_identified": True,
|
| 77 |
+
"bug_type_keywords": [
|
| 78 |
+
"logic", "logic error", "logical", "privilege escalation",
|
| 79 |
+
"authorization", "authentication bypass", "access control",
|
| 80 |
+
],
|
| 81 |
+
"location_keywords": [
|
| 82 |
+
"request_admin or", "or user.role", "or", "condition",
|
| 83 |
+
"if request_admin", "or user.role == 'admin'",
|
| 84 |
+
],
|
| 85 |
+
"description_keywords": [
|
| 86 |
+
"or", "and", "privilege", "escalation", "bypass", "admin",
|
| 87 |
+
"role", "caller", "request_admin", "logic", "elevation",
|
| 88 |
+
"any caller", "arbitrary",
|
| 89 |
+
],
|
| 90 |
+
"fix_keywords": [
|
| 91 |
+
"and", "request_admin and user.role", "and user.role == 'admin'",
|
| 92 |
+
"and user.role", "both",
|
| 93 |
+
],
|
| 94 |
+
"severity_valid": ["critical", "high"],
|
| 95 |
+
},
|
| 96 |
+
},
|
| 97 |
+
|
| 98 |
+
# ── HARD ──────────────────────────────────
|
| 99 |
+
"hard": {
|
| 100 |
+
"id": "task_hard_001",
|
| 101 |
+
"difficulty": "hard",
|
| 102 |
+
"language": "python",
|
| 103 |
+
"description": (
|
| 104 |
+
"This function fetches records from a database using user-supplied input. "
|
| 105 |
+
"Identify the security vulnerability."
|
| 106 |
+
),
|
| 107 |
+
"code": (
|
| 108 |
+
"def fetch_records(user_id: str, sort_column: str):\n"
|
| 109 |
+
' """Fetch user records sorted by a given column."""\n'
|
| 110 |
+
" conn = get_db_connection()\n"
|
| 111 |
+
" cursor = conn.cursor()\n"
|
| 112 |
+
"\n"
|
| 113 |
+
" query = (\n"
|
| 114 |
+
' f"SELECT id, name, email FROM users "\n'
|
| 115 |
+
' f"WHERE user_id = {user_id} "\n'
|
| 116 |
+
' f"ORDER BY {sort_column}"\n'
|
| 117 |
+
" )\n"
|
| 118 |
+
" cursor.execute(query)\n"
|
| 119 |
+
" rows = cursor.fetchall()\n"
|
| 120 |
+
" conn.close()\n"
|
| 121 |
+
" return rows"
|
| 122 |
+
),
|
| 123 |
+
"ground_truth": {
|
| 124 |
+
"bug_identified": True,
|
| 125 |
+
"bug_type_keywords": [
|
| 126 |
+
"sql injection", "injection", "sqli", "sql",
|
| 127 |
+
"security vulnerability", "security", "second-order",
|
| 128 |
+
],
|
| 129 |
+
"location_keywords": [
|
| 130 |
+
"f\"", "f-string", "format", "user_id", "sort_column",
|
| 131 |
+
"query", "ORDER BY", "WHERE user_id",
|
| 132 |
+
],
|
| 133 |
+
"description_keywords": [
|
| 134 |
+
"sql injection", "injection", "parameterized", "f-string",
|
| 135 |
+
"format string", "user input", "sanitize", "escape",
|
| 136 |
+
"malicious", "attack", "tautology", "union", "drop",
|
| 137 |
+
"ORDER BY", "sort_column", "arbitrary",
|
| 138 |
+
],
|
| 139 |
+
"fix_keywords": [
|
| 140 |
+
"parameterized", "?", "%s", "cursor.execute(query, (",
|
| 141 |
+
"cursor.execute(query, [", "prepared statement",
|
| 142 |
+
"whitelist", "allowlist", "ALLOWED_COLUMNS",
|
| 143 |
+
"sanitize", "if sort_column not in",
|
| 144 |
+
],
|
| 145 |
+
"severity_valid": ["critical"],
|
| 146 |
+
},
|
| 147 |
+
},
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
# ──────────────────────────────────────────────
|
| 152 |
+
# GRADER
|
| 153 |
+
# ──────────────────────────────────────────────
|
| 154 |
+
|
| 155 |
+
def grade_action(action: CodeReviewAction, task: dict) -> Tuple[float, Dict]:
|
| 156 |
+
"""
|
| 157 |
+
Score the agent's review on a 0.0–1.0 scale.
|
| 158 |
+
|
| 159 |
+
Breakdown:
|
| 160 |
+
bug_identified 0.20
|
| 161 |
+
bug_type 0.20
|
| 162 |
+
bug_location 0.10
|
| 163 |
+
bug_description 0.25 (keyword density, capped)
|
| 164 |
+
suggested_fix 0.15 (keyword density, capped)
|
| 165 |
+
severity 0.10
|
| 166 |
+
─────────────────────
|
| 167 |
+
Total 1.00
|
| 168 |
+
"""
|
| 169 |
+
gt = task["ground_truth"]
|
| 170 |
+
score = 0.0
|
| 171 |
+
breakdown: Dict[str, float] = {}
|
| 172 |
+
|
| 173 |
+
# 1. Bug identification
|
| 174 |
+
if action.bug_identified == gt["bug_identified"]:
|
| 175 |
+
score += 0.20
|
| 176 |
+
breakdown["bug_identified"] = 0.20
|
| 177 |
+
else:
|
| 178 |
+
breakdown["bug_identified"] = 0.00
|
| 179 |
+
if not action.bug_identified:
|
| 180 |
+
return 0.0, {
|
| 181 |
+
"breakdown": breakdown,
|
| 182 |
+
"total_score": 0.0,
|
| 183 |
+
"feedback": "No bug identified — one definitely exists. Look more carefully.",
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
# 2. Bug type
|
| 187 |
+
bug_type_lower = action.bug_type.lower()
|
| 188 |
+
type_match = any(kw in bug_type_lower for kw in gt["bug_type_keywords"])
|
| 189 |
+
if type_match:
|
| 190 |
+
score += 0.20
|
| 191 |
+
breakdown["bug_type"] = 0.20
|
| 192 |
+
else:
|
| 193 |
+
breakdown["bug_type"] = 0.00
|
| 194 |
+
|
| 195 |
+
# 3. Bug location
|
| 196 |
+
loc_lower = action.bug_location.lower()
|
| 197 |
+
loc_match = any(kw.lower() in loc_lower for kw in gt["location_keywords"])
|
| 198 |
+
if loc_match:
|
| 199 |
+
score += 0.10
|
| 200 |
+
breakdown["bug_location"] = 0.10
|
| 201 |
+
else:
|
| 202 |
+
breakdown["bug_location"] = 0.00
|
| 203 |
+
|
| 204 |
+
# 4. Description quality (keyword density, capped at 0.25)
|
| 205 |
+
desc_lower = action.bug_description.lower()
|
| 206 |
+
desc_hits = sum(1 for kw in gt["description_keywords"] if kw.lower() in desc_lower)
|
| 207 |
+
desc_score = round(min(0.25, desc_hits * 0.07), 3)
|
| 208 |
+
score += desc_score
|
| 209 |
+
breakdown["bug_description"] = desc_score
|
| 210 |
+
|
| 211 |
+
# 5. Fix quality (keyword density, capped at 0.15)
|
| 212 |
+
fix_lower = action.suggested_fix.lower()
|
| 213 |
+
fix_hits = sum(1 for kw in gt["fix_keywords"] if kw.lower() in fix_lower)
|
| 214 |
+
fix_score = round(min(0.15, fix_hits * 0.08), 3)
|
| 215 |
+
score += fix_score
|
| 216 |
+
breakdown["suggested_fix"] = fix_score
|
| 217 |
+
|
| 218 |
+
# 6. Severity
|
| 219 |
+
if action.severity.lower() in gt["severity_valid"]:
|
| 220 |
+
score += 0.10
|
| 221 |
+
breakdown["severity"] = 0.10
|
| 222 |
+
else:
|
| 223 |
+
breakdown["severity"] = 0.00
|
| 224 |
+
|
| 225 |
+
total = round(min(1.0, score), 3)
|
| 226 |
+
|
| 227 |
+
# Build human-readable feedback
|
| 228 |
+
hints = []
|
| 229 |
+
if breakdown["bug_type"] == 0:
|
| 230 |
+
hints.append("Reconsider the bug category — be more specific.")
|
| 231 |
+
if breakdown["bug_location"] == 0:
|
| 232 |
+
hints.append("Pinpoint the exact line or expression that contains the bug.")
|
| 233 |
+
if breakdown["suggested_fix"] < 0.08:
|
| 234 |
+
hints.append("Your fix does not address the root cause — revise it.")
|
| 235 |
+
if breakdown["severity"] == 0:
|
| 236 |
+
hints.append("Re-evaluate the severity level.")
|
| 237 |
+
|
| 238 |
+
feedback = " ".join(hints) if hints else "Strong analysis — refine the fix if needed."
|
| 239 |
+
|
| 240 |
+
return total, {"breakdown": breakdown, "total_score": total, "feedback": feedback}
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
# ──────────────────────────────────────────────
|
| 244 |
+
# ENVIRONMENT
|
| 245 |
+
# ──────────────────────────────────────────────
|
| 246 |
+
|
| 247 |
+
class CodeReviewEnvironment:
|
| 248 |
+
def __init__(self):
|
| 249 |
+
self._state: Optional[CodeReviewState] = None
|
| 250 |
+
self._current_task: Optional[dict] = None
|
| 251 |
+
|
| 252 |
+
def reset(self, difficulty: str = "easy") -> CodeReviewObservation:
|
| 253 |
+
if difficulty not in TASKS:
|
| 254 |
+
difficulty = "easy"
|
| 255 |
+
task = TASKS[difficulty]
|
| 256 |
+
self._current_task = task
|
| 257 |
+
self._state = CodeReviewState(
|
| 258 |
+
task_id=task["id"],
|
| 259 |
+
difficulty=difficulty,
|
| 260 |
+
step_count=0,
|
| 261 |
+
done=False,
|
| 262 |
+
total_reward=0.0,
|
| 263 |
+
task_complete=False,
|
| 264 |
+
)
|
| 265 |
+
return self._build_obs(step_number=0, previous_feedback=None)
|
| 266 |
+
|
| 267 |
+
def step(self, action: CodeReviewAction) -> Tuple[CodeReviewObservation, float, bool, Dict]:
|
| 268 |
+
if self._state is None or self._state.done:
|
| 269 |
+
raise ValueError("Call reset() before step().")
|
| 270 |
+
|
| 271 |
+
self._state.step_count += 1
|
| 272 |
+
reward, info = grade_action(action, self._current_task)
|
| 273 |
+
self._state.total_reward = round(self._state.total_reward + reward, 3)
|
| 274 |
+
|
| 275 |
+
# Done if agent nailed it or max steps reached
|
| 276 |
+
done = reward >= 0.80 or self._state.step_count >= MAX_STEPS
|
| 277 |
+
self._state.done = done
|
| 278 |
+
self._state.task_complete = reward >= 0.80
|
| 279 |
+
|
| 280 |
+
feedback = info.get("feedback") if not done else None
|
| 281 |
+
obs = self._build_obs(
|
| 282 |
+
step_number=self._state.step_count,
|
| 283 |
+
previous_feedback=feedback,
|
| 284 |
+
)
|
| 285 |
+
return obs, reward, done, info
|
| 286 |
+
|
| 287 |
+
def state(self) -> CodeReviewState:
|
| 288 |
+
if self._state is None:
|
| 289 |
+
return CodeReviewState(
|
| 290 |
+
task_id="", difficulty="easy",
|
| 291 |
+
step_count=0, done=False,
|
| 292 |
+
total_reward=0.0, task_complete=False,
|
| 293 |
+
)
|
| 294 |
+
return self._state
|
| 295 |
+
|
| 296 |
+
# ── helpers ───────────────────────────────
|
| 297 |
+
|
| 298 |
+
def _build_obs(self, step_number: int, previous_feedback: Optional[str]) -> CodeReviewObservation:
|
| 299 |
+
t = self._current_task
|
| 300 |
+
return CodeReviewObservation(
|
| 301 |
+
code_snippet=t["code"],
|
| 302 |
+
language=t["language"],
|
| 303 |
+
task_description=t["description"],
|
| 304 |
+
task_id=t["id"],
|
| 305 |
+
difficulty=t["difficulty"],
|
| 306 |
+
step_number=step_number,
|
| 307 |
+
max_steps=MAX_STEPS,
|
| 308 |
+
previous_feedback=previous_feedback,
|
| 309 |
+
)
|
inference.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Baseline inference script for Code Security Review OpenEnv.
|
| 3 |
+
|
| 4 |
+
Usage:
|
| 5 |
+
python inference.py
|
| 6 |
+
|
| 7 |
+
Required environment variables:
|
| 8 |
+
API_BASE_URL — LLM API endpoint (default: HF router)
|
| 9 |
+
MODEL_NAME — Model identifier
|
| 10 |
+
HF_TOKEN — Hugging Face / API key
|
| 11 |
+
ENV_BASE_URL — Running environment URL (default: http://localhost:7860)
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import os
|
| 15 |
+
import json
|
| 16 |
+
import time
|
| 17 |
+
import re
|
| 18 |
+
import requests
|
| 19 |
+
from openai import OpenAI
|
| 20 |
+
|
| 21 |
+
# ── Config ────────────────────────────────────────────────────────────────────
|
| 22 |
+
API_BASE_URL = os.environ.get("API_BASE_URL", "https://router.huggingface.co/v1")
|
| 23 |
+
MODEL_NAME = os.environ.get("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct")
|
| 24 |
+
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 25 |
+
ENV_BASE_URL = os.environ.get("ENV_BASE_URL", "http://localhost:7860")
|
| 26 |
+
|
| 27 |
+
client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
|
| 28 |
+
|
| 29 |
+
SYSTEM_PROMPT = """You are a senior security-focused code reviewer.
|
| 30 |
+
|
| 31 |
+
When given a code snippet, carefully analyse it for bugs and security issues.
|
| 32 |
+
|
| 33 |
+
Respond with ONLY a valid JSON object — no markdown, no explanation outside the JSON.
|
| 34 |
+
|
| 35 |
+
Schema:
|
| 36 |
+
{
|
| 37 |
+
"bug_identified": true or false,
|
| 38 |
+
"bug_location": "exact location (function name, line description, variable, expression)",
|
| 39 |
+
"bug_type": "off-by-one | logic-error | security-vulnerability | null-dereference | none",
|
| 40 |
+
"bug_description": "detailed explanation of why this is a bug and the impact",
|
| 41 |
+
"severity": "none | low | medium | high | critical",
|
| 42 |
+
"suggested_fix": "the corrected code snippet or a precise description of the fix"
|
| 43 |
+
}"""
|
| 44 |
+
|
| 45 |
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
| 46 |
+
|
| 47 |
+
def env_post(path: str, data: dict | None = None, params: dict | None = None) -> dict:
|
| 48 |
+
url = f"{ENV_BASE_URL}{path}"
|
| 49 |
+
resp = requests.post(url, json=data or {}, params=params or {}, timeout=30)
|
| 50 |
+
resp.raise_for_status()
|
| 51 |
+
return resp.json()
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def parse_json_from_llm(text: str) -> dict:
|
| 55 |
+
"""Robustly extract JSON from LLM output, stripping markdown fences."""
|
| 56 |
+
text = text.strip()
|
| 57 |
+
# Strip ```json ... ``` or ``` ... ```
|
| 58 |
+
text = re.sub(r"^```(?:json)?\s*", "", text)
|
| 59 |
+
text = re.sub(r"\s*```$", "", text)
|
| 60 |
+
return json.loads(text)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def build_prompt(obs: dict) -> str:
|
| 64 |
+
lines = [
|
| 65 |
+
f"Language: {obs['language']}",
|
| 66 |
+
f"Task: {obs['task_description']}",
|
| 67 |
+
"",
|
| 68 |
+
f"```{obs['language']}",
|
| 69 |
+
obs["code_snippet"],
|
| 70 |
+
"```",
|
| 71 |
+
]
|
| 72 |
+
if obs.get("previous_feedback"):
|
| 73 |
+
lines += ["", f"Previous feedback: {obs['previous_feedback']}",
|
| 74 |
+
"Revise your analysis accordingly."]
|
| 75 |
+
return "\n".join(lines)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
# ── Task runner ───────────────────────────────────────────────────────────────
|
| 79 |
+
|
| 80 |
+
def run_task(difficulty: str, task_num: int) -> dict:
|
| 81 |
+
reset_resp = env_post("/reset", params={"difficulty": difficulty})
|
| 82 |
+
obs = reset_resp["observation"]
|
| 83 |
+
|
| 84 |
+
print(f"[START] task={task_num} difficulty={difficulty} task_id={obs['task_id']} max_steps={obs['max_steps']}")
|
| 85 |
+
|
| 86 |
+
cumulative_reward = 0.0
|
| 87 |
+
step_num = 0
|
| 88 |
+
done = False
|
| 89 |
+
|
| 90 |
+
while not done and step_num < obs["max_steps"]:
|
| 91 |
+
step_num += 1
|
| 92 |
+
prompt = build_prompt(obs)
|
| 93 |
+
|
| 94 |
+
# ── LLM call ──────────────────────────────────────────────────────────
|
| 95 |
+
t0 = time.time()
|
| 96 |
+
try:
|
| 97 |
+
response = client.chat.completions.create(
|
| 98 |
+
model=MODEL_NAME,
|
| 99 |
+
messages=[
|
| 100 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 101 |
+
{"role": "user", "content": prompt},
|
| 102 |
+
],
|
| 103 |
+
temperature=0.1,
|
| 104 |
+
max_tokens=600,
|
| 105 |
+
)
|
| 106 |
+
raw = response.choices[0].message.content
|
| 107 |
+
action_dict = parse_json_from_llm(raw)
|
| 108 |
+
except Exception as exc:
|
| 109 |
+
print(f"[ERROR] task={task_num} step={step_num} llm_error={exc}")
|
| 110 |
+
action_dict = {
|
| 111 |
+
"bug_identified": False,
|
| 112 |
+
"bug_location": "",
|
| 113 |
+
"bug_type": "none",
|
| 114 |
+
"bug_description": "",
|
| 115 |
+
"severity": "none",
|
| 116 |
+
"suggested_fix": "",
|
| 117 |
+
}
|
| 118 |
+
latency = round(time.time() - t0, 2)
|
| 119 |
+
|
| 120 |
+
# ── Step env ──────────────────────────────────────────────────────────
|
| 121 |
+
step_resp = env_post("/step", data=action_dict)
|
| 122 |
+
reward = step_resp["reward"]
|
| 123 |
+
done = step_resp["done"]
|
| 124 |
+
obs = step_resp["observation"]
|
| 125 |
+
info = step_resp.get("info", {})
|
| 126 |
+
|
| 127 |
+
cumulative_reward += reward
|
| 128 |
+
|
| 129 |
+
print(
|
| 130 |
+
f"[STEP] task={task_num} step={step_num} "
|
| 131 |
+
f"reward={reward:.3f} cumulative={cumulative_reward:.3f} "
|
| 132 |
+
f"done={done} latency_s={latency}"
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
result = {
|
| 136 |
+
"task_num": task_num,
|
| 137 |
+
"difficulty": difficulty,
|
| 138 |
+
"total_reward": round(cumulative_reward, 3),
|
| 139 |
+
"steps_taken": step_num,
|
| 140 |
+
"success": cumulative_reward >= 0.8,
|
| 141 |
+
}
|
| 142 |
+
print(
|
| 143 |
+
f"[END] task={task_num} difficulty={difficulty} "
|
| 144 |
+
f"total_reward={result['total_reward']} success={result['success']}"
|
| 145 |
+
)
|
| 146 |
+
return result
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
# ── Main ──────────────────────────────────────────────────────────────────────
|
| 150 |
+
|
| 151 |
+
def main():
|
| 152 |
+
print(f"[INFO] model={MODEL_NAME} env={ENV_BASE_URL}")
|
| 153 |
+
|
| 154 |
+
tasks = [
|
| 155 |
+
("easy", 1),
|
| 156 |
+
("medium", 2),
|
| 157 |
+
("hard", 3),
|
| 158 |
+
]
|
| 159 |
+
results = []
|
| 160 |
+
|
| 161 |
+
for difficulty, task_num in tasks:
|
| 162 |
+
try:
|
| 163 |
+
r = run_task(difficulty, task_num)
|
| 164 |
+
except Exception as exc:
|
| 165 |
+
print(f"[ERROR] task={task_num} difficulty={difficulty} error={exc}")
|
| 166 |
+
r = {"task_num": task_num, "difficulty": difficulty,
|
| 167 |
+
"total_reward": 0.0, "success": False}
|
| 168 |
+
results.append(r)
|
| 169 |
+
|
| 170 |
+
avg = round(sum(r["total_reward"] for r in results) / len(results), 3)
|
| 171 |
+
successes = sum(1 for r in results if r.get("success"))
|
| 172 |
+
print(f"\n[SUMMARY] avg_reward={avg} tasks_passed={successes}/{len(results)}")
|
| 173 |
+
for r in results:
|
| 174 |
+
print(f" [{r['difficulty']:6}] reward={r['total_reward']:.3f} success={r.get('success', False)}")
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
if __name__ == "__main__":
|
| 178 |
+
main()
|
models.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import Optional, Any, Dict
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class CodeReviewAction(BaseModel):
|
| 6 |
+
"""Action taken by the agent: a structured code review."""
|
| 7 |
+
bug_identified: bool = Field(..., description="Whether a bug was found")
|
| 8 |
+
bug_location: str = Field(..., description="Location of the bug (function, line, variable)")
|
| 9 |
+
bug_type: str = Field(..., description="Type: off-by-one | logic-error | security-vulnerability | none")
|
| 10 |
+
bug_description: str = Field(..., description="Detailed explanation of why this is a bug")
|
| 11 |
+
severity: str = Field(..., description="Severity: none | low | medium | high | critical")
|
| 12 |
+
suggested_fix: str = Field(..., description="The corrected code or a description of how to fix it")
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class CodeReviewObservation(BaseModel):
|
| 16 |
+
"""What the agent sees at each step."""
|
| 17 |
+
code_snippet: str = Field(..., description="The code to review")
|
| 18 |
+
language: str = Field(..., description="Programming language")
|
| 19 |
+
task_description: str = Field(..., description="What the code is supposed to do")
|
| 20 |
+
task_id: str = Field(..., description="Unique task identifier")
|
| 21 |
+
difficulty: str = Field(..., description="easy | medium | hard")
|
| 22 |
+
step_number: int = Field(..., description="Current step within this episode")
|
| 23 |
+
max_steps: int = Field(..., description="Maximum steps allowed per episode")
|
| 24 |
+
previous_feedback: Optional[str] = Field(None, description="Feedback from previous step if any")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class CodeReviewState(BaseModel):
|
| 28 |
+
"""Internal environment state."""
|
| 29 |
+
task_id: str
|
| 30 |
+
difficulty: str
|
| 31 |
+
step_count: int
|
| 32 |
+
done: bool
|
| 33 |
+
total_reward: float
|
| 34 |
+
task_complete: bool
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class StepResponse(BaseModel):
|
| 38 |
+
observation: CodeReviewObservation
|
| 39 |
+
reward: float
|
| 40 |
+
done: bool
|
| 41 |
+
info: Dict[str, Any]
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class ResetResponse(BaseModel):
|
| 45 |
+
observation: CodeReviewObservation
|
openenv.yaml
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: code-review-env
|
| 2 |
+
version: 1.0.0
|
| 3 |
+
description: >
|
| 4 |
+
RL environment for training AI agents to detect bugs and security
|
| 5 |
+
vulnerabilities in real Python code. Covers off-by-one errors,
|
| 6 |
+
authentication logic flaws, and SQL injection — with deterministic
|
| 7 |
+
programmatic graders and partial-progress reward signals.
|
| 8 |
+
|
| 9 |
+
author: Inmodel Labs
|
| 10 |
+
tags:
|
| 11 |
+
- code-review
|
| 12 |
+
- security
|
| 13 |
+
- software-engineering
|
| 14 |
+
- real-world
|
| 15 |
+
- python
|
| 16 |
+
|
| 17 |
+
tasks:
|
| 18 |
+
- id: task_easy_001
|
| 19 |
+
difficulty: easy
|
| 20 |
+
description: "Detect off-by-one error in array traversal loop"
|
| 21 |
+
reset_params:
|
| 22 |
+
difficulty: easy
|
| 23 |
+
|
| 24 |
+
- id: task_medium_001
|
| 25 |
+
difficulty: medium
|
| 26 |
+
description: "Detect authentication logic flaw enabling privilege escalation"
|
| 27 |
+
reset_params:
|
| 28 |
+
difficulty: medium
|
| 29 |
+
|
| 30 |
+
- id: task_hard_001
|
| 31 |
+
difficulty: hard
|
| 32 |
+
description: "Detect SQL injection via unsanitised f-string database query"
|
| 33 |
+
reset_params:
|
| 34 |
+
difficulty: hard
|
| 35 |
+
|
| 36 |
+
action_space:
|
| 37 |
+
type: object
|
| 38 |
+
properties:
|
| 39 |
+
bug_identified: { type: boolean }
|
| 40 |
+
bug_location: { type: string }
|
| 41 |
+
bug_type: { type: string }
|
| 42 |
+
bug_description: { type: string }
|
| 43 |
+
severity: { type: string, enum: [none, low, medium, high, critical] }
|
| 44 |
+
suggested_fix: { type: string }
|
| 45 |
+
required:
|
| 46 |
+
- bug_identified
|
| 47 |
+
- bug_location
|
| 48 |
+
- bug_type
|
| 49 |
+
- bug_description
|
| 50 |
+
- severity
|
| 51 |
+
- suggested_fix
|
| 52 |
+
|
| 53 |
+
observation_space:
|
| 54 |
+
type: object
|
| 55 |
+
properties:
|
| 56 |
+
code_snippet: { type: string }
|
| 57 |
+
language: { type: string }
|
| 58 |
+
task_description: { type: string }
|
| 59 |
+
task_id: { type: string }
|
| 60 |
+
difficulty: { type: string, enum: [easy, medium, hard] }
|
| 61 |
+
step_number: { type: integer }
|
| 62 |
+
max_steps: { type: integer }
|
| 63 |
+
previous_feedback: { type: string, nullable: true }
|
| 64 |
+
|
| 65 |
+
reward:
|
| 66 |
+
min: 0.0
|
| 67 |
+
max: 1.0
|
| 68 |
+
description: >
|
| 69 |
+
Partial rewards for: bug identification (0.20), correct bug type (0.20),
|
| 70 |
+
precise location (0.10), description quality (0.25, keyword density),
|
| 71 |
+
fix quality (0.15, keyword density), correct severity (0.10).
|
| 72 |
+
|
| 73 |
+
endpoints:
|
| 74 |
+
health: GET /health
|
| 75 |
+
reset: POST /reset
|
| 76 |
+
step: POST /step
|
| 77 |
+
state: GET /state
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.0
|
| 2 |
+
uvicorn[standard]==0.30.6
|
| 3 |
+
pydantic==2.7.4
|
| 4 |
+
requests==2.32.3
|
| 5 |
+
openai==1.40.0
|