Nitishkumar-ai commited on
Commit
3583511
·
0 Parent(s):

first commit

Browse files
Files changed (8) hide show
  1. Dockerfile +19 -0
  2. README.md +148 -0
  3. app.py +64 -0
  4. environment.py +309 -0
  5. inference.py +178 -0
  6. models.py +45 -0
  7. openenv.yaml +77 -0
  8. 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