NanduKondreddy commited on
Commit
cda147c
·
0 Parent(s):

Replace repo with updated config-debug files

Browse files
.dockerignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ __pycache__
2
+ *.pyc
3
+ .git
4
+ test_*.py
5
+ validate-submission.sh
.gitattributes ADDED
File without changes
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ __pycache__/
2
+ *.pyc
Dockerfile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ RUN useradd -m -u 1000 user
9
+ COPY --chown=user . .
10
+ USER user
11
+
12
+ EXPOSE 7860
13
+
14
+ CMD ["python", "-m", "uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: ConfigDebugEnv
3
+ colorFrom: red
4
+ colorTo: yellow
5
+ sdk: docker
6
+ app_port: 7860
7
+ tags:
8
+ - openenv
9
+ - devops
10
+ - configuration
11
+ - debugging
12
+ - reinforcement-learning
13
+ - multi-task-rl
14
+ pinned: false
15
+ ---
16
+
17
+ # ConfigDebugEnv: Multi-Task RL Environment for Configuration Debugging
18
+
19
+ > **Challenge**: Train AI agents to autonomously debug broken configuration files across 7 real-world DevOps formats. This is a **multi-step sequential decision-making problem** requiring iterative reasoning and progressive understanding.
20
+
21
+ ## Why This Matters
22
+
23
+ **The Problem**: Configuration errors cause ~40% of production incidents and are among the hardest to debug manually because they require domain expertise across multiple technologies (JSON, YAML, Docker, Kubernetes, nginx, etc.).
24
+
25
+ **Why RL?**: Static rule-based fixes fail because:
26
+ - Bugs interact (fixing one reveals another)
27
+ - Context matters (nginx syntax differs from Kubernetes)
28
+ - Validation is semantic (type checker would pass some errors)
29
+
30
+ **ConfigDebugEnv's Solution**: Train agents with **partial rewards** to discover multi-step fixes:
31
+ - Agent attempts fix → gets 0.4 reward + error guidance
32
+ - Agent iterates → gets 0.7 reward + next error guidance
33
+ - Agent completes → gets 1.0 reward + advances to next task
34
+
35
+ This mirrors real-world debugging where solutions emerge through iteration, not revelation.
36
+
37
+ ---
38
+
39
+ ## Environment Design
40
+
41
+ ### Action Space
42
+ ```
43
+ ConfigDebugAction.fixed_config: str # Corrected configuration
44
+ ```
45
+
46
+ ### Observation Space
47
+ ```
48
+ {
49
+ "broken_config": str, # Current broken config
50
+ "file_type": str, # Format: json, yaml, dockerfile, etc
51
+ "error_message": str, # Specific guidance (e.g., "replicas must be int")
52
+ "task_id": str, # Current task ID
53
+ "task_description": str, # Human-readable task
54
+ "difficulty": str, # easy, medium, hard, very_hard
55
+ "num_bugs": int, # Total bugs in this config
56
+ "bugs_found_so_far": int, # Bugs fixed in this attempt
57
+ "previous_reward": float, # Last reward: [0.0, 1.0]
58
+ }
59
+ ```
60
+
61
+ ### Reward Structure (Sequential Decision Making)
62
+
63
+ Each task has **3 progressive levels**:
64
+
65
+ | Level | Example (Kubernetes) | Reward | Guides Next Attempt |
66
+ |-------|----------------------|--------|---------------------|
67
+ | L1 | Fix replicas type | 0.4 | "containerPort must be int" |
68
+ | L2 | Fix port type too | 0.7 | "cpu must include unit (m)" |
69
+ | L3 | Fix all bugs | 1.0 | ✅ Task complete, advance |
70
+
71
+ **Why this works**: Agents learn to read error messages, make targeted fixes, and build on partial success—exactly like human debugging.
72
+
73
+ **⚠️ Important**: Hard tasks (Kubernetes, Nginx) **require sequential reasoning and cannot be solved in a single step**. Perfect first-attempt solutions will not occur; agents must iterate based on error guidance.
74
+
75
+ ---
76
+
77
+ ## The 7 Tasks (Progressive Difficulty)
78
+
79
+ | Task | Format | Difficulty | Bugs | Key Challenge |
80
+ |------|--------|------------|------|----------------|
81
+ | **task1_json** | JSON | Easy | 2 | Type detection |
82
+ | **task2_yaml** | YAML | Easy | 2 | Indentation / structure |
83
+ | **task3_dockerfile** | Dockerfile | Medium | 3 | Multi-stage builds |
84
+ | **task4_compose** | docker-compose | Medium | 4 | Service networking |
85
+ | **task5_k8s** | Kubernetes | Hard | 3* | Multi-step type/domain fixes |
86
+ | **task6_github_actions** | GitHub Actions | Hard | 5 | Workflow triggers |
87
+ | **task7_nginx** | Nginx config | Very Hard | 3* | Multi-step directive/routing fixes |
88
+
89
+ *task5_k8s and task7_nginx optimized for multi-step learning with targeted error guidance
90
+
91
+ ---
92
+
93
+ ## Multi-Task Learning
94
+
95
+ The environment progresses through all 7 tasks sequentially:
96
+ 1. Agent learns from task1 → task2 → ... → task7
97
+ 2. Each task builds on previous knowledge
98
+ 3. Harder tasks should show better reasoning (agents see more diverse error types)
99
+
100
+ ---
101
+
102
+ ## API Endpoints
103
+
104
+ ### Core OpenEnv Endpoints
105
+ - `POST /reset` - Reset environment to task1
106
+ - `POST /step` - Submit action (fixed config) → get reward + next observation
107
+ - `GET /observation` - Get current observation
108
+ - `GET /state` - Get full environment state
109
+
110
+ ### Utility Endpoints
111
+ - `GET /metadata` - Environment specification (auto-generated by OpenEnv)
112
+ - `GET /info` - Service info
113
+ - `GET /health` - Health check
114
+ - `GET /tasks` - List all tasks with metadata
115
+
116
+ ---
117
+
118
+ ## Example Session
119
+
120
+ ```python
121
+ from server.config_debug_environment import ConfigDebugEnvironment
122
+ from server.models import ConfigDebugAction
123
+
124
+ env = ConfigDebugEnvironment()
125
+ obs = env.reset() # Start at task1_json
126
+
127
+ # Attempt 1: Agent tries initial fix
128
+ action = ConfigDebugAction(fixed_config='{"key": "value"}')
129
+ obs = env.step(action)
130
+ print(f"Reward: {obs.reward}") # → 0.4 (partial credit)
131
+ print(f"Error: {obs.error_message}") # → Guides next attempt
132
+
133
+ # Attempt 2: Agent learns and improves
134
+ action = ConfigDebugAction(fixed_config='{"key": "value", "number": 42}')
135
+ obs = env.step(action)
136
+ print(f"Reward: {obs.reward}") # → 0.7
137
+ print(f"Error: {obs.error_message}") # → Final hint
138
+
139
+ # Attempt 3: Agent completes
140
+ action = ConfigDebugAction(fixed_config='{"key": "value", "number": 42, "enabled": true}')
141
+ obs = env.step(action)
142
+ print(f"Reward: {obs.reward}") # → 1.0 ✅
143
+ print(f"Done: {obs.done}") # → False (more tasks remain)
144
+ obs = obs.task_id # → task2_yaml
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Key Strengths
150
+
151
+ ✅ **True RL Problem**: Partial rewards with sequential decision-making
152
+ ✅ **Multi-Step Reasoning**: Errors cascade; fixes must be iterative
153
+ ✅ **Domain-Diverse**: 7 config formats = varied error types
154
+ ✅ **Scalable Difficulty**: Easy tasks build foundation for hard tasks
155
+ ✅ **Real-World Relevance**: Configuration bugs are a major DevOps pain point
156
+
157
+ ---
158
+
159
+ ## Technical Stack
160
+
161
+ - **Framework**: OpenEnv (FastAPI-based)
162
+ - **Language**: Python 3.12+
163
+ - **Graders**: Domain-specific validators (YAML, JSON, Docker, K8s, nginx)
164
+ - **Deployment**: Docker + Hugging Face Spaces
165
+
166
+ ---
167
+
168
+ ## Getting Started
169
+
170
+ ```bash
171
+ # Install dependencies
172
+ pip install -r requirements.txt
173
+
174
+ # Run tests
175
+ python test_env.py
176
+
177
+ # Start server
178
+ uvicorn server.app:app --reload
179
+
180
+ # Access at http://localhost:8000
181
+ ```
182
+
183
+ ---
184
+
185
+ ## Evaluation Criteria
186
+
187
+ - ✅ **Correct Task Progression** - All 7 tasks with proper graders
188
+ - ✅ **Multi-Step Learning** - Agents show iterative improvement
189
+ - ✅ **Error Guidance** - Clear feedback directs next fix
190
+ - ✅ **Reward Semantics** - Partial credit enables intermediate learning
191
+
192
+ ---
193
+
194
+ ## For Judges
195
+
196
+ This project demonstrates **advanced RL environment design**:
197
+
198
+ 1. **Problem Formulation**: Configuration debugging is inherently sequential and iterative
199
+ 2. **Reward Engineering**: Partial rewards guide exploration; not just binary pass/fail
200
+ 3. **Task Curriculum**: 7 tasks from easy→hard with consistent structure
201
+ 4. **Error Pedagogy**: Error messages teach agents what to fix next
202
+ 5. **Scalability**: Framework generalizes to any text-based debugging task
203
+
204
+ ConfigDebugEnv isn't just a benchmark—it's a **learning tool** that teaches agents to think like human debuggers.
205
+
206
+ ---
207
+
208
+ **Built for**: Meta PyTorch OpenEnv Hackathon x SST
209
+ **Version**: 1.0.0
210
+ - GET /docs - Swagger API docs
211
+
212
+ ## Setup
213
+
214
+ Run locally:
215
+
216
+ pip install fastapi uvicorn pydantic pyyaml httpx openai gradio
217
+ uvicorn server.env:app --host 0.0.0.0 --port 7860
218
+
219
+ Run with Docker:
220
+
221
+ docker build -t config-debug-env .
222
+ docker run -p 7860:7860 config-debug-env
223
+
224
+ ## Baseline Results
225
+
226
+ Qwen/Qwen2.5-72B-Instruct: 7/7 tasks, score = 1.000
__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from server.models import ConfigDebugAction, ConfigDebugObservation, ConfigDebugState
2
+ from server.config_debug_environment import ConfigDebugEnvironment
3
+ from client import ConfigDebugEnv
client.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Client for ConfigDebugEnv - connects to the environment server."""
2
+ from typing import Dict, Any
3
+
4
+ from openenv.core.env_client import EnvClient
5
+ from openenv.core.client_types import StepResult
6
+ from server.models import ConfigDebugAction, ConfigDebugObservation, ConfigDebugState
7
+
8
+
9
+ class ConfigDebugEnv(EnvClient[ConfigDebugAction, ConfigDebugObservation, ConfigDebugState]):
10
+ """Typed client for the ConfigDebugEnv environment."""
11
+
12
+ def _step_payload(self, action: ConfigDebugAction) -> Dict[str, Any]:
13
+ return {"fixed_config": action.fixed_config}
14
+
15
+ def _parse_result(self, payload: Dict[str, Any]) -> StepResult[ConfigDebugObservation]:
16
+ obs_data = payload.get("observation", {})
17
+ obs = ConfigDebugObservation(**obs_data)
18
+ return StepResult(
19
+ observation=obs,
20
+ reward=payload.get("reward"),
21
+ done=payload.get("done", False),
22
+ )
23
+
24
+ def _parse_state(self, payload: Dict[str, Any]) -> ConfigDebugState:
25
+ return ConfigDebugState(**payload)
inference.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Inference Script — ConfigDebugEnv
3
+ ===================================
4
+ MANDATORY
5
+ - Before submitting, ensure the following variables are defined in your environment configuration:
6
+ API_BASE_URL The API endpoint for the LLM.
7
+ MODEL_NAME The model identifier to use for inference.
8
+ HF_TOKEN Your Hugging Face / API key.
9
+ IMAGE_NAME The name of the local image to use for the environment if you are using
10
+ from_docker_image() method
11
+
12
+ STDOUT FORMAT
13
+ - The script emits exactly three line types to stdout:
14
+ [START] task=<task_name> env=<benchmark> model=<model_name>
15
+ [STEP] step=<n> action=<action_str> reward=<0.00> done=<true|false> error=<msg|null>
16
+ [END] success=<true|false> steps=<n> score=<score> rewards=<r1,r2,...,rn>
17
+ """
18
+
19
+ import asyncio
20
+ import os
21
+ import textwrap
22
+ from typing import List, Optional
23
+
24
+ from openai import OpenAI
25
+
26
+ # Constants (not env-dependent)
27
+ IMAGE_NAME = None # Will be read at runtime
28
+ TASK_NAME = "config-debug" # Default
29
+ BENCHMARK = "config_debug_env" # Default
30
+ MAX_STEPS = 35 # 7 tasks × 5 steps each
31
+ TEMPERATURE = 0.1
32
+ MAX_TOKENS = 2000
33
+ SUCCESS_SCORE_THRESHOLD = 0.5 # normalized score in [0, 1]
34
+ MAX_TOTAL_REWARD = 7.0 # 7 tasks, 1.0 max per task
35
+
36
+ SYSTEM_PROMPT = textwrap.dedent(
37
+ """
38
+ You are an expert DevOps engineer specializing in configuration file debugging.
39
+ You will be given a broken configuration file and must fix ALL bugs in it.
40
+ Return ONLY the fixed configuration file content.
41
+ No explanations, no markdown formatting, no code blocks. Just the raw fixed configuration.
42
+ """
43
+ ).strip()
44
+
45
+
46
+ def log_start(task: str, env: str, model: str) -> None:
47
+ print(f"[START] task={task} env={env} model={model}", flush=True)
48
+
49
+
50
+ def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
51
+ error_val = error if error else "null"
52
+ done_val = str(done).lower()
53
+ print(
54
+ f"[STEP] step={step} action={action} reward={reward:.2f} done={done_val} error={error_val}",
55
+ flush=True,
56
+ )
57
+
58
+
59
+ def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
60
+ rewards_str = ",".join(f"{r:.2f}" for r in rewards)
61
+ print(
62
+ f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}",
63
+ flush=True,
64
+ )
65
+
66
+
67
+ def strip_code_blocks(text: str) -> str:
68
+ """Remove markdown code blocks if LLM wraps output in them."""
69
+ text = text.strip()
70
+ if text.startswith("```"):
71
+ lines = text.split("\n")
72
+ if lines[-1].strip() == "```":
73
+ lines = lines[1:-1]
74
+ else:
75
+ lines = lines[1:]
76
+ text = "\n".join(lines)
77
+ return text
78
+
79
+
80
+ def build_user_prompt(obs: dict, step: int, history: List[str]) -> str:
81
+ history_block = "\n".join(history[-4:]) if history else "None"
82
+ return textwrap.dedent(
83
+ f"""
84
+ Fix the following broken {obs['file_type']} configuration file.
85
+
86
+ Task: {obs['task_description']}
87
+ Difficulty: {obs['difficulty']}
88
+ Number of bugs to find: {obs['num_bugs']}
89
+ Bugs fixed so far: {obs['bugs_found_so_far']}
90
+ Error message: {obs['error_message']}
91
+ Step: {step}
92
+
93
+ Previous attempts:
94
+ {history_block}
95
+
96
+ Broken configuration:
97
+ ```
98
+ {obs['broken_config']}
99
+ ```
100
+
101
+ Return ONLY the fixed configuration file content.
102
+ """
103
+ ).strip()
104
+
105
+
106
+ def get_model_message(client: OpenAI, obs: dict, step: int, history: List[str], model_name: str) -> str:
107
+ user_prompt = build_user_prompt(obs, step, history)
108
+ try:
109
+ completion = client.chat.completions.create(
110
+ model=model_name,
111
+ messages=[
112
+ {"role": "system", "content": SYSTEM_PROMPT},
113
+ {"role": "user", "content": user_prompt},
114
+ ],
115
+ temperature=TEMPERATURE,
116
+ max_tokens=MAX_TOKENS,
117
+ stream=False,
118
+ )
119
+ text = (completion.choices[0].message.content or "").strip()
120
+ return strip_code_blocks(text) if text else ""
121
+ except Exception as exc:
122
+ print(f"[DEBUG] Model request failed: {exc}", flush=True)
123
+ return ""
124
+
125
+
126
+ async def main() -> None:
127
+ # Re-read env vars at runtime (evaluator may inject after module import)
128
+ api_key = os.getenv("API_KEY") or os.getenv("HF_TOKEN")
129
+ api_base_url = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
130
+ model_name = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct")
131
+ task_name = os.getenv("CONFIG_DEBUG_TASK", "config-debug")
132
+ benchmark = os.getenv("CONFIG_DEBUG_BENCHMARK", "config_debug_env")
133
+
134
+ client = OpenAI(base_url=api_base_url, api_key=api_key)
135
+
136
+ # Connect to the environment via HTTP
137
+ import httpx
138
+ import sys
139
+
140
+ class HTTPEnvClient:
141
+ """Simple HTTP client that mimics the OpenEnv SDK interface."""
142
+
143
+ def __init__(self, base_url: str):
144
+ self.base_url = base_url.rstrip("/")
145
+ self.http = httpx.AsyncClient(timeout=60.0)
146
+
147
+ async def reset(self):
148
+ resp = await self.http.post(f"{self.base_url}/reset")
149
+ resp.raise_for_status()
150
+ return resp.json()
151
+
152
+ async def step(self, action_data: dict):
153
+ resp = await self.http.post(f"{self.base_url}/step", json=action_data)
154
+ resp.raise_for_status()
155
+ return resp.json()
156
+
157
+ async def close(self):
158
+ await self.http.aclose()
159
+
160
+ env_url = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:7860"
161
+ env = HTTPEnvClient(env_url)
162
+
163
+ history: List[str] = []
164
+ rewards: List[float] = []
165
+ steps_taken = 0
166
+ score = 0.0
167
+ success = False
168
+
169
+ log_start(task=task_name, env=benchmark, model=model_name)
170
+
171
+ try:
172
+ result = await env.reset()
173
+ obs = result["observation"]
174
+ state = result["state"]
175
+
176
+ step_num = 0
177
+ while not state["is_done"]:
178
+ step_num += 1
179
+
180
+ fixed_config = get_model_message(client, obs, step_num, history, model_name)
181
+
182
+ step_result = await env.step({"fixed_config": fixed_config})
183
+ obs = step_result["observation"]
184
+ state = step_result["state"]
185
+ reward = step_result.get("reward", 0.0)
186
+ done = state["is_done"]
187
+ info = step_result.get("info", {})
188
+ error = info.get("error_message") if info.get("error_message") != "All checks passed!" else None
189
+
190
+ rewards.append(reward)
191
+ steps_taken = step_num
192
+
193
+ action_summary = f"fix({info.get('task_id', 'unknown')})"
194
+ log_step(step=step_num, action=action_summary, reward=reward, done=done, error=error)
195
+
196
+ history.append(f"Step {step_num}: {action_summary} -> reward {reward:+.2f}")
197
+
198
+ if done:
199
+ break
200
+
201
+ score = sum(rewards) / MAX_TOTAL_REWARD if MAX_TOTAL_REWARD > 0 else 0.0
202
+ score = min(max(score, 0.0), 1.0)
203
+ success = score >= SUCCESS_SCORE_THRESHOLD
204
+
205
+ finally:
206
+ try:
207
+ await env.close()
208
+ except Exception as e:
209
+ print(f"[DEBUG] env.close() error: {e}", flush=True)
210
+ log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
211
+
212
+
213
+ if __name__ == "__main__":
214
+ try:
215
+ asyncio.run(main())
216
+ except Exception as e:
217
+ print(f"[END] success=false error={str(e)}", flush=True)
openenv.yaml ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ spec_version: 1
2
+ name: config-debug-env
3
+ type: space
4
+ runtime: fastapi
5
+ app: server.app:app
6
+ port: 7860
7
+ description: "An RL environment for training AI agents to debug broken configuration files"
8
+ version: "1.0.0"
9
+ author: "meta-hackathon-participant"
10
+ tags:
11
+ - devops
12
+ - configuration
13
+ - debugging
14
+ - infrastructure
15
+
16
+ action_model: server.models:ConfigDebugAction
17
+ observation_model: server.models:ConfigDebugObservation
18
+ state_model: server.models:ConfigDebugState
19
+
20
+ tasks:
21
+ - id: task1_json
22
+ name: "JSON Config Debug"
23
+ difficulty: easy
24
+ num_bugs: 2
25
+ grader: server.graders.grader_api:grade_task1
26
+ has_grader: true
27
+ - id: task2_yaml
28
+ name: "YAML Config Debug"
29
+ difficulty: easy
30
+ num_bugs: 2
31
+ grader: server.graders.grader_api:grade_task2
32
+ has_grader: true
33
+ - id: task3_dockerfile
34
+ name: "Dockerfile Debug"
35
+ difficulty: medium
36
+ num_bugs: 3
37
+ grader: server.graders.grader_api:grade_task3
38
+ has_grader: true
39
+ - id: task4_compose
40
+ name: "Docker Compose Debug"
41
+ difficulty: medium
42
+ num_bugs: 4
43
+ grader: server.graders.grader_api:grade_task4
44
+ has_grader: true
45
+ - id: task5_k8s
46
+ name: "Kubernetes Config Debug"
47
+ difficulty: hard
48
+ num_bugs: 5
49
+ grader: server.graders.grader_api:grade_task5
50
+ has_grader: true
51
+ - id: task6_github_actions
52
+ name: "GitHub Actions Debug"
53
+ difficulty: hard
54
+ num_bugs: 5
55
+ grader: server.graders.grader_api:grade_task6
56
+ has_grader: true
57
+ - id: task7_nginx
58
+ name: "Nginx Config Debug"
59
+ difficulty: very_hard
60
+ num_bugs: 3
61
+ grader: server.graders.grader_api:grade_task7
62
+ has_grader: true
pyproject.toml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "config-debug-env"
3
+ version = "1.0.0"
4
+ description = "An RL environment for debugging broken configuration files"
5
+ requires-python = ">=3.10"
6
+ dependencies = [
7
+ "fastapi",
8
+ "uvicorn",
9
+ "pydantic",
10
+ "pyyaml",
11
+ "httpx",
12
+ "openai",
13
+ "gradio",
14
+ "openenv-core>=0.2.0",
15
+ ]
16
+
17
+ [project.scripts]
18
+ server = "server.app:main"
19
+
20
+ [build-system]
21
+ requires = ["setuptools"]
22
+ build-backend = "setuptools.backends._legacy:_Backend"
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ pydantic
4
+ pyyaml
5
+ httpx
6
+ openai
7
+ gradio
8
+ openenv-core>=0.2.0
server/Dockerfile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ RUN useradd -m -u 1000 user
9
+ COPY --chown=user . .
10
+ USER user
11
+
12
+ EXPOSE 7860
13
+
14
+ CMD ["python", "-m", "uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "7860"]
server/__init__.py ADDED
File without changes
server/app.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI application for ConfigDebugEnv.
2
+
3
+ Uses OpenEnv's create_fastapi_app() for standard framework compatibility
4
+ (WebSocket sessions, standard endpoints, grader discovery).
5
+ """
6
+ import json
7
+ import gradio as gr
8
+
9
+ from openenv.core.env_server import create_fastapi_app
10
+ from server.models import ConfigDebugAction, ConfigDebugObservation, ConfigDebugState
11
+ from server.config_debug_environment import ConfigDebugEnvironment
12
+ from server.tasks.task_registry import get_task, TASK_ORDER
13
+
14
+ # ---- Create the standard OpenEnv FastAPI app ----
15
+ # create_fastapi_app expects a callable (factory) that returns an Environment
16
+ app = create_fastapi_app(
17
+ ConfigDebugEnvironment, # factory / class — called per session
18
+ ConfigDebugAction, # action model (inherits Action)
19
+ ConfigDebugObservation, # observation model (inherits Observation)
20
+ )
21
+
22
+
23
+
24
+
25
+ # ---- Custom endpoints ----
26
+
27
+ @app.get("/info")
28
+ def info():
29
+ return {"name": "ConfigDebugEnv", "version": "1.0.0", "status": "running"}
30
+
31
+
32
+ @app.get("/health")
33
+ def health():
34
+ return {"status": "healthy"}
35
+
36
+
37
+
38
+
39
+
40
+ @app.get("/tasks")
41
+ def tasks():
42
+ return {
43
+ "tasks": [
44
+ {
45
+ "id": tid,
46
+ "name": get_task(tid).description,
47
+ "difficulty": get_task(tid).difficulty,
48
+ "file_type": get_task(tid).file_type,
49
+ "num_bugs": get_task(tid).num_bugs,
50
+ "has_grader": True,
51
+ }
52
+ for tid in TASK_ORDER
53
+ ],
54
+ "total_tasks": len(TASK_ORDER),
55
+ "tasks_with_graders": len(TASK_ORDER),
56
+ }
57
+
58
+
59
+ # ---- Gradio Web UI ----
60
+
61
+ _ui_env = ConfigDebugEnvironment()
62
+
63
+
64
+ def format_state(env):
65
+ """Format environment state with progress bar and RL signals."""
66
+ state = env.state
67
+ progress_bar = "█" * int(state.progress_ratio * 10) + "░" * (10 - int(state.progress_ratio * 10))
68
+
69
+ return f"""
70
+ Task Progress: {len(state.tasks_completed)+1}/7
71
+ Progress: {progress_bar} ({int(state.progress_ratio*100)}%)
72
+ Total Reward: {state.total_reward:.2f}
73
+
74
+ Current Task: {state.current_task_id}
75
+ Difficulty: {state.current_difficulty}
76
+
77
+ Bugs Found: {state.bugs_found_so_far}
78
+ Error: {state.current_error_message or 'None'}
79
+
80
+ Completed: {', '.join(state.tasks_completed) if state.tasks_completed else 'None'}
81
+ Remaining: {', '.join(state.tasks_remaining[:3]) if state.tasks_remaining else 'None'}
82
+ """
83
+
84
+
85
+ def ui_get_state():
86
+ """Get current environment state (inspectable state)."""
87
+ return format_state(_ui_env)
88
+
89
+
90
+ def ui_reset():
91
+ _ui_env.reset()
92
+ obs = _ui_env._build_observation()
93
+ return (
94
+ f"Task: {obs.task_id} | Difficulty: {obs.difficulty} | Bugs: {obs.num_bugs}",
95
+ obs.task_description,
96
+ obs.broken_config,
97
+ obs.error_message,
98
+ format_state(_ui_env),
99
+ "Environment reset. Submit a fixed config to begin.",
100
+ )
101
+
102
+
103
+ def ui_step(fixed_config):
104
+ if _ui_env._done:
105
+ return (
106
+ "All tasks completed!",
107
+ "",
108
+ "",
109
+ "Episode done. Click Reset to start again.",
110
+ format_state(_ui_env),
111
+ f"Final score: {_ui_env.total_reward:.1f} / {len(TASK_ORDER)}.0",
112
+ )
113
+
114
+ action = ConfigDebugAction(fixed_config=fixed_config)
115
+ obs = _ui_env.step(action)
116
+
117
+ history = f"Reward: {obs.reward:.2f} | Bugs found: {obs.bugs_found_so_far}/{obs.num_bugs}\nFeedback: {obs.error_message}"
118
+ return (
119
+ f"Task: {obs.task_id} | Difficulty: {obs.difficulty} | Bugs: {obs.num_bugs}",
120
+ obs.task_description,
121
+ obs.broken_config,
122
+ obs.error_message,
123
+ format_state(_ui_env),
124
+ history,
125
+ )
126
+
127
+
128
+ with gr.Blocks(title="ConfigDebugEnv") as demo:
129
+ gr.Markdown("# ConfigDebugEnv")
130
+ gr.Markdown("An RL environment for debugging broken config files across 7 real-world formats.")
131
+
132
+ with gr.Row():
133
+ with gr.Column(scale=1):
134
+ gr.Markdown("### Agent Interface")
135
+ task_info = gr.Textbox(label="Current Task", interactive=False)
136
+ task_desc = gr.Textbox(label="Task Description", interactive=False, lines=2)
137
+ broken_config = gr.Textbox(label="Broken Config", interactive=False, lines=10)
138
+ error_msg = gr.Textbox(label="Error Message", interactive=False, lines=2)
139
+
140
+ gr.Markdown("### Take Action")
141
+ fixed_config_input = gr.Textbox(label="Your Fixed Config", placeholder="Paste your fixed configuration here...", lines=10)
142
+ with gr.Row():
143
+ reset_btn = gr.Button("Reset Environment", variant="secondary")
144
+ step_btn = gr.Button("Step", variant="primary")
145
+ state_btn = gr.Button("Get State", variant="secondary")
146
+
147
+ with gr.Column(scale=1):
148
+ gr.Markdown("### State Observer")
149
+ state_display = gr.Textbox(label="Current State (with RL Signals)", interactive=False, lines=14)
150
+ history_display = gr.Textbox(label="Action History / Reward", interactive=False, lines=4)
151
+
152
+ reset_btn.click(
153
+ fn=ui_reset,
154
+ outputs=[task_info, task_desc, broken_config, error_msg, state_display, history_display],
155
+ )
156
+ step_btn.click(
157
+ fn=ui_step,
158
+ inputs=[fixed_config_input],
159
+ outputs=[task_info, task_desc, broken_config, error_msg, state_display, history_display],
160
+ )
161
+ state_btn.click(
162
+ fn=ui_get_state,
163
+ outputs=[state_display],
164
+ )
165
+
166
+
167
+ app = gr.mount_gradio_app(app, demo, path="/")
168
+
169
+
170
+ def main(host: str = "0.0.0.0", port: int = 7860):
171
+ import uvicorn
172
+ uvicorn.run(app, host=host, port=port)
173
+
174
+
175
+ if __name__ == "__main__":
176
+ main()
server/config_debug_environment.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ConfigDebugEnvironment - OpenEnv-compatible environment class.
2
+
3
+ Inherits from openenv.core.env_server.Environment and implements
4
+ the standard reset/step/state interface with multi-task logic.
5
+ """
6
+ from typing import Optional, Any
7
+ from uuid import uuid4
8
+
9
+ from openenv.core.env_server import Environment
10
+ from server.models import ConfigDebugAction, ConfigDebugObservation, ConfigDebugState
11
+ from server.tasks.task_registry import get_task, TASK_ORDER
12
+
13
+ MAX_STEPS_PER_TASK = 5
14
+
15
+
16
+ class ConfigDebugEnvironment(Environment):
17
+ """Multi-task config debugging environment.
18
+
19
+ Manages 7 sequential tasks internally. Each WebSocket session
20
+ (via create_fastapi_app) gets its own instance with independent state.
21
+ """
22
+
23
+ SUPPORTS_CONCURRENT_SESSIONS = True
24
+
25
+ def __init__(self):
26
+ self._init_episode()
27
+
28
+ def _init_episode(self):
29
+ self.task_ids = list(TASK_ORDER)
30
+ self.current_task_index = 0
31
+ self.current_step = 0
32
+ self.total_reward = 0.0
33
+ self._done = False
34
+ self.tasks_completed: list = []
35
+ self.bugs_found_so_far = 0
36
+ self.previous_reward = 0.0
37
+ self.current_error_message: Optional[str] = None
38
+ self.current_broken_config: Optional[str] = None
39
+ self._episode_id = str(uuid4())
40
+ self._global_step = 0
41
+
42
+ # ---- OpenEnv interface methods ----
43
+
44
+ def reset(self, seed: Optional[int] = None, episode_id: Optional[str] = None, **kwargs: Any) -> ConfigDebugObservation:
45
+ """Reset environment to initial state (task 1)."""
46
+ self._init_episode()
47
+ if episode_id:
48
+ self._episode_id = episode_id
49
+ return self._build_observation()
50
+
51
+ def step(self, action: ConfigDebugAction, timeout_s: Optional[float] = None, **kwargs: Any) -> ConfigDebugObservation:
52
+ """Process an action: run the grader, advance tasks if done."""
53
+ if self._done:
54
+ return self._build_observation()
55
+
56
+ task_id = self._current_task_id()
57
+ task = get_task(task_id)
58
+
59
+ # Run the grader
60
+ reward, error_message, bugs_fixed = task.grader(action.fixed_config)
61
+ reward = max(0.0, min(1.0, reward))
62
+
63
+ self.current_step += 1
64
+ self._global_step += 1
65
+ self.bugs_found_so_far = len(bugs_fixed)
66
+ self.previous_reward = round(reward, 4)
67
+ self.current_error_message = error_message
68
+
69
+ # Check if task is complete
70
+ task_done = reward >= 0.99 or self.current_step >= MAX_STEPS_PER_TASK
71
+
72
+ if task_done:
73
+ self.total_reward += reward
74
+ self.tasks_completed.append(task_id)
75
+ self.current_task_index += 1
76
+ self.current_step = 0
77
+ self.bugs_found_so_far = 0
78
+ self.current_error_message = None
79
+ self.current_broken_config = None
80
+ if self.current_task_index >= len(self.task_ids):
81
+ self._done = True
82
+ else:
83
+ self.current_broken_config = action.fixed_config
84
+
85
+ obs = self._build_observation()
86
+ obs.done = self._done
87
+ obs.reward = round(reward, 4)
88
+ return obs
89
+
90
+ @property
91
+ def state(self) -> ConfigDebugState:
92
+ """Return current environment state with enhanced RL signals."""
93
+ tasks_remaining = self.task_ids[self.current_task_index:]
94
+ if self._done:
95
+ tasks_remaining = []
96
+
97
+ total_tasks = len(self.task_ids)
98
+ completed_tasks = len(self.tasks_completed)
99
+ progress_ratio = completed_tasks / total_tasks if total_tasks > 0 else 0.0
100
+
101
+ current_task = get_task(self._current_task_id())
102
+
103
+ return ConfigDebugState(
104
+ episode_id=self._episode_id,
105
+ step_count=self._global_step,
106
+ current_task_id=self._current_task_id(),
107
+ current_step=self.current_step,
108
+ max_steps=MAX_STEPS_PER_TASK,
109
+ total_reward=round(self.total_reward, 4),
110
+ is_done=self._done,
111
+ tasks_completed=list(self.tasks_completed),
112
+ tasks_remaining=tasks_remaining,
113
+ # Enhanced RL signals
114
+ bugs_found_so_far=self.bugs_found_so_far,
115
+ current_error_message=self.current_error_message,
116
+ progress_ratio=round(progress_ratio, 2),
117
+ current_difficulty=current_task.difficulty,
118
+ )
119
+
120
+ # ---- Internal helpers ----
121
+
122
+ def _current_task_id(self) -> str:
123
+ if self.current_task_index < len(self.task_ids):
124
+ return self.task_ids[self.current_task_index]
125
+ return self.task_ids[-1]
126
+
127
+ def _build_observation(self) -> ConfigDebugObservation:
128
+ task_id = self._current_task_id()
129
+ task = get_task(task_id)
130
+ broken = self.current_broken_config if self.current_broken_config is not None else task.broken_config
131
+ error = self.current_error_message if self.current_error_message is not None else task.error_message
132
+
133
+ return ConfigDebugObservation(
134
+ broken_config=broken,
135
+ file_type=task.file_type,
136
+ error_message=error,
137
+ task_id=task.task_id,
138
+ task_description=task.description,
139
+ difficulty=task.difficulty,
140
+ num_bugs=task.num_bugs,
141
+ bugs_found_so_far=self.bugs_found_so_far,
142
+ previous_reward=self.previous_reward,
143
+ done=self._done,
144
+ reward=self.previous_reward,
145
+ )
server/env.py ADDED
@@ -0,0 +1,362 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from typing import Optional
3
+ import json
4
+ import gradio as gr
5
+
6
+ from server.models import ConfigDebugAction, ConfigDebugObservation, ConfigDebugState
7
+ from server.tasks.task_registry import get_task, get_all_task_ids, TASK_ORDER
8
+
9
+ app = FastAPI(title="ConfigDebugEnv", version="1.0.0")
10
+
11
+ # --- Environment State ---
12
+ MAX_STEPS_PER_TASK = 5
13
+
14
+
15
+ class EnvironmentState:
16
+ """Mutable environment state that persists across requests."""
17
+
18
+ def __init__(self):
19
+ self.reset_state()
20
+
21
+ def reset_state(self):
22
+ self.task_ids = list(TASK_ORDER)
23
+ self.current_task_index = 0
24
+ self.current_step = 0
25
+ self.total_reward = 0.0
26
+ self.is_done = False
27
+ self.tasks_completed = []
28
+ self.bugs_found_so_far = 0
29
+ self.previous_reward = 0.0
30
+ self.current_error_message: Optional[str] = None
31
+ self.current_broken_config: Optional[str] = None
32
+
33
+
34
+ env_state = EnvironmentState()
35
+
36
+
37
+ def _get_current_task_id() -> str:
38
+ if env_state.current_task_index < len(env_state.task_ids):
39
+ return env_state.task_ids[env_state.current_task_index]
40
+ return env_state.task_ids[-1]
41
+
42
+
43
+ def _build_observation() -> ConfigDebugObservation:
44
+ task_id = _get_current_task_id()
45
+ task = get_task(task_id)
46
+
47
+ broken_config = (
48
+ env_state.current_broken_config
49
+ if env_state.current_broken_config is not None
50
+ else task.broken_config
51
+ )
52
+ error_message = (
53
+ env_state.current_error_message
54
+ if env_state.current_error_message is not None
55
+ else task.error_message
56
+ )
57
+
58
+ return ConfigDebugObservation(
59
+ broken_config=broken_config,
60
+ file_type=task.file_type,
61
+ error_message=error_message,
62
+ task_id=task.task_id,
63
+ task_description=task.description,
64
+ difficulty=task.difficulty,
65
+ num_bugs=task.num_bugs,
66
+ bugs_found_so_far=env_state.bugs_found_so_far,
67
+ previous_reward=env_state.previous_reward,
68
+ )
69
+
70
+
71
+ def _build_state() -> ConfigDebugState:
72
+ task_id = _get_current_task_id()
73
+ tasks_remaining = env_state.task_ids[env_state.current_task_index:]
74
+ if env_state.is_done:
75
+ tasks_remaining = []
76
+
77
+ return ConfigDebugState(
78
+ current_task_id=task_id,
79
+ current_step=env_state.current_step,
80
+ max_steps=MAX_STEPS_PER_TASK,
81
+ total_reward=round(env_state.total_reward, 4),
82
+ is_done=env_state.is_done,
83
+ tasks_completed=list(env_state.tasks_completed),
84
+ tasks_remaining=tasks_remaining,
85
+ )
86
+
87
+
88
+ # --- API Endpoints ---
89
+
90
+
91
+ @app.get("/health")
92
+ def health():
93
+ return {"status": "healthy"}
94
+
95
+
96
+ @app.get("/info")
97
+ def info():
98
+ return {"name": "ConfigDebugEnv", "version": "1.0.0", "status": "running"}
99
+
100
+
101
+ @app.post("/reset")
102
+ def reset(task_id: str = None):
103
+ """Reset the environment to initial state, return first observation."""
104
+ env_state.reset_state()
105
+ if task_id and task_id in env_state.task_ids:
106
+ env_state.current_task_index = env_state.task_ids.index(task_id)
107
+
108
+ observation = _build_observation()
109
+ state = _build_state()
110
+
111
+ return {
112
+ "observation": observation.model_dump(),
113
+ "state": state.model_dump(),
114
+ }
115
+
116
+
117
+ @app.post("/step")
118
+ def step(action: ConfigDebugAction):
119
+ """
120
+ Take a step in the environment.
121
+ The agent submits a fixed config, the grader evaluates it.
122
+ """
123
+ if env_state.is_done:
124
+ raise HTTPException(
125
+ status_code=400,
126
+ detail="Environment is done. Call /reset to start a new episode.",
127
+ )
128
+
129
+ task_id = _get_current_task_id()
130
+ task = get_task(task_id)
131
+
132
+ # Run the grader
133
+ reward, error_message, bugs_fixed = task.grader(action.fixed_config)
134
+ reward = max(0.01, min(0.99, reward))
135
+
136
+ env_state.current_step += 1
137
+ env_state.bugs_found_so_far = len(bugs_fixed)
138
+ env_state.previous_reward = round(reward, 4)
139
+
140
+ # Update error message for feedback
141
+ env_state.current_error_message = error_message
142
+
143
+ # Check if task is complete (perfect score or max steps reached)
144
+ task_done = reward >= 0.99 or env_state.current_step >= MAX_STEPS_PER_TASK
145
+
146
+ task_reward = reward # Reward for this step
147
+
148
+ if task_done:
149
+ # Record the best reward for this task
150
+ env_state.total_reward += reward
151
+ env_state.tasks_completed.append(task_id)
152
+ env_state.current_task_index += 1
153
+
154
+ # Reset per-task state
155
+ env_state.current_step = 0
156
+ env_state.bugs_found_so_far = 0
157
+ env_state.current_error_message = None
158
+ env_state.current_broken_config = None
159
+
160
+ # Check if all tasks are done
161
+ if env_state.current_task_index >= len(env_state.task_ids):
162
+ env_state.is_done = True
163
+ else:
164
+ # If the agent submitted something, use it as the new "broken" config
165
+ # so the agent can iterate
166
+ env_state.current_broken_config = action.fixed_config
167
+
168
+ observation = _build_observation()
169
+ state = _build_state()
170
+
171
+ return {
172
+ "observation": observation.model_dump(),
173
+ "reward": round(task_reward, 4),
174
+ "done": env_state.is_done,
175
+ "state": state.model_dump(),
176
+ "info": {
177
+ "task_id": task_id,
178
+ "bugs_fixed": bugs_fixed,
179
+ "error_message": error_message,
180
+ "task_done": task_done,
181
+ },
182
+ }
183
+
184
+
185
+ @app.get("/state")
186
+ def state():
187
+ """Return current environment state."""
188
+ return _build_state().model_dump()
189
+
190
+
191
+ @app.get("/observation")
192
+ def observation():
193
+ """Return current observation."""
194
+ if env_state.is_done:
195
+ raise HTTPException(
196
+ status_code=400,
197
+ detail="Environment is done. Call /reset to start a new episode.",
198
+ )
199
+ return _build_observation().model_dump()
200
+
201
+
202
+
203
+ @app.get("/metadata")
204
+ def metadata():
205
+ return {
206
+ "env_name": "config_debug_env",
207
+ "version": "1.0.0",
208
+ "description": "Config file debugging environment",
209
+ "tasks": [
210
+ {"id": "task1_json", "name": "JSON Config Debug", "difficulty": "easy", "num_bugs": 2, "has_grader": True, "grader": "server.graders.json_grader:grade_task1"},
211
+ {"id": "task2_yaml", "name": "YAML Config Debug", "difficulty": "easy", "num_bugs": 2, "has_grader": True, "grader": "server.graders.yaml_grader:grade_task2"},
212
+ {"id": "task3_dockerfile", "name": "Dockerfile Debug", "difficulty": "medium", "num_bugs": 3, "has_grader": True, "grader": "server.graders.dockerfile_grader:grade_task3"},
213
+ {"id": "task4_compose", "name": "Docker Compose Debug", "difficulty": "medium", "num_bugs": 4, "has_grader": True, "grader": "server.graders.compose_grader:grade_task4"},
214
+ {"id": "task5_k8s", "name": "Kubernetes Config Debug", "difficulty": "hard", "num_bugs": 5, "has_grader": True, "grader": "server.graders.k8s_grader:grade_task5"},
215
+ {"id": "task6_github_actions", "name": "GitHub Actions Debug", "difficulty": "hard", "num_bugs": 5, "has_grader": True, "grader": "server.graders.github_actions_grader:grade_task6"},
216
+ {"id": "task7_nginx", "name": "Nginx Config Debug", "difficulty": "very_hard", "num_bugs": 6, "has_grader": True, "grader": "server.graders.nginx_grader:grade_task7"},
217
+ ],
218
+ "action_model": "ConfigDebugAction",
219
+ "observation_model": "ConfigDebugObservation",
220
+ "state_model": "ConfigDebugState",
221
+ }
222
+
223
+
224
+ @app.get("/tasks")
225
+ def tasks():
226
+ """Return list of all tasks with grader information."""
227
+ return {
228
+ "tasks": [
229
+ {
230
+ "id": tid,
231
+ "name": get_task(tid).description,
232
+ "difficulty": get_task(tid).difficulty,
233
+ "file_type": get_task(tid).file_type,
234
+ "num_bugs": get_task(tid).num_bugs,
235
+ "has_grader": True,
236
+ }
237
+ for tid in TASK_ORDER
238
+ ],
239
+ "total_tasks": len(TASK_ORDER),
240
+ "tasks_with_graders": len(TASK_ORDER),
241
+ }
242
+
243
+ @app.get("/schema")
244
+ def schema():
245
+ return {
246
+ "action": ConfigDebugAction.model_json_schema(),
247
+ "observation": ConfigDebugObservation.model_json_schema(),
248
+ "state": ConfigDebugState.model_json_schema(),
249
+ }
250
+
251
+ # --- Gradio Web UI ---
252
+
253
+
254
+ def ui_reset():
255
+ env_state.reset_state()
256
+ obs = _build_observation()
257
+ st = _build_state()
258
+ return (
259
+ f"Task: {obs.task_id} | Difficulty: {obs.difficulty} | Bugs: {obs.num_bugs}",
260
+ obs.task_description,
261
+ obs.broken_config,
262
+ obs.error_message,
263
+ json.dumps(st.model_dump(), indent=2),
264
+ "Environment reset. Submit a fixed config to begin.",
265
+ )
266
+
267
+
268
+ def ui_step(fixed_config):
269
+ if env_state.is_done:
270
+ st = _build_state()
271
+ return (
272
+ "All tasks completed!",
273
+ "",
274
+ "",
275
+ "Episode done. Click Reset to start again.",
276
+ json.dumps(st.model_dump(), indent=2),
277
+ f"Final score: {env_state.total_reward:.1f} / {len(TASK_ORDER)}.0",
278
+ )
279
+
280
+ task_id = _get_current_task_id()
281
+ task = get_task(task_id)
282
+ reward, error_message, bugs_fixed = task.grader(fixed_config)
283
+ reward = max(0.01, min(0.99, reward))
284
+
285
+ env_state.current_step += 1
286
+ env_state.bugs_found_so_far = len(bugs_fixed)
287
+ env_state.previous_reward = round(reward, 4)
288
+ env_state.current_error_message = error_message
289
+
290
+ task_done = reward >= 0.99 or env_state.current_step >= MAX_STEPS_PER_TASK
291
+
292
+ if task_done:
293
+ env_state.total_reward += reward
294
+ env_state.tasks_completed.append(task_id)
295
+ env_state.current_task_index += 1
296
+ env_state.current_step = 0
297
+ env_state.bugs_found_so_far = 0
298
+ env_state.current_error_message = None
299
+ env_state.current_broken_config = None
300
+ if env_state.current_task_index >= len(env_state.task_ids):
301
+ env_state.is_done = True
302
+ else:
303
+ env_state.current_broken_config = fixed_config
304
+
305
+ obs = _build_observation()
306
+ st = _build_state()
307
+ history = f"Reward: {reward:.2f} | Bugs fixed: {bugs_fixed} | Task done: {task_done}\nFeedback: {error_message}"
308
+
309
+ return (
310
+ f"Task: {obs.task_id} | Difficulty: {obs.difficulty} | Bugs: {obs.num_bugs}",
311
+ obs.task_description,
312
+ obs.broken_config,
313
+ obs.error_message,
314
+ json.dumps(st.model_dump(), indent=2),
315
+ history,
316
+ )
317
+
318
+
319
+ def ui_get_state():
320
+ st = _build_state()
321
+ return json.dumps(st.model_dump(), indent=2)
322
+
323
+
324
+ with gr.Blocks(title="ConfigDebugEnv", theme=gr.themes.Soft()) as demo:
325
+ gr.Markdown("# ConfigDebugEnv")
326
+ gr.Markdown("An RL environment for debugging broken config files across 7 real-world formats: JSON, YAML, Dockerfile, docker-compose, Kubernetes, GitHub Actions, nginx.")
327
+
328
+ with gr.Row():
329
+ with gr.Column(scale=1):
330
+ gr.Markdown("### HumanAgent Interface")
331
+ task_info = gr.Textbox(label="Current Task", interactive=False)
332
+ task_desc = gr.Textbox(label="Task Description", interactive=False, lines=2)
333
+ broken_config = gr.Textbox(label="Broken Config", interactive=False, lines=10)
334
+ error_msg = gr.Textbox(label="Error Message", interactive=False, lines=2)
335
+
336
+ gr.Markdown("### Take Action")
337
+ fixed_config_input = gr.Textbox(label="Your Fixed Config", placeholder="Paste your fixed configuration here...", lines=10)
338
+ with gr.Row():
339
+ reset_btn = gr.Button("Reset Environment", variant="secondary")
340
+ step_btn = gr.Button("Step", variant="primary")
341
+ state_btn = gr.Button("Get State", variant="secondary")
342
+
343
+ with gr.Column(scale=1):
344
+ gr.Markdown("### State Observer")
345
+ state_display = gr.Textbox(label="Current State", interactive=False, lines=12)
346
+ history_display = gr.Textbox(label="Action History / Reward", interactive=False, lines=4)
347
+
348
+ reset_btn.click(
349
+ fn=ui_reset,
350
+ outputs=[task_info, task_desc, broken_config, error_msg, state_display, history_display],
351
+ )
352
+ step_btn.click(
353
+ fn=ui_step,
354
+ inputs=[fixed_config_input],
355
+ outputs=[task_info, task_desc, broken_config, error_msg, state_display, history_display],
356
+ )
357
+ state_btn.click(
358
+ fn=ui_get_state,
359
+ outputs=[state_display],
360
+ )
361
+
362
+ app = gr.mount_gradio_app(app, demo, path="/")
server/graders/__init__.py ADDED
File without changes
server/graders/compose_grader.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import yaml
2
+ from typing import Tuple, List
3
+
4
+
5
+ def grade_task4(submitted_config: str) -> Tuple[float, str, List[str]]:
6
+ """
7
+ Grade Task 4: Broken docker-compose.yml (4 bugs)
8
+ Bug 1 (Syntax): Wrong indentation on volumes key
9
+ Bug 2 (Semantic): Service references non-existent network name
10
+ Bug 3 (Runtime): Port mapping "8080:80" should be "8080:3000"
11
+ Bug 4 (Integration): depends_on references "postgres" but service is defined as "db"
12
+
13
+ Returns: (reward, error_message, bugs_fixed_list)
14
+ """
15
+ bugs_fixed = []
16
+ total_bugs = 4
17
+ error_messages = []
18
+
19
+ # Bug 1: Check if YAML parses (syntax layer)
20
+ try:
21
+ config = yaml.safe_load(submitted_config)
22
+ if not isinstance(config, dict):
23
+ error_messages.append("docker-compose config is not a valid mapping")
24
+ return 0.0, "; ".join(error_messages), bugs_fixed
25
+ bugs_fixed.append("syntax_valid_yaml")
26
+ except yaml.YAMLError as e:
27
+ error_messages.append(f"YAML parse error: {str(e)}")
28
+ return 0.0, "; ".join(error_messages), bugs_fixed
29
+
30
+ services = config.get("services", {})
31
+ if not isinstance(services, dict):
32
+ error_messages.append("'services' key is missing or not a mapping")
33
+ reward = len(bugs_fixed) / total_bugs
34
+ return reward, "; ".join(error_messages), bugs_fixed
35
+
36
+ defined_service_names = set(services.keys())
37
+ defined_networks = set(config.get("networks", {}).keys()) if isinstance(config.get("networks"), dict) else set()
38
+
39
+ # Bug 4: Check depends_on references valid service names
40
+ all_deps_valid = True
41
+ for svc_name, svc_config in services.items():
42
+ if not isinstance(svc_config, dict):
43
+ continue
44
+ deps = svc_config.get("depends_on", [])
45
+ if isinstance(deps, list):
46
+ for dep in deps:
47
+ if dep not in defined_service_names:
48
+ all_deps_valid = False
49
+ error_messages.append(
50
+ f"Service '{svc_name}' depends_on '{dep}', "
51
+ f"but no service named '{dep}' is defined. "
52
+ f"Defined services: {sorted(defined_service_names)}"
53
+ )
54
+
55
+ if all_deps_valid:
56
+ bugs_fixed.append("depends_on_valid")
57
+
58
+ # Bug 2: Check all referenced networks exist in top-level networks
59
+ all_nets_valid = True
60
+ for svc_name, svc_config in services.items():
61
+ if not isinstance(svc_config, dict):
62
+ continue
63
+ svc_networks = svc_config.get("networks", [])
64
+ if isinstance(svc_networks, list):
65
+ for net in svc_networks:
66
+ if net not in defined_networks:
67
+ all_nets_valid = False
68
+ error_messages.append(
69
+ f"Service '{svc_name}' references network '{net}', "
70
+ f"but it's not defined in top-level networks. "
71
+ f"Defined networks: {sorted(defined_networks)}"
72
+ )
73
+
74
+ if all_nets_valid:
75
+ bugs_fixed.append("networks_valid")
76
+
77
+ # Bug 3: Check port mapping - web service should map to 3000
78
+ web_svc = services.get("web", {})
79
+ if isinstance(web_svc, dict):
80
+ ports = web_svc.get("ports", [])
81
+ port_correct = False
82
+ for port_mapping in ports:
83
+ port_str = str(port_mapping)
84
+ # Check the container port (right side) is 3000
85
+ if ":3000" in port_str:
86
+ port_correct = True
87
+ break
88
+ if port_correct:
89
+ bugs_fixed.append("port_mapping_correct")
90
+ else:
91
+ error_messages.append(
92
+ "Web service port mapping is incorrect. "
93
+ "The application runs on port 3000, so the container port should be 3000."
94
+ )
95
+ else:
96
+ error_messages.append("Web service is missing or misconfigured")
97
+
98
+ # Calculate reward
99
+ reward = len(bugs_fixed) / total_bugs
100
+
101
+ # Bonus for parsing
102
+ if len(bugs_fixed) >= 1:
103
+ reward = min(1.0, reward + 0.1)
104
+
105
+ if len(bugs_fixed) == total_bugs:
106
+ reward = 1.0
107
+
108
+ error_msg = "; ".join(error_messages) if error_messages else "All checks passed!"
109
+ return reward, error_msg, bugs_fixed
server/graders/dockerfile_grader.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from typing import Tuple, List
3
+
4
+ ALLOWED_BASE_IMAGES = [
5
+ "python:3.9-slim", "python:3.10-slim", "python:3.11-slim",
6
+ "python:3.9", "python:3.10", "python:3.11",
7
+ "python:3.9-alpine", "python:3.10-alpine", "python:3.11-alpine",
8
+ "python:3.9-slim-bullseye", "python:3.9-slim-bookworm",
9
+ "node:18", "node:20", "node:18-slim", "node:20-slim",
10
+ "ubuntu:22.04", "ubuntu:20.04", "debian:bullseye-slim",
11
+ ]
12
+
13
+
14
+ def grade_task3(submitted_config: str) -> Tuple[float, str, List[str]]:
15
+ """
16
+ Grade Task 3: Broken Dockerfile (3 bugs)
17
+ Bug 1 (Syntax): RUN command with wrong escape character (^ instead of \\)
18
+ Bug 2 (Semantic): Wrong base image tag (python:3.9-slm instead of python:3.9-slim)
19
+ Bug 3 (Runtime): Missing EXPOSE directive
20
+
21
+ Returns: (reward, error_message, bugs_fixed_list)
22
+ """
23
+ bugs_fixed = []
24
+ total_bugs = 3
25
+ error_messages = []
26
+
27
+ lines = submitted_config.strip().split("\n")
28
+
29
+ # Bug 2: Check base image is valid
30
+ from_lines = [l for l in lines if l.strip().upper().startswith("FROM")]
31
+ if from_lines:
32
+ from_image = from_lines[0].strip().split()[1] if len(from_lines[0].strip().split()) > 1 else ""
33
+ if from_image in ALLOWED_BASE_IMAGES:
34
+ bugs_fixed.append("valid_base_image")
35
+ else:
36
+ error_messages.append(
37
+ f"Base image '{from_image}' is not a recognized valid image. "
38
+ f"Did you mean 'python:3.9-slim'?"
39
+ )
40
+ else:
41
+ error_messages.append("Missing FROM instruction in Dockerfile")
42
+
43
+ # Bug 1: Check RUN syntax - no ^ for line continuation
44
+ run_blocks = []
45
+ in_run = False
46
+ current_run = []
47
+ for line in lines:
48
+ stripped = line.strip()
49
+ if stripped.upper().startswith("RUN "):
50
+ in_run = True
51
+ current_run = [line]
52
+ elif in_run:
53
+ if current_run[-1].rstrip().endswith("\\"):
54
+ current_run.append(line)
55
+ else:
56
+ run_blocks.append("\n".join(current_run))
57
+ in_run = False
58
+ current_run = []
59
+ if stripped.upper().startswith("RUN "):
60
+ in_run = True
61
+ current_run = [line]
62
+ if not in_run and current_run:
63
+ run_blocks.append("\n".join(current_run))
64
+ current_run = []
65
+ if current_run:
66
+ run_blocks.append("\n".join(current_run))
67
+
68
+ has_caret = any("^" in line and not line.strip().startswith("#") for line in lines)
69
+ # Check if multi-line RUN uses proper backslash
70
+ has_proper_continuation = any(
71
+ line.rstrip().endswith("\\") for line in lines
72
+ if not line.strip().startswith("#")
73
+ )
74
+
75
+ if not has_caret:
76
+ bugs_fixed.append("run_syntax_correct")
77
+ else:
78
+ error_messages.append(
79
+ "RUN command uses '^' for line continuation. "
80
+ "Dockerfiles use '\\' for multi-line commands."
81
+ )
82
+
83
+ # Bug 3: Check EXPOSE directive present
84
+ expose_lines = [l for l in lines if l.strip().upper().startswith("EXPOSE")]
85
+ if expose_lines:
86
+ bugs_fixed.append("expose_present")
87
+ else:
88
+ error_messages.append(
89
+ "Missing EXPOSE directive. The application port should be exposed."
90
+ )
91
+
92
+ # Calculate reward
93
+ reward = len(bugs_fixed) / total_bugs
94
+
95
+ # Bonus for having a parseable Dockerfile structure
96
+ has_from = any(l.strip().upper().startswith("FROM") for l in lines)
97
+ has_cmd_or_entrypoint = any(
98
+ l.strip().upper().startswith(("CMD", "ENTRYPOINT")) for l in lines
99
+ )
100
+ if has_from and has_cmd_or_entrypoint:
101
+ reward = min(1.0, reward + 0.1)
102
+
103
+ if len(bugs_fixed) == total_bugs:
104
+ reward = 1.0
105
+
106
+ error_msg = "; ".join(error_messages) if error_messages else "All checks passed!"
107
+ return reward, error_msg, bugs_fixed
server/graders/github_actions_grader.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import yaml
2
+ from typing import Tuple, List
3
+
4
+ VALID_RUNNERS = [
5
+ "ubuntu-latest", "ubuntu-22.04", "ubuntu-20.04", "ubuntu-24.04",
6
+ "macos-latest", "macos-14", "macos-13", "macos-12",
7
+ "windows-latest", "windows-2022", "windows-2019",
8
+ ]
9
+
10
+ VALID_CHECKOUT_PARAMS = [
11
+ "repository", "ref", "token", "ssh-key", "ssh-known-hosts", "ssh-strict",
12
+ "persist-credentials", "path", "clean", "filter", "sparse-checkout",
13
+ "sparse-checkout-cone-mode", "fetch-depth", "fetch-tags",
14
+ "show-progress", "lfs", "submodules", "set-safe-directory",
15
+ ]
16
+
17
+
18
+ def grade_task6(submitted_config: str) -> Tuple[float, str, List[str]]:
19
+ """
20
+ Grade Task 6: Broken GitHub Actions Workflow (5 bugs)
21
+ Bug 1 (Syntax): Missing colon after 'on' trigger
22
+ Bug 2 (Semantic): runs-on uses "ubuntu-latets" (typo)
23
+ Bug 3 (Semantic): actions/checkout with invalid parameter name "fetch-deph"
24
+ Bug 4 (Runtime): env var references secrets but not defined in env block (acceptable)
25
+ Bug 5 (Integration): Upload artifact name "build-output" vs download "build-artifact"
26
+
27
+ Returns: (reward, error_message, bugs_fixed_list)
28
+ """
29
+ bugs_fixed = []
30
+ total_bugs = 5
31
+ error_messages = []
32
+
33
+ # Bug 1: Check if YAML parses (syntax layer - the missing colon after 'on')
34
+ try:
35
+ config = yaml.safe_load(submitted_config)
36
+ if not isinstance(config, dict):
37
+ error_messages.append("GitHub Actions workflow is not a valid mapping")
38
+ return 0.0, "; ".join(error_messages), bugs_fixed
39
+ bugs_fixed.append("syntax_valid_yaml")
40
+ except yaml.YAMLError as e:
41
+ error_messages.append(f"YAML parse error: {str(e)}")
42
+ return 0.0, "; ".join(error_messages), bugs_fixed
43
+
44
+ jobs = config.get("jobs", {})
45
+ if not isinstance(jobs, dict):
46
+ error_messages.append("'jobs' key is missing or not a mapping")
47
+ reward = len(bugs_fixed) / total_bugs
48
+ return reward, "; ".join(error_messages), bugs_fixed
49
+
50
+ # Bug 2: Check runner names
51
+ all_runners_valid = True
52
+ for job_name, job_config in jobs.items():
53
+ if not isinstance(job_config, dict):
54
+ continue
55
+ runner = job_config.get("runs-on", "")
56
+ if runner and runner not in VALID_RUNNERS:
57
+ all_runners_valid = False
58
+ error_messages.append(
59
+ f"Job '{job_name}' uses runner '{runner}' which is not valid. "
60
+ f"Did you mean 'ubuntu-latest'?"
61
+ )
62
+ if all_runners_valid:
63
+ bugs_fixed.append("valid_runners")
64
+
65
+ # Bug 3: Check checkout action parameters
66
+ checkout_params_valid = True
67
+ for job_name, job_config in jobs.items():
68
+ if not isinstance(job_config, dict):
69
+ continue
70
+ steps = job_config.get("steps", [])
71
+ if not isinstance(steps, list):
72
+ continue
73
+ for step in steps:
74
+ if not isinstance(step, dict):
75
+ continue
76
+ uses = step.get("uses", "")
77
+ if "actions/checkout" in str(uses):
78
+ with_params = step.get("with", {})
79
+ if isinstance(with_params, dict):
80
+ for param_name in with_params.keys():
81
+ if param_name not in VALID_CHECKOUT_PARAMS:
82
+ checkout_params_valid = False
83
+ error_messages.append(
84
+ f"actions/checkout parameter '{param_name}' is invalid. "
85
+ f"Did you mean 'fetch-depth'?"
86
+ )
87
+ if checkout_params_valid:
88
+ bugs_fixed.append("checkout_params_valid")
89
+
90
+ # Bug 4: Check that env vars used with secrets are defined in env block
91
+ # This is more of a best-practice check - we'll check the env block exists on steps using secrets
92
+ env_usage_valid = True
93
+ for job_name, job_config in jobs.items():
94
+ if not isinstance(job_config, dict):
95
+ continue
96
+ steps = job_config.get("steps", [])
97
+ if not isinstance(steps, list):
98
+ continue
99
+ for step in steps:
100
+ if not isinstance(step, dict):
101
+ continue
102
+ step_env = step.get("env", {})
103
+ run_cmd = str(step.get("run", ""))
104
+ # Check if run uses env vars that aren't defined
105
+ if "${{" in run_cmd and "secrets." in run_cmd:
106
+ if not step_env:
107
+ env_usage_valid = False
108
+ error_messages.append(
109
+ f"Step '{step.get('name', 'unnamed')}' references secrets "
110
+ f"in run command but doesn't define them in env block."
111
+ )
112
+ if env_usage_valid:
113
+ bugs_fixed.append("env_vars_defined")
114
+
115
+ # Bug 5: Check artifact name consistency between upload and download
116
+ upload_names = set()
117
+ download_names = set()
118
+ for job_name, job_config in jobs.items():
119
+ if not isinstance(job_config, dict):
120
+ continue
121
+ steps = job_config.get("steps", [])
122
+ if not isinstance(steps, list):
123
+ continue
124
+ for step in steps:
125
+ if not isinstance(step, dict):
126
+ continue
127
+ uses = str(step.get("uses", ""))
128
+ with_params = step.get("with", {})
129
+ if not isinstance(with_params, dict):
130
+ continue
131
+ if "upload-artifact" in uses:
132
+ name = with_params.get("name", "")
133
+ if name:
134
+ upload_names.add(name)
135
+ elif "download-artifact" in uses:
136
+ name = with_params.get("name", "")
137
+ if name:
138
+ download_names.add(name)
139
+
140
+ if download_names and upload_names:
141
+ if download_names.issubset(upload_names):
142
+ bugs_fixed.append("artifact_names_match")
143
+ else:
144
+ mismatched = download_names - upload_names
145
+ error_messages.append(
146
+ f"Artifact name mismatch: download references {sorted(mismatched)} "
147
+ f"but upload defines {sorted(upload_names)}. Names must match."
148
+ )
149
+ elif not download_names and not upload_names:
150
+ bugs_fixed.append("artifact_names_match")
151
+ else:
152
+ error_messages.append("Artifact upload/download configuration is incomplete")
153
+
154
+ # Calculate reward
155
+ reward = len(bugs_fixed) / total_bugs
156
+
157
+ if len(bugs_fixed) >= 1:
158
+ reward = min(1.0, reward + 0.1)
159
+
160
+ if len(bugs_fixed) == total_bugs:
161
+ reward = 1.0
162
+
163
+ error_msg = "; ".join(error_messages) if error_messages else "All checks passed!"
164
+ return reward, error_msg, bugs_fixed
server/graders/grader_api.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Grader API wrapper layer for OpenEnv validator compatibility.
2
+
3
+ OpenEnv validator expects graders to:
4
+ 1. Be callable
5
+ 2. Return float only (not tuple)
6
+ 3. Return values in [0.0, 1.0] range
7
+
8
+ Our graders return (reward, error_msg, bugs_fixed).
9
+ This wrapper extracts just the float reward.
10
+ """
11
+
12
+ from server.graders.json_grader import grade_task1 as _g1
13
+ from server.graders.yaml_grader import grade_task2 as _g2
14
+ from server.graders.dockerfile_grader import grade_task3 as _g3
15
+ from server.graders.compose_grader import grade_task4 as _g4
16
+ from server.graders.k8s_grader import grade_task5 as _g5
17
+ from server.graders.github_actions_grader import grade_task6 as _g6
18
+ from server.graders.nginx_grader import grade_task7 as _g7
19
+
20
+
21
+ def _extract(result):
22
+ """Extract float reward from grader result tuple or return as-is if already float."""
23
+ if isinstance(result, tuple):
24
+ return float(result[0])
25
+ return float(result)
26
+
27
+
28
+ def grade_task1(x):
29
+ """Task 1 (JSON) grader wrapper."""
30
+ return _extract(_g1(x))
31
+
32
+
33
+ def grade_task2(x):
34
+ """Task 2 (YAML) grader wrapper."""
35
+ return _extract(_g2(x))
36
+
37
+
38
+ def grade_task3(x):
39
+ """Task 3 (Dockerfile) grader wrapper."""
40
+ return _extract(_g3(x))
41
+
42
+
43
+ def grade_task4(x):
44
+ """Task 4 (Docker Compose) grader wrapper."""
45
+ return _extract(_g4(x))
46
+
47
+
48
+ def grade_task5(x):
49
+ """Task 5 (Kubernetes) grader wrapper."""
50
+ return _extract(_g5(x))
51
+
52
+
53
+ def grade_task6(x):
54
+ """Task 6 (GitHub Actions) grader wrapper."""
55
+ return _extract(_g6(x))
56
+
57
+
58
+ def grade_task7(x):
59
+ """Task 7 (Nginx) grader wrapper."""
60
+ return _extract(_g7(x))
server/graders/json_grader.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import Tuple, List
3
+
4
+
5
+ def grade_task1(submitted_config: str) -> Tuple[float, str, List[str]]:
6
+ """
7
+ Grade Task 1: Broken JSON Config (2 bugs)
8
+ Bug 1 (Syntax): Missing comma between key-value pairs
9
+ Bug 2 (Semantic): Wrong data type for "port" field (string instead of int)
10
+
11
+ Returns: (reward, error_message, bugs_fixed_list)
12
+ """
13
+ bugs_fixed = []
14
+ total_bugs = 2
15
+ error_messages = []
16
+
17
+ # Bug 1: Check if JSON parses (syntax layer)
18
+ try:
19
+ config = json.loads(submitted_config)
20
+ bugs_fixed.append("syntax_valid_json")
21
+ except json.JSONDecodeError as e:
22
+ error_messages.append(f"JSON parse error: {str(e)}")
23
+ reward = len(bugs_fixed) / total_bugs
24
+ return reward, "; ".join(error_messages), bugs_fixed
25
+
26
+ # Bug 2: Check port is integer (semantic layer)
27
+ if "port" in config:
28
+ if isinstance(config["port"], int):
29
+ bugs_fixed.append("port_is_integer")
30
+ else:
31
+ error_messages.append(
32
+ f"Field 'port' should be integer, got {type(config['port']).__name__}"
33
+ )
34
+ else:
35
+ error_messages.append("Missing required field: 'port'")
36
+
37
+ # Calculate reward
38
+ reward = len(bugs_fixed) / total_bugs
39
+
40
+ # Bonus for full parse
41
+ if len(bugs_fixed) >= 1:
42
+ reward = min(1.0, reward + 0.1)
43
+
44
+ if len(bugs_fixed) == total_bugs:
45
+ reward = 1.0
46
+
47
+ error_msg = "; ".join(error_messages) if error_messages else "All checks passed!"
48
+ return reward, error_msg, bugs_fixed
server/graders/k8s_grader.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import yaml
2
+ from typing import Tuple, List
3
+
4
+
5
+ def grade_task5(fixed_config: str) -> Tuple[float, str, List[str]]:
6
+ """
7
+ Grade Task 5: Multi-step Kubernetes Deployment YAML debugging.
8
+
9
+ Bug 1: replicas = "three" (string, should be integer) → 0.4 reward
10
+ Bug 2: containerPort = "80" (string, should be integer) → +0.3 reward
11
+ Bug 3: cpu = "500" (missing 'm' suffix, should be "500m") → +0.3 reward
12
+
13
+ Returns: (reward, error_message, bugs_fixed_list)
14
+ """
15
+ reward = 0.0
16
+ errors = []
17
+ fixed = []
18
+
19
+ # Parse YAML
20
+ try:
21
+ config = yaml.safe_load(fixed_config)
22
+ except Exception as e:
23
+ return 0.0, f"Invalid YAML format: {str(e)}", ["yaml"]
24
+
25
+ # --- STEP 1: replicas fix (0.0 → 0.4) ---
26
+ try:
27
+ replicas = config["spec"]["replicas"]
28
+ if isinstance(replicas, int):
29
+ reward += 0.4
30
+ fixed.append("replicas")
31
+ else:
32
+ errors.append("replicas must be an integer (not string)")
33
+ except Exception:
34
+ errors.append("missing or invalid replicas field")
35
+
36
+ # --- STEP 2: containerPort fix (0.4 → 0.7) ---
37
+ try:
38
+ port = config["spec"]["template"]["spec"]["containers"][0]["ports"][0]["containerPort"]
39
+ if isinstance(port, int):
40
+ reward += 0.3
41
+ fixed.append("port")
42
+ else:
43
+ errors.append("containerPort must be integer (not string)")
44
+ except Exception:
45
+ errors.append("missing or invalid containerPort")
46
+
47
+ # --- STEP 3: cpu resource unit fix (0.7 → 1.0) ---
48
+ try:
49
+ cpu = config["spec"]["template"]["spec"]["containers"][0]["resources"]["limits"]["cpu"]
50
+ if isinstance(cpu, str) and cpu.endswith("m"):
51
+ reward += 0.3
52
+ fixed.append("cpu")
53
+ else:
54
+ errors.append("cpu must be in millicores format (e.g., 500m, not plain integer)")
55
+ except Exception:
56
+ errors.append("missing or invalid cpu limit")
57
+
58
+ reward = round(min(reward, 1.0), 2)
59
+
60
+ if reward == 1.0:
61
+ return 1.0, "Deployment config is fully valid", fixed
62
+
63
+ error_msg = " ; ".join(errors) if errors else "Configuration has issues"
64
+ return reward, error_msg, fixed
server/graders/nginx_grader.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Tuple, List
2
+
3
+
4
+ def grade_task7(fixed_config: str) -> Tuple[float, str, List[str]]:
5
+ """
6
+ Grade Task 7: Multi-step nginx config debugging.
7
+
8
+ Bug 1: Missing semicolons in listen and error_log directives → 0.3 reward
9
+ Bug 2: Missing http:// prefix in proxy_pass directive → +0.3 reward
10
+ Bug 3: Improper routing with missing API endpoint headers → +0.4 reward
11
+
12
+ Returns: (reward, error_message, bugs_fixed_list)
13
+ """
14
+ reward = 0.0
15
+ errors = []
16
+ fixed = []
17
+
18
+ config = fixed_config.strip()
19
+
20
+ # --- STEP 1: Syntax checks (semicolons) ---
21
+ if "listen 80;" in config and "error_log logs/error.log;" in config:
22
+ reward += 0.3
23
+ fixed.append("syntax")
24
+ else:
25
+ errors.append("Missing semicolon in listen or error_log directives")
26
+
27
+ # --- STEP 2: Directive correctness (proxy_pass protocol) ---
28
+ if "proxy_pass http://localhost:3000;" in config:
29
+ reward += 0.3
30
+ fixed.append("proxy_pass")
31
+ else:
32
+ errors.append("proxy_pass for / must include http:// protocol prefix (e.g., http://localhost:3000;)")
33
+
34
+ # --- STEP 3: Routing logic (API endpoint with headers) ---
35
+ if ("location /api/" in config or "location /api {" in config):
36
+ if "proxy_set_header Host" in config and "proxy_set_header X-Real-IP" in config:
37
+ reward += 0.4
38
+ fixed.append("routing")
39
+ else:
40
+ errors.append("API routing missing required proxy headers (Host and X-Real-IP)")
41
+ else:
42
+ errors.append("Improper API route configuration (use 'location /api/' with headers)")
43
+
44
+ reward = round(min(reward, 1.0), 2)
45
+
46
+ if reward == 1.0:
47
+ return 1.0, "Nginx config fully valid", fixed
48
+
49
+ return reward, " ; ".join(errors), fixed
server/graders/yaml_grader.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import yaml
2
+ from typing import Tuple, List
3
+
4
+
5
+ def grade_task2(submitted_config: str) -> Tuple[float, str, List[str]]:
6
+ """
7
+ Grade Task 2: Broken YAML Config (2 bugs)
8
+ Bug 1 (Syntax): Wrong indentation on nested key (3 spaces instead of 2)
9
+ Bug 2 (Semantic): Missing required field "version"
10
+
11
+ Returns: (reward, error_message, bugs_fixed_list)
12
+ """
13
+ bugs_fixed = []
14
+ total_bugs = 2
15
+ error_messages = []
16
+
17
+ # Bug 1: Check if YAML parses (syntax layer)
18
+ try:
19
+ config = yaml.safe_load(submitted_config)
20
+ if isinstance(config, dict):
21
+ bugs_fixed.append("syntax_valid_yaml")
22
+ else:
23
+ error_messages.append("YAML parsed but result is not a mapping/dictionary")
24
+ reward = len(bugs_fixed) / total_bugs
25
+ return reward, "; ".join(error_messages), bugs_fixed
26
+ except yaml.YAMLError as e:
27
+ error_messages.append(f"YAML parse error: {str(e)}")
28
+ reward = len(bugs_fixed) / total_bugs
29
+ return reward, "; ".join(error_messages), bugs_fixed
30
+
31
+ # Bug 2: Check required field "version" exists
32
+ service = config.get("service", config)
33
+ if isinstance(service, dict) and "version" in service:
34
+ bugs_fixed.append("version_field_present")
35
+ else:
36
+ error_messages.append(
37
+ "Missing required field: 'version' under 'service'"
38
+ )
39
+
40
+ # Calculate reward
41
+ reward = len(bugs_fixed) / total_bugs
42
+
43
+ # Bonus for full parse
44
+ if len(bugs_fixed) >= 1:
45
+ reward = min(1.0, reward + 0.1)
46
+
47
+ if len(bugs_fixed) == total_bugs:
48
+ reward = 1.0
49
+
50
+ error_msg = "; ".join(error_messages) if error_messages else "All checks passed!"
51
+ return reward, error_msg, bugs_fixed
server/models.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional
2
+ from openenv.core.env_server import Action, Observation, State
3
+
4
+
5
+ class ConfigDebugAction(Action):
6
+ """Action: the agent submits a fixed configuration file"""
7
+ fixed_config: str # The corrected configuration file content
8
+
9
+
10
+ class ConfigDebugObservation(Observation):
11
+ """Observation: what the agent sees.
12
+ Inherits done: bool, reward: Optional[float], metadata from Observation.
13
+ """
14
+ broken_config: str = ""
15
+ file_type: str = ""
16
+ error_message: str = ""
17
+ task_id: str = ""
18
+ task_description: str = ""
19
+ difficulty: str = ""
20
+ num_bugs: int = 0
21
+ bugs_found_so_far: int = 0
22
+ previous_reward: float = 0.0
23
+
24
+
25
+ class ConfigDebugState(State):
26
+ """Full environment state.
27
+ Inherits episode_id: Optional[str], step_count: int from State.
28
+ """
29
+ current_task_id: str = ""
30
+ current_step: int = 0
31
+ max_steps: int = 5
32
+ total_reward: float = 0.0
33
+ is_done: bool = False
34
+ tasks_completed: List[str] = []
35
+ tasks_remaining: List[str] = []
36
+
37
+ # Enhanced state fields for better RL signal
38
+ bugs_found_so_far: int = 0
39
+ current_error_message: Optional[str] = None
40
+ progress_ratio: float = 0.0
41
+ current_difficulty: Optional[str] = None
server/requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ pydantic
4
+ pyyaml
5
+ httpx
6
+ openai
7
+ gradio
8
+ openenv-core>=0.2.0
server/tasks/__init__.py ADDED
File without changes
server/tasks/task1_json.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ TASK_ID = "task1_json"
2
+ DIFFICULTY = "easy"
3
+ FILE_TYPE = "json"
4
+ NUM_BUGS = 2
5
+
6
+ DESCRIPTION = (
7
+ "A simple application configuration file in JSON format. "
8
+ "It should define the app name, port (as integer), host, and debug mode."
9
+ )
10
+
11
+ # The broken config (what the agent sees)
12
+ # Bug 1 (Syntax): Missing comma after "my-service"
13
+ # Bug 2 (Semantic): port is string "8080" instead of integer 8080
14
+ BROKEN_CONFIG = """{
15
+ "app_name": "my-service"
16
+ "port": "8080",
17
+ "host": "0.0.0.0",
18
+ "debug": true
19
+ }"""
20
+
21
+ ERROR_MESSAGE = (
22
+ "JSON parse error: Expecting ',' delimiter: line 3 column 5 (char 33). "
23
+ "Additionally, there may be type issues with some fields."
24
+ )
25
+
26
+ GROUND_TRUTH = """{
27
+ "app_name": "my-service",
28
+ "port": 8080,
29
+ "host": "0.0.0.0",
30
+ "debug": true
31
+ }"""
server/tasks/task2_yaml.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ TASK_ID = "task2_yaml"
2
+ DIFFICULTY = "easy"
3
+ FILE_TYPE = "yaml"
4
+ NUM_BUGS = 2
5
+
6
+ DESCRIPTION = (
7
+ "A service configuration file in YAML format. "
8
+ "It should define the service name, version, port, host, database settings, "
9
+ "and logging configuration with proper indentation and all required fields."
10
+ )
11
+
12
+ # The broken config (what the agent sees)
13
+ # Bug 1 (Syntax): Wrong indentation on nested key (3 spaces instead of 2)
14
+ # Bug 2 (Semantic): Missing required field "version"
15
+ BROKEN_CONFIG = """service:
16
+ name: my-service
17
+ port: 8080
18
+ host: 0.0.0.0
19
+ database:
20
+ host: localhost
21
+ port: 5432
22
+ name: mydb
23
+ logging:
24
+ level: info
25
+ format: json"""
26
+
27
+ ERROR_MESSAGE = (
28
+ "YAML parse error: mapping values are not allowed in this context. "
29
+ "Additionally, the configuration may be missing required fields."
30
+ )
31
+
32
+ GROUND_TRUTH = """service:
33
+ name: my-service
34
+ version: "1.0.0"
35
+ port: 8080
36
+ host: 0.0.0.0
37
+ database:
38
+ host: localhost
39
+ port: 5432
40
+ name: mydb
41
+ logging:
42
+ level: info
43
+ format: json"""
server/tasks/task3_dockerfile.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ TASK_ID = "task3_dockerfile"
2
+ DIFFICULTY = "medium"
3
+ FILE_TYPE = "dockerfile"
4
+ NUM_BUGS = 3
5
+
6
+ DESCRIPTION = (
7
+ "A Dockerfile for a Python web application. It should use the python:3.9-slim "
8
+ "base image, install dependencies from requirements.txt, copy the application code, "
9
+ "expose port 8000, and run the application with uvicorn."
10
+ )
11
+
12
+ # Bug 1 (Syntax): Wrong escape character for multi-line RUN (using ^ instead of \)
13
+ # Bug 2 (Semantic): Wrong base image tag (python:3.9-slm instead of python:3.9-slim)
14
+ # Bug 3 (Runtime): Missing EXPOSE directive for the application port
15
+ BROKEN_CONFIG = """FROM python:3.9-slm
16
+
17
+ WORKDIR /app
18
+
19
+ COPY requirements.txt .
20
+
21
+ RUN pip install --no-cache-dir -r requirements.txt ^
22
+ && pip install uvicorn
23
+
24
+ COPY . .
25
+
26
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]"""
27
+
28
+ ERROR_MESSAGE = (
29
+ "Dockerfile build error: failed to resolve base image 'python:3.9-slm'. "
30
+ "Additionally, the RUN command may have syntax issues with multi-line continuation, "
31
+ "and the container may not be accessible on the expected port."
32
+ )
33
+
34
+ GROUND_TRUTH = """FROM python:3.9-slim
35
+
36
+ WORKDIR /app
37
+
38
+ COPY requirements.txt .
39
+
40
+ RUN pip install --no-cache-dir -r requirements.txt \\
41
+ && pip install uvicorn
42
+
43
+ COPY . .
44
+
45
+ EXPOSE 8000
46
+
47
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]"""
server/tasks/task4_compose.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ TASK_ID = "task4_compose"
2
+ DIFFICULTY = "medium"
3
+ FILE_TYPE = "docker-compose"
4
+ NUM_BUGS = 4
5
+
6
+ DESCRIPTION = (
7
+ "A docker-compose.yml file for a multi-service application with a web frontend "
8
+ "(Node.js app running on port 3000), a PostgreSQL database, and a Redis cache. "
9
+ "Services should be connected via a custom network and the web service should "
10
+ "depend on the database and cache services."
11
+ )
12
+
13
+ # Bug 1 (Syntax): Wrong indentation on volumes key
14
+ # Bug 2 (Semantic): Service references non-existent network name
15
+ # Bug 3 (Runtime): Port mapping "8080:80" should be "8080:3000" (app runs on 3000)
16
+ # Bug 4 (Integration): depends_on references "postgres" but service is defined as "db"
17
+ BROKEN_CONFIG = """version: "3.8"
18
+
19
+ services:
20
+ web:
21
+ build: ./app
22
+ ports:
23
+ - "8080:80"
24
+ environment:
25
+ - DATABASE_URL=postgresql://user:pass@db:5432/mydb
26
+ - REDIS_URL=redis://cache:6379
27
+ depends_on:
28
+ - postgres
29
+ - cache
30
+ networks:
31
+ - frontend-net
32
+
33
+ db:
34
+ image: postgres:15
35
+ environment:
36
+ - POSTGRES_USER=user
37
+ - POSTGRES_PASSWORD=pass
38
+ - POSTGRES_DB=mydb
39
+ volumes:
40
+ - db-data:/var/lib/postgresql/data
41
+ networks:
42
+ - backend-net
43
+
44
+ cache:
45
+ image: redis:7-alpine
46
+ networks:
47
+ - backend-net
48
+
49
+ networks:
50
+ app-network:
51
+ driver: bridge
52
+
53
+ volumes:
54
+ db-data:"""
55
+
56
+ ERROR_MESSAGE = (
57
+ "docker-compose config error: yaml parse error near 'volumes' key. "
58
+ "Additionally, there are issues with service references, network names, "
59
+ "and port mappings that need to be fixed."
60
+ )
61
+
62
+ GROUND_TRUTH = """version: "3.8"
63
+
64
+ services:
65
+ web:
66
+ build: ./app
67
+ ports:
68
+ - "8080:3000"
69
+ environment:
70
+ - DATABASE_URL=postgresql://user:pass@db:5432/mydb
71
+ - REDIS_URL=redis://cache:6379
72
+ depends_on:
73
+ - db
74
+ - cache
75
+ networks:
76
+ - app-network
77
+
78
+ db:
79
+ image: postgres:15
80
+ environment:
81
+ - POSTGRES_USER=user
82
+ - POSTGRES_PASSWORD=pass
83
+ - POSTGRES_DB=mydb
84
+ volumes:
85
+ - db-data:/var/lib/postgresql/data
86
+ networks:
87
+ - app-network
88
+
89
+ cache:
90
+ image: redis:7-alpine
91
+ networks:
92
+ - app-network
93
+
94
+ networks:
95
+ app-network:
96
+ driver: bridge
97
+
98
+ volumes:
99
+ db-data:"""
server/tasks/task5_k8s.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ TASK_ID = "task5_k8s"
2
+ DIFFICULTY = "hard"
3
+ FILE_TYPE = "kubernetes"
4
+ NUM_BUGS = 3
5
+
6
+ DESCRIPTION = (
7
+ "A Kubernetes Deployment manifest with multi-step bugs. Requires fixing replicas type, "
8
+ "containerPort type, and CPU resource units. Each fix provides intermediate feedback."
9
+ )
10
+
11
+ # Bug 1 (Type): replicas = "three" (string, should be integer)
12
+ # Bug 2 (Type): containerPort = "80" (string, should be integer)
13
+ # Bug 3 (Domain): cpu = "500" (missing 'm' suffix, should be "500m")
14
+ BROKEN_CONFIG = """apiVersion: apps/v1
15
+ kind: Deployment
16
+ metadata:
17
+ name: my-app
18
+ spec:
19
+ replicas: "three"
20
+ selector:
21
+ matchLabels:
22
+ app: my-app
23
+ template:
24
+ metadata:
25
+ labels:
26
+ app: my-app
27
+ spec:
28
+ containers:
29
+ - name: my-container
30
+ image: nginx
31
+ ports:
32
+ - containerPort: "80"
33
+ resources:
34
+ limits:
35
+ cpu: "500"
36
+ """
37
+
38
+ ERROR_MESSAGE = (
39
+ "Kubernetes manifest has type and unit errors: replicas must be integer, "
40
+ "containerPort must be integer, and cpu must include millicores unit (e.g., 500m)."
41
+ )
42
+
43
+ GROUND_TRUTH = """apiVersion: apps/v1
44
+ kind: Deployment
45
+ metadata:
46
+ name: my-app
47
+ spec:
48
+ replicas: 3
49
+ selector:
50
+ matchLabels:
51
+ app: my-app
52
+ template:
53
+ metadata:
54
+ labels:
55
+ app: my-app
56
+ spec:
57
+ containers:
58
+ - name: my-container
59
+ image: nginx
60
+ ports:
61
+ - containerPort: 80
62
+ resources:
63
+ limits:
64
+ cpu: "500m"
65
+ """
66
+
server/tasks/task6_github_actions.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ TASK_ID = "task6_github_actions"
2
+ DIFFICULTY = "hard"
3
+ FILE_TYPE = "github-actions"
4
+ NUM_BUGS = 5
5
+
6
+ DESCRIPTION = (
7
+ "A GitHub Actions workflow that builds a Node.js application on push to main, "
8
+ "runs tests, uploads a build artifact, and deploys it. It should use ubuntu-latest "
9
+ "runner, checkout the code, setup Node.js, install deps, build, upload artifact as "
10
+ "'build-output', and download the same artifact in the deploy job."
11
+ )
12
+
13
+ # Bug 1 (Syntax): Missing colon after "on" trigger value (push should be on:\n push:)
14
+ # Bug 2 (Semantic): runs-on uses "ubuntu-latets" (typo, should be "ubuntu-latest")
15
+ # Bug 3 (Semantic): Step uses actions/checkout@v2 but with has invalid parameter name
16
+ # Bug 4 (Runtime): Environment variable referenced but not defined in env block
17
+ # Bug 5 (Integration): Build step artifact name "build-output" but deploy references "build-artifact"
18
+ BROKEN_CONFIG = """name: CI/CD Pipeline
19
+
20
+ on
21
+ push:
22
+ branches: [main]
23
+ pull_request:
24
+ branches: [main]
25
+
26
+ jobs:
27
+ build:
28
+ runs-on: ubuntu-latets
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+ with:
32
+ fetch-deph: 0
33
+
34
+ - name: Setup Node.js
35
+ uses: actions/setup-node@v4
36
+ with:
37
+ node-version: '18'
38
+
39
+ - name: Install dependencies
40
+ run: npm ci
41
+
42
+ - name: Run tests
43
+ run: npm test
44
+ env:
45
+ API_ENDPOINT: ${{ secrets.API_KEY }}
46
+
47
+ - name: Build
48
+ run: npm run build
49
+
50
+ - name: Upload artifact
51
+ uses: actions/upload-artifact@v4
52
+ with:
53
+ name: build-output
54
+ path: dist/
55
+
56
+ deploy:
57
+ needs: build
58
+ runs-on: ubuntu-latest
59
+ steps:
60
+ - name: Download artifact
61
+ uses: actions/download-artifact@v4
62
+ with:
63
+ name: build-artifact
64
+
65
+ - name: Deploy
66
+ run: echo "Deploying..."
67
+ """
68
+
69
+ ERROR_MESSAGE = (
70
+ "GitHub Actions workflow error: YAML parse error near 'on' key. "
71
+ "Additionally, the runner name may have a typo, an action parameter name is invalid, "
72
+ "environment variables may be misconfigured, and artifact names between jobs may not match."
73
+ )
74
+
75
+ GROUND_TRUTH = """name: CI/CD Pipeline
76
+
77
+ on:
78
+ push:
79
+ branches: [main]
80
+ pull_request:
81
+ branches: [main]
82
+
83
+ jobs:
84
+ build:
85
+ runs-on: ubuntu-latest
86
+ steps:
87
+ - uses: actions/checkout@v4
88
+ with:
89
+ fetch-depth: 0
90
+
91
+ - name: Setup Node.js
92
+ uses: actions/setup-node@v4
93
+ with:
94
+ node-version: '18'
95
+
96
+ - name: Install dependencies
97
+ run: npm ci
98
+
99
+ - name: Run tests
100
+ run: npm test
101
+ env:
102
+ API_ENDPOINT: ${{ secrets.API_KEY }}
103
+
104
+ - name: Build
105
+ run: npm run build
106
+
107
+ - name: Upload artifact
108
+ uses: actions/upload-artifact@v4
109
+ with:
110
+ name: build-output
111
+ path: dist/
112
+
113
+ deploy:
114
+ needs: build
115
+ runs-on: ubuntu-latest
116
+ steps:
117
+ - name: Download artifact
118
+ uses: actions/download-artifact@v4
119
+ with:
120
+ name: build-output
121
+
122
+ - name: Deploy
123
+ run: echo "Deploying..."
124
+ """
server/tasks/task7_nginx.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ TASK_ID = "task7_nginx"
2
+ DIFFICULTY = "very_hard"
3
+ FILE_TYPE = "nginx"
4
+ NUM_BUGS = 3
5
+
6
+ DESCRIPTION = (
7
+ "An nginx reverse proxy configuration with multi-step bugs. "
8
+ "Requires fixing syntax (semicolons), directives (protocol), and routing logic (headers)."
9
+ )
10
+
11
+ # Bug 1 (Syntax): Missing semicolons in listen and error_log directives
12
+ # Bug 2 (Directive): proxy_pass missing http:// protocol prefix
13
+ # Bug 3 (Logic): Improper routing with missing headers for API endpoint
14
+ BROKEN_CONFIG = """events {}
15
+
16
+ http {
17
+ server {
18
+ listen 80
19
+
20
+ location / {
21
+ proxy_pass localhost:3000
22
+ }
23
+
24
+ location /api {
25
+ proxy_pass http://localhost:5000
26
+ proxy_set_header Host $host
27
+ proxy_set_header X-Real-IP $remote_addr
28
+ }
29
+
30
+ error_log logs/error.log
31
+ }
32
+ }"""
33
+
34
+ ERROR_MESSAGE = (
35
+ "nginx configuration has syntax, directive, and routing errors: "
36
+ "missing semicolons, missing http:// prefix in proxy_pass, and improper API routing."
37
+ )
38
+
39
+ GROUND_TRUTH = """events {}
40
+
41
+ http {
42
+ server {
43
+ listen 80;
44
+
45
+ location / {
46
+ proxy_pass http://localhost:3000;
47
+ }
48
+
49
+ location /api/ {
50
+ proxy_pass http://localhost:5000;
51
+ proxy_set_header Host $host;
52
+ proxy_set_header X-Real-IP $remote_addr;
53
+ }
54
+
55
+ error_log logs/error.log;
56
+ }
57
+ }"""
server/tasks/task_registry.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Callable, Dict, Tuple, List
2
+
3
+ from server.tasks import task1_json, task2_yaml, task3_dockerfile
4
+ from server.tasks import task4_compose, task5_k8s, task6_github_actions, task7_nginx
5
+ from server.graders.json_grader import grade_task1
6
+ from server.graders.yaml_grader import grade_task2
7
+ from server.graders.dockerfile_grader import grade_task3
8
+ from server.graders.compose_grader import grade_task4
9
+ from server.graders.k8s_grader import grade_task5
10
+ from server.graders.github_actions_grader import grade_task6
11
+ from server.graders.nginx_grader import grade_task7
12
+
13
+
14
+ class TaskInfo:
15
+ def __init__(self, module, grader_func: Callable):
16
+ self.task_id: str = module.TASK_ID
17
+ self.difficulty: str = module.DIFFICULTY
18
+ self.file_type: str = module.FILE_TYPE
19
+ self.num_bugs: int = module.NUM_BUGS
20
+ self.description: str = module.DESCRIPTION
21
+ self.broken_config: str = module.BROKEN_CONFIG
22
+ self.error_message: str = module.ERROR_MESSAGE
23
+ self.ground_truth: str = module.GROUND_TRUTH
24
+ self.grader: Callable[[str], Tuple[float, str, List[str]]] = grader_func
25
+
26
+
27
+ TASK_ORDER = [
28
+ "task1_json",
29
+ "task2_yaml",
30
+ "task3_dockerfile",
31
+ "task4_compose",
32
+ "task5_k8s",
33
+ "task6_github_actions",
34
+ "task7_nginx",
35
+ ]
36
+
37
+ TASK_REGISTRY: Dict[str, TaskInfo] = {
38
+ "task1_json": TaskInfo(task1_json, grade_task1),
39
+ "task2_yaml": TaskInfo(task2_yaml, grade_task2),
40
+ "task3_dockerfile": TaskInfo(task3_dockerfile, grade_task3),
41
+ "task4_compose": TaskInfo(task4_compose, grade_task4),
42
+ "task5_k8s": TaskInfo(task5_k8s, grade_task5),
43
+ "task6_github_actions": TaskInfo(task6_github_actions, grade_task6),
44
+ "task7_nginx": TaskInfo(task7_nginx, grade_task7),
45
+ }
46
+
47
+
48
+ def get_task(task_id: str) -> TaskInfo:
49
+ return TASK_REGISTRY[task_id]
50
+
51
+
52
+ def get_all_task_ids() -> List[str]:
53
+ return list(TASK_ORDER)
test_env.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from server.config_debug_environment import ConfigDebugEnvironment
2
+ from server.models import ConfigDebugAction
3
+ from server.tasks.task_registry import get_task
4
+
5
+ env = ConfigDebugEnvironment()
6
+
7
+ # ONLY ONE RESET
8
+ obs = env.reset()
9
+
10
+ for i in range(3):
11
+ print(f"\n--- Step {i+1} ---")
12
+
13
+ task = get_task(obs.task_id)
14
+ correct_output = task.ground_truth
15
+
16
+ action = ConfigDebugAction(fixed_config=correct_output)
17
+ obs = env.step(action)
18
+
19
+ print("Task ID:", obs.task_id)
20
+ print("Reward:", obs.reward)
21
+ print("Done:", obs.done)
test_k8s_multistep.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from server.graders.k8s_grader import grade_task5
2
+
3
+ # Test 1: Only fix replicas
4
+ config1 = """apiVersion: apps/v1
5
+ kind: Deployment
6
+ metadata:
7
+ name: my-app
8
+ spec:
9
+ replicas: 3
10
+ selector:
11
+ matchLabels:
12
+ app: my-app
13
+ template:
14
+ metadata:
15
+ labels:
16
+ app: my-app
17
+ spec:
18
+ containers:
19
+ - name: my-container
20
+ image: nginx
21
+ ports:
22
+ - containerPort: "80"
23
+ resources:
24
+ limits:
25
+ cpu: "500"
26
+ """
27
+
28
+ reward1, msg1, fixed1 = grade_task5(config1)
29
+ print(f'Step 1 (replicas fixed): Reward={reward1}, Fixed={fixed1}')
30
+ print(f' Error: {msg1}\n')
31
+
32
+ # Test 2: Fix replicas + port
33
+ config2 = """apiVersion: apps/v1
34
+ kind: Deployment
35
+ metadata:
36
+ name: my-app
37
+ spec:
38
+ replicas: 3
39
+ selector:
40
+ matchLabels:
41
+ app: my-app
42
+ template:
43
+ metadata:
44
+ labels:
45
+ app: my-app
46
+ spec:
47
+ containers:
48
+ - name: my-container
49
+ image: nginx
50
+ ports:
51
+ - containerPort: 80
52
+ resources:
53
+ limits:
54
+ cpu: "500"
55
+ """
56
+
57
+ reward2, msg2, fixed2 = grade_task5(config2)
58
+ print(f'Step 2 (replicas + port fixed): Reward={reward2}, Fixed={fixed2}')
59
+ print(f' Error: {msg2}\n')
60
+
61
+ # Test 3: Fix everything
62
+ config3 = """apiVersion: apps/v1
63
+ kind: Deployment
64
+ metadata:
65
+ name: my-app
66
+ spec:
67
+ replicas: 3
68
+ selector:
69
+ matchLabels:
70
+ app: my-app
71
+ template:
72
+ metadata:
73
+ labels:
74
+ app: my-app
75
+ spec:
76
+ containers:
77
+ - name: my-container
78
+ image: nginx
79
+ ports:
80
+ - containerPort: 80
81
+ resources:
82
+ limits:
83
+ cpu: "500m"
84
+ """
85
+
86
+ reward3, msg3, fixed3 = grade_task5(config3)
87
+ print(f'Step 3 (all fixed): Reward={reward3}, Fixed={fixed3}')
88
+ print(f' Message: {msg3}')
test_nginx_multistep.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from server.graders.nginx_grader import grade_task7
2
+
3
+ # Test 1: Only fix syntax (listen and error_log semicolons)
4
+ config1 = """events {}
5
+
6
+ http {
7
+ server {
8
+ listen 80;
9
+
10
+ location / {
11
+ proxy_pass localhost:3000
12
+ }
13
+
14
+ location /api {
15
+ proxy_pass http://localhost:5000
16
+ }
17
+
18
+ error_log logs/error.log;
19
+ }
20
+ }"""
21
+
22
+ reward1, msg1, fixed1 = grade_task7(config1)
23
+ print(f'Step 1 (syntax fixed): Reward={reward1}, Fixed={fixed1}')
24
+ print(f' Error: {msg1}\n')
25
+
26
+ # Test 2: Fix syntax + proxy_pass (add http://)
27
+ config2 = """events {}
28
+
29
+ http {
30
+ server {
31
+ listen 80;
32
+
33
+ location / {
34
+ proxy_pass http://localhost:3000;
35
+ }
36
+
37
+ location /api {
38
+ proxy_pass http://localhost:5000
39
+ }
40
+
41
+ error_log logs/error.log;
42
+ }
43
+ }"""
44
+
45
+ reward2, msg2, fixed2 = grade_task7(config2)
46
+ print(f'Step 2 (syntax + proxy_pass fixed): Reward={reward2}, Fixed={fixed2}')
47
+ print(f' Error: {msg2}\n')
48
+
49
+ # Test 3: Fix everything (add /api/ and headers)
50
+ config3 = """events {}
51
+
52
+ http {
53
+ server {
54
+ listen 80;
55
+
56
+ location / {
57
+ proxy_pass http://localhost:3000;
58
+ }
59
+
60
+ location /api/ {
61
+ proxy_pass http://localhost:5000;
62
+ proxy_set_header Host $host;
63
+ proxy_set_header X-Real-IP $remote_addr;
64
+ }
65
+
66
+ error_log logs/error.log;
67
+ }
68
+ }"""
69
+
70
+ reward3, msg3, fixed3 = grade_task7(config3)
71
+ print(f'Step 3 (all fixed): Reward={reward3}, Fixed={fixed3}')
72
+ print(f' Message: {msg3}')
uv.lock ADDED
The diff for this file is too large to render. See raw diff