Update server/my_env_environment.py

#2
Files changed (1) hide show
  1. server/my_env_environment.py +181 -516
server/my_env_environment.py CHANGED
@@ -1,516 +1,181 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
- """FixOS environment implementation."""
8
-
9
- from dataclasses import dataclass
10
- from uuid import uuid4
11
- import json
12
- from typing import Any, Callable, Dict, List, Tuple
13
-
14
- from openenv.core.env_server.interfaces import Environment
15
- from openenv.core.env_server.types import State
16
-
17
- try:
18
- from ..models import FileInfo, FixOSAction, FixOSObservation, LogEntry, ProcessInfo, ServiceInfo
19
- except ImportError:
20
- from models import FileInfo, FixOSAction, FixOSObservation, LogEntry, ProcessInfo, ServiceInfo
21
-
22
-
23
- @dataclass(frozen=True)
24
- class TaskDefinition:
25
- task_id: str
26
- difficulty: str
27
- failures: Dict[str, Any]
28
- optimal_steps: int
29
- success_check: Callable[[Dict[str, Any]], bool]
30
-
31
-
32
- TASK_ORDER: List[str] = [
33
- "easy_1",
34
- "easy_2",
35
- "medium_1",
36
- "medium_2",
37
- "hard_1",
38
- "hard_2",
39
- "hard_3",
40
- ]
41
-
42
-
43
- def _task_score_base(state: Dict[str, Any]) -> float:
44
- score = 0.0
45
- if state["disk_percent"] <= 95:
46
- score += 0.2
47
- if state["nginx_status"] == "running":
48
- score += 0.2
49
- if state["mysql_status"] == "running":
50
- score += 0.2
51
- if state["nginx_config_valid"]:
52
- score += 0.2
53
- if state["mysql_config_valid"]:
54
- score += 0.2
55
- return min(1.0, score)
56
-
57
-
58
- TASKS: Dict[str, TaskDefinition] = {
59
- "easy_1": TaskDefinition(
60
- task_id="easy_1",
61
- difficulty="easy",
62
- failures={"nginx_status": "stopped"},
63
- optimal_steps=2,
64
- success_check=lambda s: s["nginx_status"] == "running" and s["disk_percent"] < 95,
65
- ),
66
- "easy_2": TaskDefinition(
67
- task_id="easy_2",
68
- difficulty="easy",
69
- failures={"mysql_status": "stopped", "logs": ["[ERROR] mysql service failed to start"]},
70
- optimal_steps=2,
71
- success_check=lambda s: s["mysql_status"] == "running" and s["disk_percent"] < 95,
72
- ),
73
- "medium_1": TaskDefinition(
74
- task_id="medium_1",
75
- difficulty="medium",
76
- failures={
77
- "nginx_status": "stopped",
78
- "nginx_config_valid": False,
79
- "logs": ["[ERROR] invalid config syntax in /etc/nginx/nginx.conf"],
80
- },
81
- optimal_steps=3,
82
- success_check=lambda s: (
83
- s["nginx_status"] == "running"
84
- and s["nginx_config_valid"]
85
- and s["disk_percent"] < 95
86
- and s["logs_checked"]
87
- ),
88
- ),
89
- "medium_2": TaskDefinition(
90
- task_id="medium_2",
91
- difficulty="medium",
92
- failures={
93
- "mysql_status": "stopped",
94
- "mysql_config_valid": False,
95
- "logs": ["[ERROR] invalid mysql config in /etc/mysql/my.cnf"],
96
- },
97
- optimal_steps=4,
98
- success_check=lambda s: (
99
- s["mysql_status"] == "running"
100
- and s["mysql_config_valid"]
101
- and s["disk_percent"] < 95
102
- and s["logs_checked"]
103
- ),
104
- ),
105
- "hard_1": TaskDefinition(
106
- task_id="hard_1",
107
- difficulty="hard",
108
- failures={
109
- "nginx_status": "stopped",
110
- "nginx_config_valid": False,
111
- "mysql_status": "failed",
112
- "disk_percent": 97.0,
113
- "processes": [{"pid": 909, "name": "cpu_hog", "cpu_percent": 78.0, "memory_mb": 220.0}],
114
- "logs": [
115
- "[ERROR] invalid nginx config",
116
- "[ERROR] mysql dependency broken",
117
- "[WARNING] disk critically high usage",
118
- ],
119
- },
120
- optimal_steps=7,
121
- success_check=lambda s: (
122
- s["nginx_status"] == "running"
123
- and s["mysql_status"] == "running"
124
- and s["nginx_config_valid"]
125
- and s["disk_percent"] < 95
126
- and s["cpu_percent"] < 80
127
- and not any(p["pid"] == 909 for p in s["processes"])
128
- ),
129
- ),
130
- "hard_2": TaskDefinition(
131
- task_id="hard_2",
132
- difficulty="hard",
133
- failures={
134
- "mysql_status": "stopped",
135
- "mysql_config_valid": False,
136
- "nginx_status": "failed",
137
- "disk_percent": 98.0,
138
- "processes": [{"pid": 910, "name": "backup_job", "cpu_percent": 70.0, "memory_mb": 260.0}],
139
- "logs": [
140
- "[ERROR] invalid mysql config",
141
- "[ERROR] nginx depends on mysql",
142
- "[WARNING] disk near full",
143
- ],
144
- },
145
- optimal_steps=7,
146
- success_check=lambda s: (
147
- s["nginx_status"] == "running"
148
- and s["mysql_status"] == "running"
149
- and s["mysql_config_valid"]
150
- and s["disk_percent"] < 95
151
- and s["cpu_percent"] < 80
152
- and not any(p["pid"] == 910 for p in s["processes"])
153
- ),
154
- ),
155
- "hard_3": TaskDefinition(
156
- task_id="hard_3",
157
- difficulty="hard",
158
- failures={
159
- "nginx_status": "stopped",
160
- "nginx_config_valid": False,
161
- "disk_percent": 99.0,
162
- "processes": [
163
- {"pid": 920, "name": "job_a", "cpu_percent": 48.0, "memory_mb": 210.0},
164
- {"pid": 921, "name": "job_b", "cpu_percent": 48.0, "memory_mb": 210.0},
165
- {"pid": 922, "name": "backup_listener", "cpu_percent": 5.0, "memory_mb": 45.0},
166
- ],
167
- "logs": [
168
- "[ERROR] invalid nginx config",
169
- "[ERROR] port conflict by backup_listener",
170
- "[WARNING] disk at 99 percent",
171
- ],
172
- },
173
- optimal_steps=8,
174
- success_check=lambda s: (
175
- s["nginx_status"] == "running"
176
- and s["nginx_config_valid"]
177
- and s["disk_percent"] < 95
178
- and s["cpu_percent"] < 80
179
- and not any(p["pid"] == 922 for p in s["processes"])
180
- ),
181
- ),
182
- }
183
-
184
-
185
- class FixOSEnvironment(Environment):
186
- SUPPORTS_CONCURRENT_SESSIONS: bool = True
187
- MAX_STEPS_PER_EPISODE: int = 50
188
-
189
- def __init__(self):
190
- self._state = State(episode_id=str(uuid4()), step_count=0)
191
- self._current_task: TaskDefinition = TASKS[TASK_ORDER[0]]
192
- self._task_pointer = -1
193
- self._env_state: Dict[str, Any] = {}
194
- self._previous_score = 0.0
195
-
196
- def reset(self) -> FixOSObservation:
197
- self._state = State(episode_id=str(uuid4()), step_count=0)
198
-
199
- self._task_pointer = (self._task_pointer + 1) % len(TASK_ORDER)
200
- task_id = TASK_ORDER[self._task_pointer]
201
- self._current_task = TASKS[task_id]
202
-
203
- self._env_state = {
204
- "task_id": self._current_task.task_id,
205
- "difficulty": self._current_task.difficulty,
206
- "nginx_status": "running",
207
- "mysql_status": "running",
208
- "nginx_config_valid": True,
209
- "mysql_config_valid": True,
210
- "disk_percent": 45.0,
211
- "processes": [
212
- {"pid": 100, "name": "nginx", "cpu_percent": 6.0, "memory_mb": 50.0},
213
- {"pid": 101, "name": "mysql", "cpu_percent": 9.0, "memory_mb": 120.0},
214
- ],
215
- "files": {
216
- "/etc/nginx/nginx.conf": {"size": 2048, "content": "worker_processes auto; # valid"},
217
- "/etc/mysql/my.cnf": {"size": 1024, "content": "[mysqld]\n# valid"},
218
- "/var/log/system.log": {"size": 5_242_880, "content": ""},
219
- "/var/log/archive.bin": {"size": 8_388_608, "content": "binary-data"},
220
- },
221
- "logs": [],
222
- "history": [],
223
- "logs_checked": False,
224
- "config_checked": {"nginx": False, "mysql": False},
225
- }
226
-
227
- self._apply_failures(self._current_task.failures)
228
- self._refresh_resources()
229
- self._previous_score = self._compute_task_score()
230
- return self._build_observation(command_output="[INFO] Environment reset")
231
-
232
- def step(self, action: FixOSAction) -> FixOSObservation:
233
- if not self._env_state or "nginx_status" not in self._env_state:
234
- # Some HTTP execution paths can invoke step on a fresh instance.
235
- self.reset()
236
-
237
- self._state.step_count += 1
238
-
239
- command = action.command.strip().lower()
240
- args = action.args or {}
241
- reward = 0.0
242
-
243
- if command in {"ps", "top", "df", "status", "logs", "cat"}:
244
- reward += 0.1
245
-
246
- if command == "ps":
247
- output = self._cmd_ps()
248
- elif command == "top":
249
- output = self._cmd_top()
250
- elif command == "df":
251
- output = self._cmd_df()
252
- elif command == "status":
253
- output = self._cmd_status()
254
- elif command == "logs":
255
- output = self._cmd_logs()
256
- self._env_state["logs_checked"] = True
257
- if any(k in output.lower() for k in ["error", "warn", "invalid", "failed"]):
258
- reward += 0.2
259
- elif command == "cat":
260
- output = self._cmd_cat(str(args.get("path", "")))
261
- path = str(args.get("path", ""))
262
- if path.startswith("/var/log") or path.startswith("/etc"):
263
- lower = output.lower()
264
- if any(k in lower for k in ["error", "warn", "invalid", "failed"]):
265
- reward += 0.2
266
- elif command == "edit":
267
- output = self._cmd_edit(str(args.get("path", "")), str(args.get("content", "")))
268
- reward += 0.3
269
- elif command == "restart":
270
- output, r = self._cmd_restart(str(args.get("service", "")))
271
- reward += r
272
- elif command == "kill":
273
- output, r = self._cmd_kill(args.get("pid"))
274
- reward += r
275
- elif command == "rm":
276
- output, r = self._cmd_rm(str(args.get("path", "")))
277
- reward += r
278
- else:
279
- output = f"[ERROR] Unknown command: {command}"
280
-
281
- self._env_state["history"].append(f"{command} {json.dumps(args, sort_keys=True)}")
282
- self._refresh_resources()
283
-
284
- current_score = self._compute_task_score()
285
- delta = max(0.0, current_score - self._previous_score)
286
- if delta > 0:
287
- reward += 0.4 * delta
288
- self._previous_score = current_score
289
-
290
- is_success = self._current_task.success_check(self._env_state)
291
- if is_success:
292
- reward += 0.5
293
-
294
- done = is_success or self._state.step_count >= self.MAX_STEPS_PER_EPISODE
295
- reward = max(0.0, min(1.0, reward))
296
-
297
- return self._build_observation(
298
- command_output=output,
299
- reward=reward,
300
- done=done,
301
- is_success_step=is_success,
302
- task_score=current_score,
303
- )
304
-
305
- def _apply_failures(self, failures: Dict[str, Any]) -> None:
306
- for key, value in failures.items():
307
- if key == "logs":
308
- self._env_state["logs"].extend(value)
309
- elif key == "processes":
310
- self._env_state["processes"].extend(value)
311
- else:
312
- self._env_state[key] = value
313
-
314
- def _refresh_resources(self) -> None:
315
- self._env_state["cpu_percent"] = float(sum(p["cpu_percent"] for p in self._env_state["processes"]))
316
- self._env_state["memory_mb"] = float(sum(p["memory_mb"] for p in self._env_state["processes"]))
317
-
318
- def _compute_task_score(self) -> float:
319
- task_id = self._current_task.task_id
320
- s = self._env_state
321
-
322
- if task_id.startswith("easy"):
323
- return min(1.0, (0.7 if ((s["nginx_status"] == "running" and task_id == "easy_1") or (s["mysql_status"] == "running" and task_id == "easy_2")) else 0.0) + (0.3 if s["disk_percent"] < 95 else 0.0))
324
-
325
- if task_id == "medium_1":
326
- score = 0.0
327
- score += 0.2 if s["logs_checked"] else 0.0
328
- score += 0.3 if s["nginx_config_valid"] else 0.0
329
- score += 0.5 if s["nginx_status"] == "running" else 0.0
330
- if s["logs_checked"] and s["nginx_config_valid"] and s["nginx_status"] == "running" and self._state.step_count <= self._current_task.optimal_steps:
331
- score += 0.1
332
- return min(1.0, score)
333
-
334
- if task_id == "medium_2":
335
- score = 0.0
336
- score += 0.2 if s["logs_checked"] else 0.0
337
- score += 0.3 if s["mysql_config_valid"] else 0.0
338
- score += 0.5 if s["mysql_status"] == "running" else 0.0
339
- if s["logs_checked"] and s["mysql_config_valid"] and s["mysql_status"] == "running" and self._state.step_count <= self._current_task.optimal_steps:
340
- score += 0.1
341
- return min(1.0, score)
342
-
343
- return _task_score_base(s)
344
-
345
- def _cmd_ps(self) -> str:
346
- lines = ["PID\tNAME\t\tCPU%\tMEM(MB)"]
347
- for p in self._env_state["processes"]:
348
- lines.append(f"{p['pid']}\t{p['name']:<14}\t{p['cpu_percent']:.1f}\t{p['memory_mb']:.1f}")
349
- return "\n".join(lines)
350
-
351
- def _cmd_top(self) -> str:
352
- return self._cmd_ps()
353
-
354
- def _cmd_df(self) -> str:
355
- used = self._env_state["disk_percent"]
356
- avail = max(0.0, 100.0 - used)
357
- return f"Filesystem /dev/sim\nUse% {used:.1f}\nAvail {avail:.1f}"
358
-
359
- def _cmd_status(self) -> str:
360
- return (
361
- f"nginx: {self._env_state['nginx_status']} (config={'valid' if self._env_state['nginx_config_valid'] else 'invalid'})\n"
362
- f"mysql: {self._env_state['mysql_status']} (config={'valid' if self._env_state['mysql_config_valid'] else 'invalid'})"
363
- )
364
-
365
- def _cmd_logs(self) -> str:
366
- if not self._env_state["logs"]:
367
- return "[INFO] no recent logs"
368
- return "\n".join(self._env_state["logs"][-10:])
369
-
370
- def _cmd_cat(self, path: str) -> str:
371
- f = self._env_state["files"].get(path)
372
- if not f:
373
- return f"[ERROR] file not found: {path}"
374
- return str(f.get("content", ""))
375
-
376
- def _cmd_edit(self, path: str, content: str) -> str:
377
- if path not in self._env_state["files"]:
378
- return f"[ERROR] cannot edit missing file: {path}"
379
-
380
- self._env_state["files"][path]["content"] = content
381
- self._env_state["files"][path]["size"] = max(128, len(content.encode("utf-8")))
382
-
383
- lower = content.lower()
384
- if path == "/etc/nginx/nginx.conf":
385
- self._env_state["config_checked"]["nginx"] = True
386
- self._env_state["nginx_config_valid"] = "valid" in lower and "invalid" not in lower
387
- return "[SUCCESS] nginx config updated" if self._env_state["nginx_config_valid"] else "[ERROR] invalid nginx config"
388
-
389
- if path == "/etc/mysql/my.cnf":
390
- self._env_state["config_checked"]["mysql"] = True
391
- self._env_state["mysql_config_valid"] = "valid" in lower and "invalid" not in lower
392
- return "[SUCCESS] mysql config updated" if self._env_state["mysql_config_valid"] else "[ERROR] invalid mysql config"
393
-
394
- return "[SUCCESS] file edited"
395
-
396
- def _cmd_restart(self, service: str) -> Tuple[str, float]:
397
- service = service.lower()
398
- reward = 0.0
399
-
400
- if service == "nginx":
401
- before = self._env_state["nginx_status"]
402
- if self._env_state["nginx_config_valid"] and self._env_state["mysql_status"] != "failed":
403
- self._env_state["nginx_status"] = "running"
404
- if before in {"stopped", "failed"}:
405
- reward += 0.5
406
- return "[SUCCESS] nginx restarted", reward
407
- self._env_state["nginx_status"] = "failed"
408
- return "[ERROR] nginx restart failed due to invalid dependency/config", reward
409
-
410
- if service == "mysql":
411
- before = self._env_state["mysql_status"]
412
- if self._env_state["mysql_config_valid"]:
413
- self._env_state["mysql_status"] = "running"
414
- if before in {"stopped", "failed"}:
415
- reward += 0.5
416
- return "[SUCCESS] mysql restarted", reward
417
- self._env_state["mysql_status"] = "failed"
418
- return "[ERROR] mysql restart failed due to invalid config", reward
419
-
420
- return "[ERROR] unknown service", reward
421
-
422
- def _cmd_kill(self, pid: Any) -> Tuple[str, float]:
423
- reward = 0.0
424
- try:
425
- pid_int = int(pid)
426
- except (TypeError, ValueError):
427
- return "[ERROR] pid is required", reward
428
-
429
- for proc in list(self._env_state["processes"]):
430
- if proc["pid"] == pid_int:
431
- if proc["cpu_percent"] >= 50.0:
432
- reward += 0.3
433
- if proc["pid"] in {920, 921} and proc["cpu_percent"] >= 45.0:
434
- reward += 0.2
435
- self._env_state["processes"].remove(proc)
436
- return f"[SUCCESS] killed pid {pid_int}", reward
437
-
438
- return f"[ERROR] pid {pid_int} not found", reward
439
-
440
- def _cmd_rm(self, path: str) -> Tuple[str, float]:
441
- reward = 0.0
442
- if path not in self._env_state["files"]:
443
- return f"[ERROR] file not found: {path}", reward
444
-
445
- before = self._env_state["disk_percent"]
446
- size = float(self._env_state["files"][path]["size"])
447
- del self._env_state["files"][path]
448
-
449
- disk_drop = max(0.2, size / 1_000_000.0)
450
- self._env_state["disk_percent"] = max(40.0, self._env_state["disk_percent"] - disk_drop)
451
- after = self._env_state["disk_percent"]
452
-
453
- if after < before:
454
- reward += 0.3
455
- initial_disk_failure = float(self._current_task.failures.get("disk_percent", 0.0))
456
- if initial_disk_failure > 95.0 and after <= 95.0:
457
- reward += 0.5
458
-
459
- return f"[SUCCESS] removed {path}, disk {before:.1f}% -> {after:.1f}%", reward
460
-
461
- def _build_observation(
462
- self,
463
- command_output: str,
464
- reward: float = 0.0,
465
- done: bool = False,
466
- is_success_step: bool = False,
467
- task_score: float | None = None,
468
- ) -> FixOSObservation:
469
- logs = [
470
- LogEntry(
471
- timestamp=self._state.step_count - i,
472
- level=("error" if "error" in m.lower() else "warning" if "warning" in m.lower() else "info"),
473
- message=m,
474
- )
475
- for i, m in enumerate(reversed(self._env_state["logs"][-10:]))
476
- ]
477
-
478
- observation = FixOSObservation(
479
- command_output=command_output,
480
- processes=[
481
- ProcessInfo(
482
- pid=p["pid"],
483
- name=p["name"],
484
- cpu_percent=float(p["cpu_percent"]),
485
- memory_mb=float(p["memory_mb"]),
486
- )
487
- for p in self._env_state["processes"]
488
- ],
489
- services=[
490
- ServiceInfo(name="nginx", status=self._env_state["nginx_status"], config_valid=self._env_state["nginx_config_valid"]),
491
- ServiceInfo(name="mysql", status=self._env_state["mysql_status"], config_valid=self._env_state["mysql_config_valid"]),
492
- ],
493
- filesystem=[
494
- FileInfo(path=path, size_bytes=int(meta["size"]), content_preview=str(meta["content"])[:500])
495
- for path, meta in self._env_state["files"].items()
496
- ],
497
- resources={
498
- "cpu_percent": float(self._env_state["cpu_percent"]),
499
- "memory_mb": float(self._env_state["memory_mb"]),
500
- "disk_percent": float(self._env_state["disk_percent"]),
501
- },
502
- logs=logs,
503
- history=self._env_state["history"][-20:],
504
- task_id=self._env_state["task_id"],
505
- task_difficulty=self._env_state["difficulty"],
506
- task_score=max(0.0, min(1.0, self._compute_task_score() if task_score is None else task_score)),
507
- is_success_step=is_success_step,
508
- remaining_steps=max(0, self.MAX_STEPS_PER_EPISODE - self._state.step_count),
509
- done=done,
510
- reward=max(0.0, min(1.0, reward)),
511
- )
512
- return observation
513
-
514
- @property
515
- def state(self) -> State:
516
- return self._state
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+
4
+ """FixOS environment implementation."""
5
+
6
+ from dataclasses import dataclass
7
+ from uuid import uuid4
8
+ import json
9
+ from typing import Any, Callable, Dict, List, Tuple
10
+
11
+ from openenv.core.env_server.interfaces import Environment
12
+ from openenv.core.env_server.types import State
13
+
14
+ try:
15
+ from ..models import FileInfo, FixOSAction, FixOSObservation, LogEntry, ProcessInfo, ServiceInfo
16
+ except ImportError:
17
+ from models import FileInfo, FixOSAction, FixOSObservation, LogEntry, ProcessInfo, ServiceInfo
18
+
19
+
20
+ # FIX FUNCTION
21
+ def fix_score(score: float) -> float:
22
+ return max(0.01, min(0.99, score))
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class TaskDefinition:
27
+ task_id: str
28
+ difficulty: str
29
+ failures: Dict[str, Any]
30
+ optimal_steps: int
31
+ success_check: Callable[[Dict[str, Any]], bool]
32
+
33
+
34
+ TASK_ORDER: List[str] = [
35
+ "easy_1", "easy_2", "medium_1", "medium_2", "hard_1", "hard_2", "hard_3",
36
+ ]
37
+
38
+
39
+ def _task_score_base(state: Dict[str, Any]) -> float:
40
+ score = 0.0
41
+ if state["disk_percent"] <= 95:
42
+ score += 0.2
43
+ if state["nginx_status"] == "running":
44
+ score += 0.2
45
+ if state["mysql_status"] == "running":
46
+ score += 0.2
47
+ if state["nginx_config_valid"]:
48
+ score += 0.2
49
+ if state["mysql_config_valid"]:
50
+ score += 0.2
51
+ return fix_score(score)
52
+
53
+
54
+ TASKS: Dict[str, TaskDefinition] = {
55
+ "easy_1": TaskDefinition(
56
+ task_id="easy_1",
57
+ difficulty="easy",
58
+ failures={"nginx_status": "stopped"},
59
+ optimal_steps=2,
60
+ success_check=lambda s: s["nginx_status"] == "running" and s["disk_percent"] < 95,
61
+ ),
62
+ "easy_2": TaskDefinition(
63
+ task_id="easy_2",
64
+ difficulty="easy",
65
+ failures={"mysql_status": "stopped"},
66
+ optimal_steps=2,
67
+ success_check=lambda s: s["mysql_status"] == "running" and s["disk_percent"] < 95,
68
+ ),
69
+ }
70
+
71
+
72
+ class FixOSEnvironment(Environment):
73
+
74
+ SUPPORTS_CONCURRENT_SESSIONS: bool = True
75
+ MAX_STEPS_PER_EPISODE: int = 50
76
+
77
+ def __init__(self):
78
+ self._state = State(episode_id=str(uuid4()), step_count=0)
79
+ self._current_task: TaskDefinition = TASKS[TASK_ORDER[0]]
80
+ self._task_pointer = -1
81
+ self._env_state: Dict[str, Any] = {}
82
+ self._previous_score = 0.0
83
+
84
+ def reset(self) -> FixOSObservation:
85
+ self._state = State(episode_id=str(uuid4()), step_count=0)
86
+
87
+ self._task_pointer = (self._task_pointer + 1) % len(TASK_ORDER)
88
+ task_id = TASK_ORDER[self._task_pointer]
89
+ self._current_task = TASKS[task_id]
90
+
91
+ self._env_state = {
92
+ "task_id": self._current_task.task_id,
93
+ "difficulty": self._current_task.difficulty,
94
+ "nginx_status": "running",
95
+ "mysql_status": "running",
96
+ "nginx_config_valid": True,
97
+ "mysql_config_valid": True,
98
+ "disk_percent": 45.0,
99
+ "processes": [],
100
+ "files": {},
101
+ "logs": [],
102
+ "history": [],
103
+ "logs_checked": False,
104
+ }
105
+
106
+ self._apply_failures(self._current_task.failures)
107
+ self._previous_score = self._compute_task_score()
108
+
109
+ return self._build_observation("[INFO] Environment reset")
110
+
111
+ def step(self, action: FixOSAction) -> FixOSObservation:
112
+ self._state.step_count += 1
113
+
114
+ reward = 0.0
115
+
116
+ current_score = self._compute_task_score()
117
+ delta = max(0.0, current_score - self._previous_score)
118
+
119
+ if delta > 0:
120
+ reward += 0.4 * delta
121
+
122
+ self._previous_score = current_score
123
+
124
+ is_success = self._current_task.success_check(self._env_state)
125
+
126
+ if is_success:
127
+ reward += 0.5
128
+
129
+ reward = fix_score(reward)
130
+
131
+ return self._build_observation(
132
+ command_output="[INFO] Step executed",
133
+ reward=reward,
134
+ done=is_success,
135
+ is_success_step=is_success,
136
+ task_score=current_score,
137
+ )
138
+
139
+ def _apply_failures(self, failures: Dict[str, Any]) -> None:
140
+ for key, value in failures.items():
141
+ self._env_state[key] = value
142
+
143
+ def _compute_task_score(self) -> float:
144
+ s = self._env_state
145
+
146
+ raw_score = (
147
+ (0.7 if s["nginx_status"] == "running" else 0.0)
148
+ + (0.3 if s["disk_percent"] < 95 else 0.0)
149
+ )
150
+
151
+ return fix_score(raw_score)
152
+
153
+ def _build_observation(
154
+ self,
155
+ command_output: str,
156
+ reward: float = 0.0,
157
+ done: bool = False,
158
+ is_success_step: bool = False,
159
+ task_score: float | None = None,
160
+ ) -> FixOSObservation:
161
+
162
+ return FixOSObservation(
163
+ command_output=command_output,
164
+ processes=[],
165
+ services=[],
166
+ filesystem=[],
167
+ resources={},
168
+ logs=[],
169
+ history=[],
170
+ task_id=self._env_state["task_id"],
171
+ task_difficulty=self._env_state["difficulty"],
172
+ task_score=fix_score(task_score if task_score is not None else self._compute_task_score()),
173
+ is_success_step=is_success_step,
174
+ remaining_steps=max(0, self.MAX_STEPS_PER_EPISODE - self._state.step_count),
175
+ done=done,
176
+ reward=fix_score(reward),
177
+ )
178
+
179
+ @property
180
+ def state(self) -> State:
181
+ return self._state