jay2219 commited on
Commit
a7bbee7
·
1 Parent(s): 105036a
.dockerignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ .venv
2
+ .env
3
+ __pycache__
4
+ .pytest_cache
5
+ *.pyc
6
+ .git
.gitignore ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ─── SRE Incident Response Environment ───────────────────────────────────────
2
+ # Based on openenv-base for the Meta OpenEnv framework.
3
+ # Optimised for HF Spaces CPU Basic tier (2 vCPUs, 8 GB RAM).
4
+ # ──────────────────────────────────────────────────────────────────────────────
5
+
6
+ FROM python:3.11-slim
7
+
8
+ WORKDIR /app
9
+
10
+ # Install curl for the Docker HEALTHCHECK
11
+ RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
12
+
13
+ # Copy environment code
14
+ COPY . .
15
+
16
+ # Install server dependencies
17
+ RUN pip install --no-cache-dir -r server/requirements.txt
18
+
19
+ # Expose the default OpenEnv port
20
+ EXPOSE 8000
21
+
22
+ # Docker HEALTHCHECK (every 30 s, 5 s timeout, 3 retries)
23
+ HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
24
+ CMD curl -f http://localhost:8000/health || exit 1
25
+
26
+ # Start uvicorn bound to all interfaces
27
+ CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8000"]
LICENSE CHANGED
@@ -1,6 +1,6 @@
1
  MIT License
2
 
3
- Copyright (c) 2026 Jay Prajapati
4
 
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
  of this software and associated documentation files (the "Software"), to deal
 
1
  MIT License
2
 
3
+ Copyright (c) 2026 Jay
4
 
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
  of this software and associated documentation files (the "Software"), to deal
README.md ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🛡️ Sentinel-SRE-OpenEnv: Autonomous Incident Response
2
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/release/python-3110/)
3
+ [![Framework: OpenEnv](https://img.shields.io/badge/Meta Framework-OpenEnv Core-purple.svg)](https://github.com/facebookresearch/openenv)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
5
+ [![Linting: Ruff](https://img.shields.io/badge/Linting-Ruff-orange.svg)](https://github.com/astral-sh/ruff)
6
+
7
+ A production-grade Site Reliability Engineering (SRE) Reinforcement Learning environment built on the **Meta OpenEnv** framework.
8
+
9
+ **Sentinel-SRE** simulates catastrophic infrastructure failures to evaluate LLM agents on their ability to act as autonomous SREs. It features a dense reward system, multiple static incident tiers, and a first-of-its-kind **Dynamic Chaos Generator**.
10
+
11
+ ---
12
+
13
+ ## 🏗️ Architecture
14
+
15
+ The repository adheres to Meta's RFC-004 standard for distributed AI environments.
16
+
17
+ ```mermaid
18
+ graph TD
19
+ A[LLM Inference Agent] -->|JSON Actions| B(FastAPI Server)
20
+ B -->|HTTPEnvServer Validator| C{SRE Environment}
21
+ C -->|Simulated Side Effects| D[State Mutator]
22
+ D -->|Continuous Grades| E(SREGraderRubric)
23
+ E -->|Observations + Metrics| A
24
+ ```
25
+
26
+ ---
27
+
28
+ ## 🔥 Incident Topology (Tiers)
29
+
30
+ The environment challenges the agent with escalating scenarios:
31
+
32
+ | Difficulty | Incident Scenario | Primary Diagnostic Tool | Grader Metric |
33
+ |---|---|---|---|
34
+ | 🟢 **Easy** | **OOMKilled Pod Alert** | `diagnose` | Restored cluster uptime. |
35
+ | 🟡 **Medium** | **High DB Latency (12,000ms)** | `run_sql` | Database latency reduction. |
36
+ | 🔴 **Hard** | **Sudden Traffic Spike (10×)** | `scale_servers` | Time to absorb traffic (Budget: $500). |
37
+ | 💀 **Extreme** | **Bad Code Deployment** | `rollback` | Total resolution step/time. |
38
+ | 🧠 **Dynamic** | **User-Defined SRE Incident** | *Matches Archetype* | *Matches Archetype* |
39
+
40
+ ### 🧠 Featured: Dynamic Chaos Generator (Option 5)
41
+ Leverage an **SRE Incident Router** (LLM) to classify free-text natural language prompts into solvable incident archetypes. Submit any problem (e.g., *"My checkout service is timing out due to slow database queries"*) and the environment will dynamically generate matching logs and descriptions while maintaining mathematical evaluation integrity.
42
+
43
+ ---
44
+
45
+ ## 🚀 Getting Started
46
+
47
+ ### 1. Prerequisites
48
+ We use [`uv`](https://docs.astral.sh/uv/) for incredibly fast dependency management.
49
+ ```bash
50
+ # Install environment & dependencies
51
+ uv sync
52
+ ```
53
+
54
+ ### 2. Local Server Deployment
55
+ Start the OpenEnv FastAPI layer:
56
+ ```bash
57
+ uv run server/app.py
58
+ ```
59
+ *The server is now listening for OpenEnv schema actions on `http://localhost:8000`.*
60
+
61
+ ### 3. Run the Sentinel-SRE Agent
62
+ To test the environment natively, run the interactive inference script:
63
+ ```bash
64
+ # Set your API key
65
+ export HF_TOKEN="your_key"
66
+
67
+ # Execute the agent loop
68
+ uv run sre_env/inference.py
69
+ ```
70
+
71
+ ---
72
+
73
+ ## 🛠️ Testing & Linting
74
+
75
+ ### Automated Validation (Tests)
76
+ Ensure the environment math is sound before submission:
77
+ ```bash
78
+ # Execute the core test suite (Windows PowerShell)
79
+ $env:PYTHONPATH="."; uv run pytest sre_env/tests/
80
+
81
+ # Execute the core test suite (Linux/MacOS)
82
+ PYTHONPATH=. uv run pytest sre_env/tests/
83
+ ```
84
+
85
+ ### Quality Assurance (Linting)
86
+ Sentinel-SRE is built with industrial-grade linting standards:
87
+ ```bash
88
+ # Scan for errors
89
+ uv run ruff check .
90
+
91
+ # Auto-format codebase
92
+ uv run ruff format .
93
+ ```
94
+
95
+ ---
96
+
97
+ ## ⚖️ Evaluation & Grading
98
+ The `SREGraderRubric` computes continuous floats `[0.0, 1.0]` based on:
99
+ 1. **Resolution Speed:** Rewards the fastest fix.
100
+ 2. **Budget Efficiency:** Penalties for wasteful cloud spending.
101
+ 3. **Collateral Damage:** Severe penalties for terminating healthy services or executing rogue production SQL without cause.
102
+
103
+ Built for the 2026 OpenEnv AI Hackathon.
openenv.yaml ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ─── SRE Incident Response Environment — OpenEnv Manifest ─────────────────────
2
+ # Targets Hugging Face Spaces "CPU Basic" tier (2 vCPUs, 8 GB RAM).
3
+ # ──────────────────────────────────────────────────────────────────────────────
4
+
5
+ name: sentinel-sre-agent
6
+ version: "1.0.0"
7
+ description: >
8
+ Autonomous SRE incident response RL environment.
9
+ Simulates three stratified scenarios — pod crash recovery (easy),
10
+ database index optimisation (medium), and dynamic server scaling under
11
+ a strict budget (hard) — with proportional grading and dense rewards.
12
+
13
+ hardware:
14
+ tier: cpu-basic
15
+ max_vcpus: 2
16
+ max_ram_gb: 8
17
+
18
+ server:
19
+ port: 8000
20
+ dockerfile: Dockerfile
21
+
22
+ tasks:
23
+ - name: pod-restart
24
+ difficulty: easy
25
+ description: Parse alert logs and restart the failing pod to restore uptime.
26
+
27
+ - name: db-index-optimisation
28
+ difficulty: medium
29
+ description: Query slow-log, identify missing index, and execute optimisation SQL.
30
+
31
+ - name: dynamic-scaling
32
+ difficulty: hard
33
+ description: Provision servers to absorb 10× traffic spike within a $500 budget.
34
+
35
+ - name: bad-deployment-rollback
36
+ difficulty: extreme
37
+ description: Diagnose cascading 500 errors from auth-service, check deployment logs, and execute a rollback to the stable revision.
pyproject.toml ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "sentinel-sre"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "MIT"
8
+ dependencies = [
9
+ "huggingface-hub>=1.8.0",
10
+ "openenv-core>=0.2.2",
11
+ "fastapi>=0.100.0",
12
+ "uvicorn>=0.22.0",
13
+ "requests>=2.31.0",
14
+ "rich>=13.0.0",
15
+ "python-dotenv>=1.0.0",
16
+ ]
17
+
18
+ [tool.uv]
19
+ package = false
20
+
21
+ [dependency-groups]
22
+ dev = [
23
+ "ruff>=0.15.9",
24
+ "pytest>=8.0.0",
25
+ ]
26
+
27
+ [tool.ruff]
28
+ line-length = 120
29
+ target-version = "py311"
30
+
31
+ [tool.ruff.lint]
32
+ select = ["E", "F", "I"]
33
+ ignore = ["E501"] # Ignore long lines for SRE logs and prompts
server/api/routes/environment.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+ from openenv.core.env_server.http_server import StepResponse
3
+
4
+ from server.core.deps import env_instance
5
+ from server.schemas.reset import CustomResetRequest
6
+
7
+ router = APIRouter()
8
+
9
+
10
+ @router.post("/custom_reset")
11
+ def custom_reset(payload: CustomResetRequest):
12
+ """Custom endpoint to inject dynamically generated incident contexts."""
13
+ obs = env_instance.reset(seed=payload.seed)
14
+
15
+ # Override the static archetype text with the LLM's dynamic flavor
16
+ env_instance._state.task_description = payload.custom_description
17
+
18
+ # Refresh observation to reflect overrides
19
+ obs = env_instance._make_observation(
20
+ message=f"🚨 Custom Incident Registered! {payload.custom_description}", logs=payload.custom_logs, success=True
21
+ )
22
+
23
+ return StepResponse(observation=obs.model_dump(), reward=obs.reward, done=obs.done)
server/app.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uvicorn
2
+ from fastapi import FastAPI
3
+ from fastapi.responses import JSONResponse
4
+
5
+ from server.api.routes import environment
6
+ from server.core.deps import env_server
7
+
8
+ app = FastAPI(
9
+ title="SRE Incident Response Environment",
10
+ description="OpenEnv RL environment simulating autonomous SRE incident response.",
11
+ version="1.0.0",
12
+ )
13
+
14
+ # Standard endpoints (OpenEnv framework)
15
+ env_server.register_routes(app)
16
+
17
+ # Custom extensions
18
+ app.include_router(environment.router, tags=["SRE Chaos"])
19
+
20
+
21
+ @app.get("/health", tags=["Utilities"])
22
+ async def health() -> JSONResponse:
23
+ """Return 200 OK for container health checks."""
24
+ return JSONResponse(content={"status": "ok"}, status_code=200)
25
+
26
+
27
+ if __name__ == "__main__":
28
+ uvicorn.run("server.app:app", host="0.0.0.0", port=8000)
server/core/deps.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from openenv.core.env_server.http_server import HTTPEnvServer
2
+
3
+ from server.environment import SREEnvironment
4
+ from sre_env.models import SREAction, SREObservation
5
+
6
+ env_instance = SREEnvironment()
7
+
8
+ env_server = HTTPEnvServer(
9
+ env=lambda: env_instance,
10
+ action_cls=SREAction,
11
+ observation_cls=SREObservation,
12
+ )
server/environment.py ADDED
@@ -0,0 +1,769 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import random
4
+ import uuid
5
+ from typing import Any, Optional
6
+
7
+ from openenv.core.env_server.interfaces import Environment
8
+
9
+ from server.rubrics import SREGraderRubric
10
+ from sre_env.models import (
11
+ CommandType,
12
+ SREAction,
13
+ SREObservation,
14
+ SREState,
15
+ SystemMetrics,
16
+ TaskDifficulty,
17
+ )
18
+
19
+ TASK_CONFIGS = {
20
+ TaskDifficulty.EASY: {
21
+ "description": (
22
+ "INCIDENT: Pod 'pod-web-3' in the 'production' namespace is crash-looping. "
23
+ "Alerts indicate OOMKilled errors. Parse the alert logs, identify the failing "
24
+ "pod, and execute a restart to restore service uptime."
25
+ ),
26
+ "max_steps": 15,
27
+ "failing_pod": "pod-web-3",
28
+ "initial_uptime": 0.45,
29
+ "budget": 500.0,
30
+ },
31
+ TaskDifficulty.MEDIUM: {
32
+ "description": (
33
+ "INCIDENT: The 'orders' API is responding with P99 latency of 12,000 ms "
34
+ "(threshold: 200 ms). The slow-query log points to a full table scan on "
35
+ "'orders_table'. Diagnose the issue, find the missing index, and execute "
36
+ "the optimisation SQL command."
37
+ ),
38
+ "max_steps": 20,
39
+ "slow_table": "orders_table",
40
+ "missing_index_column": "customer_id",
41
+ "initial_latency_ms": 12000.0,
42
+ "target_latency_ms": 200.0,
43
+ "initial_uptime": 0.70,
44
+ "budget": 500.0,
45
+ },
46
+ TaskDifficulty.HARD: {
47
+ "description": (
48
+ "INCIDENT: Traffic spike detected - incoming requests jumped 10× to "
49
+ "50,000 RPS. Current capacity can handle 5,000 RPS. You must dynamically "
50
+ "scale servers to absorb the load WITHOUT exceeding a strict $500 cloud "
51
+ "budget. Each additional server costs $50/unit and adds 5,000 RPS capacity."
52
+ ),
53
+ "max_steps": 30,
54
+ "current_rps": 50000,
55
+ "capacity_per_server": 5000,
56
+ "cost_per_server": 50.0,
57
+ "initial_servers": 1,
58
+ "initial_uptime": 0.10,
59
+ "budget": 500.0,
60
+ },
61
+ TaskDifficulty.EXTREME: {
62
+ "description": (
63
+ "INCIDENT: A new deployment of 'auth-service' has resulted in "
64
+ "cascading 500 errors and user lockouts. Diagnose the issue, check "
65
+ "the deployment logs, and execute a rollback to the stable revision."
66
+ ),
67
+ "max_steps": 25,
68
+ "buggy_deployment": "auth-service",
69
+ "stable_revision": "v1.4.2",
70
+ "initial_uptime": 0.05,
71
+ "budget": 500.0,
72
+ },
73
+ }
74
+
75
+
76
+ class SREEnvironment(Environment[SREAction, SREObservation, SREState]):
77
+ """Autonomous SRE incident-response RL environment.
78
+
79
+ Supports three stratified tasks:
80
+ • Easy - restart a crash-looping pod
81
+ • Medium - find and fix a missing DB index
82
+ • Hard - scale servers under budget constraints
83
+ """
84
+
85
+ def __init__(self, **kwargs: Any) -> None:
86
+ super().__init__(**kwargs)
87
+ self._state = SREState()
88
+ self._rng = random.Random()
89
+ self.rubric = SREGraderRubric(self)
90
+
91
+ # Internal simulation variables (not exposed in State)
92
+ self._pod_restarted: bool = False
93
+ self._index_added: bool = False
94
+ self._servers_added: int = 0
95
+ self._diagnosed: bool = False
96
+ self._diagnosis_target: str = ""
97
+ self._current_latency_ms: float = 0.0
98
+ self._current_servers: int = 1
99
+ self._current_capacity_rps: int = 5000
100
+ self._catastrophic: bool = False
101
+ self._logs_checked: bool = False
102
+ self._rolled_back: bool = False
103
+
104
+ def reset(
105
+ self,
106
+ seed: Optional[int] = None,
107
+ episode_id: Optional[str] = None,
108
+ **kwargs: Any,
109
+ ) -> SREObservation:
110
+ """Reset the environment and select a new incident scenario."""
111
+
112
+ if seed is not None:
113
+ self._rng = random.Random(seed)
114
+ else:
115
+ self._rng = random.Random()
116
+
117
+ difficulty = self._rng.choice(list(TaskDifficulty))
118
+ config = TASK_CONFIGS[difficulty]
119
+
120
+ self._state = SREState(
121
+ episode_id=episode_id or str(uuid.uuid4()),
122
+ step_count=0,
123
+ task_difficulty=difficulty,
124
+ task_description=config["description"],
125
+ current_uptime=config["initial_uptime"],
126
+ budget_remaining=config["budget"],
127
+ max_steps=config["max_steps"],
128
+ incident_resolved=False,
129
+ root_cause_found=False,
130
+ total_reward=0.0,
131
+ )
132
+
133
+ # Reset internal sim variables
134
+ self._pod_restarted = False
135
+ self._index_added = False
136
+ self._servers_added = 0
137
+ self._diagnosed = False
138
+ self._diagnosis_target = ""
139
+ self._catastrophic = False
140
+ self._logs_checked = False
141
+ self._rolled_back = False
142
+
143
+ if difficulty == TaskDifficulty.MEDIUM:
144
+ self._current_latency_ms = config["initial_latency_ms"]
145
+ else:
146
+ self._current_latency_ms = 50.0 # healthy baseline
147
+
148
+ if difficulty == TaskDifficulty.HARD:
149
+ self._current_servers = config["initial_servers"]
150
+ self._current_capacity_rps = self._current_servers * config["capacity_per_server"]
151
+ else:
152
+ self._current_servers = 1
153
+ self._current_capacity_rps = 5000
154
+
155
+ return self._make_observation(
156
+ message=f"🚨 New incident assigned ({difficulty.value} difficulty). {config['description']}",
157
+ logs=self._generate_initial_logs(difficulty),
158
+ success=True,
159
+ )
160
+
161
+ def step(
162
+ self,
163
+ action: SREAction,
164
+ timeout_s: Optional[float] = None,
165
+ **kwargs: Any,
166
+ ) -> SREObservation:
167
+ """Execute one SRE action and return observation + dense reward."""
168
+
169
+ self._state.step_count += 1
170
+ difficulty = self._state.task_difficulty
171
+
172
+ # ── compute reward components ────────────────────────────────
173
+ progress_reward = 0.0
174
+ constraint_penalty = 0.0
175
+ time_penalty = -0.05 # constant per-step cost
176
+
177
+ message = ""
178
+ logs: list[str] = []
179
+ success = False
180
+
181
+ # ── validate action against dynamic rules ─────────────────────
182
+ valid_actions = self._get_available_actions()
183
+ if action.command_type not in valid_actions:
184
+ self._state.total_reward += time_penalty - 0.1
185
+ return self._make_observation(
186
+ message=f"❌ Action '{action.command_type}' is invalid or not currently available. Available: {', '.join(valid_actions)}",
187
+ logs=["[ERROR] Action rejected by environment schema."],
188
+ success=False,
189
+ reward=time_penalty - 0.1,
190
+ done=False,
191
+ )
192
+
193
+ cmd = CommandType(action.command_type)
194
+
195
+ if difficulty == TaskDifficulty.EASY:
196
+ progress_reward, message, logs, success = self._step_easy(action, cmd)
197
+ elif difficulty == TaskDifficulty.MEDIUM:
198
+ progress_reward, message, logs, success = self._step_medium(action, cmd)
199
+ elif difficulty == TaskDifficulty.HARD:
200
+ progress_reward, message, logs, success, constraint_penalty = self._step_hard(action, cmd)
201
+ elif difficulty == TaskDifficulty.EXTREME:
202
+ progress_reward, message, logs, success, constraint_penalty = self._step_extreme(action, cmd)
203
+
204
+ if cmd == CommandType.RESTART_POD and difficulty != TaskDifficulty.EASY:
205
+ constraint_penalty += -0.8
206
+ message += " ⚠️ Restarting a healthy pod caused a service disruption!"
207
+ self._state.current_uptime = max(0.0, self._state.current_uptime - 0.15)
208
+ self._catastrophic = True
209
+
210
+ if cmd == CommandType.RUN_SQL and difficulty != TaskDifficulty.MEDIUM and action.target_resource != "":
211
+ constraint_penalty += -0.4
212
+ message += " ⚠️ Running SQL on a production database without cause is risky."
213
+
214
+ step_reward = progress_reward + time_penalty + constraint_penalty
215
+ self._state.total_reward += step_reward
216
+
217
+ done = False
218
+ if self._state.incident_resolved:
219
+ done = True
220
+ message += " ✅ Incident fully resolved!"
221
+ elif self._state.step_count >= self._state.max_steps:
222
+ done = True
223
+ message += " ⏰ Max steps reached - episode terminated."
224
+ elif self._state.budget_remaining <= 0:
225
+ done = True
226
+ message += " 💸 Budget exhausted - episode terminated."
227
+ elif self._catastrophic and self._state.current_uptime <= 0.0:
228
+ done = True
229
+ message += " 💀 Catastrophic failure - all services down."
230
+
231
+ return self._make_observation(
232
+ message=message,
233
+ logs=logs,
234
+ success=success,
235
+ reward=step_reward,
236
+ done=done,
237
+ )
238
+
239
+ @property
240
+ def state(self) -> SREState:
241
+ """Return the current internal state."""
242
+ return self._state
243
+
244
+ def _step_easy(self, action: SREAction, cmd: CommandType) -> tuple[float, str, list[str], bool]:
245
+ """Easy: restart the correct crash-looping pod."""
246
+ config = TASK_CONFIGS[TaskDifficulty.EASY]
247
+ reward = 0.0
248
+ logs: list[str] = []
249
+
250
+ if cmd == CommandType.DIAGNOSE:
251
+ if not self._diagnosed:
252
+ self._diagnosed = True
253
+ self._diagnosis_target = action.target_resource
254
+ reward = 0.15
255
+ logs = [
256
+ "[DIAG] Scanning pods in 'production' namespace...",
257
+ "[DIAG] pod-web-1: Running (healthy)",
258
+ "[DIAG] pod-web-2: Running (healthy)",
259
+ "[DIAG] pod-web-3: CrashLoopBackOff (OOMKilled, 12 restarts)",
260
+ "[DIAG] pod-web-4: Running (healthy)",
261
+ "[DIAG] Root cause: pod-web-3 exceeding memory limits.",
262
+ ]
263
+ self._state.root_cause_found = True
264
+ return (
265
+ reward,
266
+ "Diagnosis complete. pod-web-3 identified as failing.",
267
+ logs,
268
+ True,
269
+ )
270
+ else:
271
+ return (
272
+ 0.0,
273
+ "Already diagnosed. Consider taking corrective action.",
274
+ logs,
275
+ False,
276
+ )
277
+
278
+ elif cmd == CommandType.RESTART_POD:
279
+ if action.target_resource == config["failing_pod"]:
280
+ self._pod_restarted = True
281
+ self._state.current_uptime = min(1.0, self._state.current_uptime + 0.50)
282
+ if self._diagnosed:
283
+ reward = 0.40
284
+ else:
285
+ reward = 0.25 # less reward if you skipped diagnosis
286
+ if self._state.current_uptime >= 0.95:
287
+ self._state.incident_resolved = True
288
+ logs = [
289
+ "[K8S] kubectl rollout restart deployment/web -n production",
290
+ "[K8S] pod-web-3 terminated.",
291
+ "[K8S] pod-web-3-new: Running (healthy)",
292
+ f"[METRICS] Uptime restored to {self._state.current_uptime:.0%}",
293
+ ]
294
+ return (
295
+ reward,
296
+ f"Pod '{action.target_resource}' restarted successfully.",
297
+ logs,
298
+ True,
299
+ )
300
+ else:
301
+ reward = -0.3
302
+ logs = [
303
+ f"[K8S] pod '{action.target_resource}' not found or already healthy.",
304
+ ]
305
+ return (
306
+ reward,
307
+ f"Wrong pod targeted. '{action.target_resource}' is not the failing pod.",
308
+ logs,
309
+ False,
310
+ )
311
+
312
+ elif cmd == CommandType.NOOP:
313
+ self._state.current_uptime = max(0.0, self._state.current_uptime - 0.03)
314
+ logs = [f"[METRICS] Uptime degraded to {self._state.current_uptime:.0%} (no action taken)."]
315
+ return 0.0, "No action taken. Service continues to degrade.", logs, False
316
+
317
+ else:
318
+ return (
319
+ 0.0,
320
+ f"Command '{action.command_type.value}' is not effective for this incident.",
321
+ [],
322
+ False,
323
+ )
324
+
325
+ def _step_medium(self, action: SREAction, cmd: CommandType) -> tuple[float, str, list[str], bool]:
326
+ """Medium: find missing DB index and execute optimisation SQL."""
327
+ config = TASK_CONFIGS[TaskDifficulty.MEDIUM]
328
+ reward = 0.0
329
+ logs: list[str] = []
330
+
331
+ if cmd == CommandType.DIAGNOSE:
332
+ if not self._diagnosed:
333
+ self._diagnosed = True
334
+ reward = 0.15
335
+ logs = [
336
+ "[DB] Querying slow_log for queries > 1000ms...",
337
+ "[DB] SELECT * FROM orders_table WHERE customer_id = ? - avg 11,800 ms (FULL TABLE SCAN)",
338
+ "[DB] EXPLAIN shows: type=ALL, rows=2,400,000, no index on 'customer_id'",
339
+ "[DB] Recommendation: CREATE INDEX idx_customer_id ON orders_table(customer_id)",
340
+ ]
341
+ self._state.root_cause_found = True
342
+ return (
343
+ reward,
344
+ "Slow query identified. Missing index on orders_table.customer_id.",
345
+ logs,
346
+ True,
347
+ )
348
+ else:
349
+ return 0.0, "Already diagnosed. Apply the SQL optimisation.", [], False
350
+
351
+ elif cmd == CommandType.RUN_SQL:
352
+ target = action.target_resource.lower()
353
+ sql_param = action.parameters.get("sql", "").lower()
354
+
355
+ # Check if the agent is creating the right index
356
+ is_correct_table = config["slow_table"] in target or config["slow_table"] in sql_param
357
+ is_correct_column = config["missing_index_column"] in sql_param or config["missing_index_column"] in target
358
+ has_create_index = "create index" in sql_param or "create index" in target
359
+
360
+ if has_create_index and is_correct_table and is_correct_column:
361
+ self._index_added = True
362
+ old_latency = self._current_latency_ms
363
+ self._current_latency_ms = 45.0 # excellent after indexing
364
+ self._state.current_uptime = min(1.0, self._state.current_uptime + 0.30)
365
+
366
+ if self._diagnosed:
367
+ reward = 0.40
368
+ else:
369
+ reward = 0.25
370
+
371
+ if self._current_latency_ms <= config["target_latency_ms"]:
372
+ self._state.incident_resolved = True
373
+
374
+ logs = [
375
+ "[DB] Executing: CREATE INDEX idx_customer_id ON orders_table(customer_id);",
376
+ "[DB] Index created successfully. Build time: 12.3s",
377
+ f"[DB] P99 latency dropped from {old_latency:.0f}ms to {self._current_latency_ms:.0f}ms",
378
+ f"[METRICS] Uptime improved to {self._state.current_uptime:.0%}",
379
+ ]
380
+ return (
381
+ reward,
382
+ "Index created. Query performance dramatically improved.",
383
+ logs,
384
+ True,
385
+ )
386
+
387
+ elif has_create_index and is_correct_table and not is_correct_column:
388
+ # Partial credit - right table, wrong column
389
+ self._current_latency_ms *= 0.6
390
+ self._state.current_uptime = min(1.0, self._state.current_uptime + 0.10)
391
+ reward = 0.10
392
+ logs = [
393
+ "[DB] Index created on different column. Partial improvement.",
394
+ f"[DB] P99 latency: {self._current_latency_ms:.0f}ms (still above threshold).",
395
+ ]
396
+ return (
397
+ reward,
398
+ "Index created but on wrong column. Partial improvement.",
399
+ logs,
400
+ True,
401
+ )
402
+
403
+ else:
404
+ reward = -0.1
405
+ logs = ["[DB] SQL executed but had no meaningful effect on performance."]
406
+ return (
407
+ reward,
408
+ "SQL command did not address the root cause.",
409
+ logs,
410
+ False,
411
+ )
412
+
413
+ elif cmd == CommandType.NOOP:
414
+ self._current_latency_ms *= 1.05 # latency slowly worsens
415
+ self._state.current_uptime = max(0.0, self._state.current_uptime - 0.02)
416
+ logs = [f"[METRICS] Latency: {self._current_latency_ms:.0f}ms. Uptime: {self._state.current_uptime:.0%}."]
417
+ return (
418
+ 0.0,
419
+ "No action taken. System performance continues to degrade.",
420
+ logs,
421
+ False,
422
+ )
423
+
424
+ else:
425
+ return (
426
+ 0.0,
427
+ f"Command '{action.command_type.value}' is not relevant to this DB incident.",
428
+ [],
429
+ False,
430
+ )
431
+
432
+ def _step_hard(self, action: SREAction, cmd: CommandType) -> tuple[float, str, list[str], bool, float]:
433
+ """Hard: scale servers under budget to handle traffic spike."""
434
+ config = TASK_CONFIGS[TaskDifficulty.HARD]
435
+ reward = 0.0
436
+ constraint_penalty = 0.0
437
+ logs: list[str] = []
438
+
439
+ if cmd == CommandType.DIAGNOSE:
440
+ if not self._diagnosed:
441
+ self._diagnosed = True
442
+ reward = 0.10
443
+ needed = config["current_rps"] - self._current_capacity_rps
444
+ servers_needed = max(1, needed // config["capacity_per_server"])
445
+ cost_estimate = servers_needed * config["cost_per_server"]
446
+ logs = [
447
+ f"[LB] Current traffic: {config['current_rps']:,} RPS",
448
+ f"[LB] Current capacity: {self._current_capacity_rps:,} RPS ({self._current_servers} servers)",
449
+ f"[LB] Deficit: {needed:,} RPS",
450
+ f"[LB] Estimated servers needed: {servers_needed} (${cost_estimate:.0f} total)",
451
+ f"[BUDGET] Remaining: ${self._state.budget_remaining:.0f}",
452
+ ]
453
+ self._state.root_cause_found = True
454
+ return (
455
+ reward,
456
+ "Traffic analysis complete. Scaling plan identified.",
457
+ logs,
458
+ True,
459
+ 0.0,
460
+ )
461
+ else:
462
+ return 0.0, "Already diagnosed. Proceed with scaling.", [], False, 0.0
463
+
464
+ elif cmd == CommandType.SCALE_SERVERS:
465
+ replicas = action.parameters.get("replicas", 1)
466
+ try:
467
+ replicas = int(replicas)
468
+ except (ValueError, TypeError):
469
+ replicas = 1
470
+
471
+ replicas = max(1, min(replicas, 20)) # clamp to sane range
472
+ cost = replicas * config["cost_per_server"]
473
+
474
+ # Check budget
475
+ if cost > self._state.budget_remaining:
476
+ constraint_penalty = -0.8
477
+ self._state.budget_remaining -= cost # negative budget
478
+ logs = [
479
+ f"[CLOUD] Provisioned {replicas} servers at ${cost:.0f}.",
480
+ f"[BUDGET] ⚠️ EXCEEDED! Remaining: ${self._state.budget_remaining:.0f}",
481
+ ]
482
+ return (
483
+ 0.0,
484
+ f"Budget exceeded! Over by ${abs(self._state.budget_remaining):.0f}.",
485
+ logs,
486
+ False,
487
+ constraint_penalty,
488
+ )
489
+
490
+ # Provision servers
491
+ self._state.budget_remaining -= cost
492
+ self._current_servers += replicas
493
+ self._servers_added += replicas
494
+ self._current_capacity_rps = self._current_servers * config["capacity_per_server"]
495
+
496
+ # Update uptime based on capacity vs demand
497
+ capacity_ratio = min(1.0, self._current_capacity_rps / config["current_rps"])
498
+ self._state.current_uptime = capacity_ratio
499
+
500
+ # Proportional reward based on how much capacity gap is closed
501
+ reward = 0.05 + 0.35 * capacity_ratio
502
+ if self._diagnosed:
503
+ reward += 0.05
504
+
505
+ if capacity_ratio >= 1.0:
506
+ self._state.incident_resolved = True
507
+
508
+ logs = [
509
+ f"[CLOUD] Provisioned {replicas} servers. Total: {self._current_servers}.",
510
+ f"[CLOUD] Capacity: {self._current_capacity_rps:,} / {config['current_rps']:,} RPS ({capacity_ratio:.0%})",
511
+ f"[BUDGET] Spent: ${cost:.0f}. Remaining: ${self._state.budget_remaining:.0f}.",
512
+ f"[METRICS] Uptime: {self._state.current_uptime:.0%}",
513
+ ]
514
+ return (
515
+ reward,
516
+ f"Scaled by {replicas} servers. Capacity at {capacity_ratio:.0%}.",
517
+ logs,
518
+ True,
519
+ 0.0,
520
+ )
521
+
522
+ elif cmd == CommandType.NOOP:
523
+ # Uptime drops further with no action
524
+ self._state.current_uptime = max(0.0, self._state.current_uptime - 0.05)
525
+ logs = [
526
+ f"[METRICS] Uptime: {self._state.current_uptime:.0%} (degrading).",
527
+ f"[LB] Dropping {max(0, config['current_rps'] - self._current_capacity_rps):,} requests.",
528
+ ]
529
+ return 0.0, "No action. Requests being dropped.", logs, False, 0.0
530
+
531
+ else:
532
+ return (
533
+ 0.0,
534
+ f"Command '{action.command_type.value}' is not relevant to the scaling incident.",
535
+ [],
536
+ False,
537
+ 0.0,
538
+ )
539
+
540
+ def _step_extreme(self, action: SREAction, cmd: CommandType) -> tuple[float, str, list[str], bool, float]:
541
+ """Extreme: Rollback a bad deployment based on logs."""
542
+ config = TASK_CONFIGS[TaskDifficulty.EXTREME]
543
+ reward = 0.0
544
+ constraint_penalty = 0.0
545
+ logs: list[str] = []
546
+
547
+ if cmd == CommandType.DIAGNOSE:
548
+ if not self._diagnosed:
549
+ self._diagnosed = True
550
+ reward = 0.15
551
+ logs = [
552
+ "[ALERT] auth-service returning 500 Internal Server Error for 98% of requests.",
553
+ "[DIAG] CPU/Mem healthy. Network healthy.",
554
+ "[DIAG] Root cause appears to be a recent code deployment in auth-service.",
555
+ "[HINT] Use 'check_logs' on 'auth-service' to identify the failing revision.",
556
+ ]
557
+ self._state.root_cause_found = True
558
+ return (
559
+ reward,
560
+ "Diagnosis complete. auth-service deployment is failing.",
561
+ logs,
562
+ True,
563
+ 0.0,
564
+ )
565
+ return (
566
+ 0.0,
567
+ "Already diagnosed. Proceed to check deployment logs.",
568
+ [],
569
+ False,
570
+ 0.0,
571
+ )
572
+
573
+ elif cmd == CommandType.CHECK_LOGS:
574
+ if action.target_resource == config["buggy_deployment"]:
575
+ self._logs_checked = True
576
+ reward = 0.15
577
+ logs = [
578
+ "[LOGS] Tail of auth-service deployment history:",
579
+ " Revision 3 (v1.4.3) - FAILED (NullPointerException in AuthProvider)",
580
+ " Revision 2 (v1.4.2) - STABLE (active 3 days ago)",
581
+ " Revision 1 (v1.4.1) - STABLE",
582
+ "[HINT] Use 'rollback' on 'auth-service' with parameter {'revision': 'v1.4.2'}",
583
+ ]
584
+ return (
585
+ reward,
586
+ "Deployment logs retrieved successfully.",
587
+ logs,
588
+ True,
589
+ 0.0,
590
+ )
591
+ return (
592
+ -0.1,
593
+ f"No deployment history found for {action.target_resource}",
594
+ [],
595
+ False,
596
+ 0.0,
597
+ )
598
+
599
+ elif cmd == CommandType.ROLLBACK:
600
+ target = action.target_resource
601
+ revision = action.parameters.get("revision", "")
602
+
603
+ if target != config["buggy_deployment"]:
604
+ return (
605
+ -0.3,
606
+ f"Cannot rollback {target}. It is not the source of the incident.",
607
+ [],
608
+ False,
609
+ 0.0,
610
+ )
611
+
612
+ if revision != config["stable_revision"]:
613
+ constraint_penalty = -0.5
614
+ self._catastrophic = True
615
+ self._state.current_uptime = 0.0
616
+ return (
617
+ 0.0,
618
+ f"Rolled back to bad revision '{revision}'! Complete outage.",
619
+ ["[FATAL] Rollback failed. No service startup."],
620
+ False,
621
+ constraint_penalty,
622
+ )
623
+
624
+ # Success path
625
+ self._rolled_back = True
626
+ self._state.current_uptime = min(1.0, self._state.current_uptime + 0.90)
627
+ reward = 0.50
628
+ if self._state.current_uptime >= 0.95:
629
+ self._state.incident_resolved = True
630
+
631
+ logs = [
632
+ "[K8S] kubectl rollout undo deployment/auth-service --to-revision=2",
633
+ "[K8S] auth-service rolled back to v1.4.2",
634
+ f"[METRICS] Uptime recovered to {self._state.current_uptime:.0%}",
635
+ ]
636
+ return (
637
+ reward,
638
+ f"Successfully rolled back to {revision}. Service restored.",
639
+ logs,
640
+ True,
641
+ 0.0,
642
+ )
643
+
644
+ elif cmd == CommandType.NOOP:
645
+ self._state.current_uptime = max(0.0, self._state.current_uptime - 0.05)
646
+ logs = [f"[METRICS] Users continually locked out. Uptime {self._state.current_uptime:.0%}"]
647
+ return 0.0, "No action. Outage continues.", logs, False, 0.0
648
+
649
+ return (
650
+ 0.0,
651
+ f"Command '{action.command_type.value}' doesn't help with a bad deployment.",
652
+ [],
653
+ False,
654
+ 0.0,
655
+ )
656
+
657
+ def _make_observation(
658
+ self,
659
+ message: str,
660
+ logs: list[str],
661
+ success: bool,
662
+ reward: float | None = None,
663
+ done: bool = False,
664
+ ) -> SREObservation:
665
+ """Build a full SREObservation with current metrics."""
666
+ config = TASK_CONFIGS[self._state.task_difficulty]
667
+
668
+ metrics = SystemMetrics(
669
+ cpu_percent=self._rng.uniform(30, 85),
670
+ memory_percent=self._rng.uniform(40, 90),
671
+ latency_ms=self._current_latency_ms,
672
+ uptime=self._state.current_uptime,
673
+ error_rate=max(0.0, 1.0 - self._state.current_uptime),
674
+ budget_used=config["budget"] - self._state.budget_remaining,
675
+ )
676
+
677
+ available = self._get_available_actions()
678
+
679
+ metadata = {}
680
+ if done:
681
+ temp_obs = SREObservation(
682
+ message=message,
683
+ logs=logs,
684
+ success=success,
685
+ metrics=metrics,
686
+ available_actions=available,
687
+ task_description=self._state.task_description,
688
+ done=done,
689
+ reward=reward,
690
+ )
691
+ score = self.rubric(None, temp_obs)
692
+ metadata["grader_score"] = score
693
+ metadata["total_accumulated_reward"] = self._state.total_reward
694
+
695
+ message = f"{message} [GRADER_SCORE: {score:.3f}]"
696
+
697
+ obs = SREObservation(
698
+ message=message,
699
+ logs=logs,
700
+ success=success,
701
+ metrics=metrics,
702
+ available_actions=available,
703
+ task_description=self._state.task_description,
704
+ done=done,
705
+ reward=reward,
706
+ metadata=metadata,
707
+ )
708
+
709
+ return obs
710
+
711
+ def _get_available_actions(self) -> list[str]:
712
+ """Return contextually reasonable action types."""
713
+ actions = [CommandType.DIAGNOSE.value, CommandType.NOOP.value]
714
+ difficulty = self._state.task_difficulty
715
+
716
+ # Dynamically restrict dangerous actions until diagnosis is complete
717
+ if difficulty == TaskDifficulty.EASY:
718
+ if self._diagnosed:
719
+ actions.append(CommandType.RESTART_POD.value)
720
+ elif difficulty == TaskDifficulty.MEDIUM:
721
+ if self._diagnosed:
722
+ actions.append(CommandType.RUN_SQL.value)
723
+ elif difficulty == TaskDifficulty.HARD:
724
+ if self._diagnosed:
725
+ actions.append(CommandType.SCALE_SERVERS.value)
726
+ elif difficulty == TaskDifficulty.EXTREME:
727
+ if self._diagnosed:
728
+ actions.append(CommandType.CHECK_LOGS.value)
729
+ if self._logs_checked:
730
+ actions.append(CommandType.ROLLBACK.value)
731
+
732
+ return actions
733
+
734
+ def _generate_initial_logs(self, difficulty: TaskDifficulty) -> list[str]:
735
+ """Generate realistic initial alert logs for each scenario."""
736
+ if difficulty == TaskDifficulty.EASY:
737
+ return [
738
+ "[ALERT] PagerDuty: High-severity alert triggered for 'production/web'",
739
+ "[K8S] Event: pod-web-3 - Back-off restarting failed container (OOMKilled)",
740
+ "[K8S] Event: pod-web-3 - Container 'web-app' memory limit 512Mi exceeded",
741
+ "[METRICS] Cluster uptime dropped to 45%. Error rate: 55%.",
742
+ "[HINT] Available commands: diagnose, restart_pod, noop",
743
+ ]
744
+ elif difficulty == TaskDifficulty.MEDIUM:
745
+ return [
746
+ "[ALERT] Datadog: API latency P99 > 10,000 ms on 'orders' service",
747
+ "[DB] Slow query log (last 5 min): 847 queries exceeding 1,000 ms",
748
+ "[DB] Top offender: SELECT * FROM orders_table WHERE customer_id = ?",
749
+ "[METRICS] API uptime: 70%. User-facing errors increasing.",
750
+ "[HINT] Available commands: diagnose, run_sql, noop",
751
+ ]
752
+ elif difficulty == TaskDifficulty.HARD:
753
+ return [
754
+ "[ALERT] CloudWatch: Incoming traffic surge - 50,000 RPS (10× normal)",
755
+ "[LB] Load balancer: 90% of requests timing out. Current capacity: 5,000 RPS.",
756
+ "[CLOUD] Active servers: 1. Budget remaining: $500.",
757
+ "[CLOUD] Server cost: $50/unit. Each adds 5,000 RPS capacity.",
758
+ "[METRICS] Uptime: 10%. Error rate: 90%. 45,000 requests/sec being dropped.",
759
+ "[HINT] Available commands: diagnose, scale_servers, noop",
760
+ ]
761
+ elif difficulty == TaskDifficulty.EXTREME:
762
+ return [
763
+ "[ALERT] Sentry: Massive spike in 500 errors for 'auth-service'",
764
+ "[ALERT] Zendesk: 1,200 new customer tickets 'Cannot login'",
765
+ "[K8S] Event: auth-service deployment v1.4.3 just completed",
766
+ "[METRICS] Global uptime dropped to 5%. Critical outage.",
767
+ "[HINT] Available commands: diagnose, check_logs, rollback, noop",
768
+ ]
769
+ return []
server/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi>=0.100.0
2
+ uvicorn[standard]>=0.23.0
3
+ pydantic>=2.0.0
4
+ openenv-core>=0.2.2
5
+ rich>=13.0.0
server/rubrics.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any
2
+
3
+ from openenv.core.rubrics.base import Rubric
4
+
5
+ from sre_env.models import TaskDifficulty
6
+
7
+
8
+ class SREGraderRubric(Rubric):
9
+ def __init__(self, env: Any) -> None:
10
+ super().__init__()
11
+ self._env = env
12
+
13
+ def forward(self, action: Any, observation: Any) -> float:
14
+ difficulty = self._env.state.task_difficulty
15
+
16
+ if difficulty == TaskDifficulty.EASY:
17
+ return self._grade_easy()
18
+ elif difficulty == TaskDifficulty.MEDIUM:
19
+ return self._grade_medium()
20
+ elif difficulty == TaskDifficulty.HARD:
21
+ return self._grade_hard()
22
+ elif difficulty == TaskDifficulty.EXTREME:
23
+ return self._grade_extreme()
24
+ return 0.0
25
+
26
+ def _grade_easy(self) -> float:
27
+ """Proportional to restored uptime (target ≥ 0.95)."""
28
+ return min(1.0, self._env.state.current_uptime / 0.95)
29
+
30
+ def _grade_medium(self) -> float:
31
+ """Proportional to latency improvement."""
32
+ from server.environment import TASK_CONFIGS
33
+
34
+ config = TASK_CONFIGS[TaskDifficulty.MEDIUM]
35
+ baseline = config["initial_latency_ms"]
36
+ target = config["target_latency_ms"]
37
+
38
+ # Access internal environment state
39
+ current = self._env._current_latency_ms
40
+
41
+ if current <= target:
42
+ return 1.0
43
+
44
+ improvement_ratio = (baseline - current) / (baseline - target)
45
+ return max(0.0, min(1.0, improvement_ratio))
46
+
47
+ def _grade_hard(self) -> float:
48
+ """Composite: 50% uptime + 30% budget + 20% speed bonus."""
49
+ from server.environment import TASK_CONFIGS
50
+
51
+ config = TASK_CONFIGS[TaskDifficulty.HARD]
52
+
53
+ # Uptime score
54
+ uptime_score = min(1.0, self._env.state.current_uptime / 0.90)
55
+
56
+ # Budget score — proportion of budget remaining
57
+ budget_score = max(0.0, self._env.state.budget_remaining / config["budget"])
58
+
59
+ # Speed bonus — fewer steps = higher bonus
60
+ speed_bonus = max(0.0, 1.0 - (self._env.state.step_count / self._env.state.max_steps))
61
+
62
+ return 0.5 * uptime_score + 0.3 * budget_score + 0.2 * speed_bonus
63
+
64
+ def _grade_extreme(self) -> float:
65
+ """Composite: 70% uptime + 30% speed bonus (No budget used in rollback)."""
66
+ uptime_score = min(1.0, self._env.state.current_uptime / 0.95)
67
+ speed_bonus = max(0.0, 1.0 - (self._env.state.step_count / self._env.state.max_steps))
68
+ return 0.7 * uptime_score + 0.3 * speed_bonus
server/schemas/reset.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class CustomResetRequest(BaseModel):
7
+ seed: int
8
+ difficulty: str
9
+ custom_description: str
10
+ custom_logs: List[str]
sre_env/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from sre_env.models import SREAction, SREObservation, SREState
2
+
3
+ __all__ = ["SREAction", "SREObservation", "SREState"]
sre_env/constants/prompts.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ SYSTEM_PROMPT = """
2
+ You are an expert SRE AI agent. You must respond ONLY with a JSON object.
3
+ Your goal is to resolve the incident with the highest uptime and lowest cost.
4
+ Always 'diagnose' first to understand the root cause before taking corrective actions.
5
+
6
+ Schema:
7
+ {
8
+ "command_type": "diagnose" | "restart_pod" | "run_sql" | "scale_servers" | "noop",
9
+ "target_resource": "<resource_id>",
10
+ "parameters": {}
11
+ }
12
+
13
+ Command Details:
14
+ - diagnose: Sets target_resource to 'system' or the afflicted component.
15
+ - restart_pod: Sets target_resource to the exact failing pod name.
16
+ - run_sql: Sets target_resource to the table name. Requires "sql" parameter, e.g., {"sql": "CREATE INDEX ..."}.
17
+ - scale_servers: Sets target_resource to the cluster name. Requires "replicas" integer parameter, e.g., {"replicas": 5}.
18
+
19
+ Examples:
20
+ 1. {"command_type": "diagnose", "target_resource": "production", "parameters": {}}
21
+ 2. {"command_type": "restart_pod", "target_resource": "pod-web-3", "parameters": {}}
22
+ 3. {"command_type": "run_sql", "target_resource": "orders_table", "parameters": {"sql": "CREATE INDEX idx_customer_id ON orders_table(customer_id)"}}
23
+ 4. {"command_type": "scale_servers", "target_resource": "us-east-cluster", "parameters": {"replicas": 5}}
24
+
25
+ Respond strictly with the JSON object. Do not include markdown formatting or conversational text.
26
+ """
27
+
28
+ ROUTER_SYSTEM_PROMPT = """
29
+ You are an SRE Incident Router. Map the user's custom incident specifically to one of these 4 archetypes:
30
+ 1. "easy": Requires pod restarts. Target MUST BE 'pod-web-3'.
31
+ 2. "medium": Requires adding DB indexes. Target MUST BE 'orders_table'.
32
+ 3. "hard": Requires scaling servers. Target MUST BE 'system'.
33
+ 4. "extreme": Requires diagnosing auth failures and rollback. Target MUST BE 'auth-service'.
34
+
35
+ If the custom incident is unrelated (e.g. hacking, UI design), reject it.
36
+ Return JSON ONLY:
37
+ {
38
+ "accepted": true/false,
39
+ "archetype": "easy|medium|hard|extreme",
40
+ "reason": "If rejected, explain why. Omit if accepted.",
41
+ "custom_description": "INCIDENT: Rewritten professional incident describing the problem, retaining the target requirement.",
42
+ "custom_logs": ["Simulated terminal log 1", "log 2", "log 3"]
43
+ }
44
+ """
sre_env/core/client.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+
3
+ import requests
4
+
5
+
6
+ def env_reset(base_url: str, seed: int | None = None) -> dict:
7
+ try:
8
+ payload = {"seed": seed} if seed is not None else {}
9
+ resp = requests.post(f"{base_url}/reset", json=payload, timeout=30)
10
+ resp.raise_for_status()
11
+ return resp.json()
12
+ except requests.exceptions.ConnectionError:
13
+ print(f"❌ Error: Could not connect to environment server at {base_url}")
14
+ sys.exit(1)
15
+
16
+
17
+ def env_step(base_url: str, action: dict) -> dict:
18
+ resp = requests.post(f"{base_url}/step", json={"action": action}, timeout=30)
19
+ resp.raise_for_status()
20
+ return resp.json()
sre_env/inference.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import sys
4
+ import time
5
+ from datetime import datetime
6
+
7
+ import requests
8
+ from dotenv import load_dotenv
9
+ from openai import OpenAI
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.prompt import Prompt
13
+
14
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
15
+
16
+ from sre_env.constants.prompts import ROUTER_SYSTEM_PROMPT, SYSTEM_PROMPT
17
+ from sre_env.core.client import env_reset, env_step
18
+ from sre_env.utils.parser import extract_json
19
+
20
+ load_dotenv()
21
+
22
+ API_BASE_URL = os.getenv("API_BASE_URL")
23
+ MODEL_NAME = os.getenv("MODEL_NAME")
24
+ HF_TOKEN = os.getenv("HF_TOKEN")
25
+ ENV_BASE_URL = os.getenv("ENV_BASE_URL")
26
+
27
+ MAX_STEPS = 30
28
+ TIMEOUT_MINUTES = 18
29
+
30
+
31
+ def main():
32
+ console = Console()
33
+ client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN or "dummy")
34
+
35
+ console.print(
36
+ Panel.fit(
37
+ "[bold cyan]🛡️ Sentinel-SRE Autonomous Agent[/bold cyan]\nInteractive Incident Injector",
38
+ border_style="cyan",
39
+ )
40
+ )
41
+
42
+ console.print("\n[bold white]Select an Incident to inject into the environment:[/bold white]")
43
+ console.print(" [green][1][/green] 🟢 OOMKilled Pod Crash (Easy)")
44
+ console.print(" [yellow][2][/yellow] 🟡 Missing Database Index (Medium)")
45
+ console.print(" [red][3][/red] 🔴 50,000 RPS Traffic Spike (Hard)")
46
+ console.print(" [magenta][4][/magenta] 💀 Bad Code Deployment (Extreme)")
47
+ console.print(" [blue][5][/blue] 🧠 Dynamic Task Generation (Custom Prompt)")
48
+
49
+ while True:
50
+ choice = Prompt.ask(
51
+ "\n[bold cyan]Enter scenario[/bold cyan]",
52
+ choices=["1", "2", "3", "4", "5"],
53
+ default="1",
54
+ )
55
+
56
+ if choice == "5":
57
+ user_prompt = Prompt.ask("\n[bold blue]Describe the incident you want to simulate[/bold blue]")
58
+
59
+ with console.status("[bold blue]LLM Analyzing Scenario...", spinner="dots"):
60
+ try:
61
+ resp = client.chat.completions.create(
62
+ model=MODEL_NAME,
63
+ messages=[
64
+ {"role": "system", "content": ROUTER_SYSTEM_PROMPT},
65
+ {"role": "user", "content": user_prompt},
66
+ ],
67
+ response_format={"type": "json_object"},
68
+ )
69
+ result = json.loads(resp.choices[0].message.content)
70
+ if not result.get("accepted"):
71
+ console.print(f"[bold red]❌ Rejected:[/bold red] {result.get('reason')}")
72
+ continue
73
+
74
+ diff_map = {"easy": 2, "medium": 1, "hard": 5, "extreme": 0}
75
+ chosen_seed = diff_map.get(result.get("archetype", "easy"), 2)
76
+
77
+ with console.status("[bold green]Injecting Custom Chaos...", spinner="aesthetic"):
78
+ payload = {
79
+ "seed": chosen_seed,
80
+ "difficulty": result.get("archetype", "easy"),
81
+ "custom_description": result["custom_description"],
82
+ "custom_logs": result.get("custom_logs", []),
83
+ }
84
+ r = requests.post(f"{ENV_BASE_URL}/custom_reset", json=payload, timeout=10)
85
+ reset_resp = r.json()
86
+ break
87
+ except Exception as e:
88
+ console.print(f"[bold red]LLM Error:[/bold red] {e}")
89
+ continue
90
+ else:
91
+ seed_map = {"1": 2, "2": 1, "3": 5, "4": 0}
92
+ chosen_seed = seed_map[choice]
93
+ with console.status("[bold green]Injecting Static Chaos...", spinner="aesthetic"):
94
+ reset_resp = env_reset(ENV_BASE_URL, seed=chosen_seed)
95
+ break
96
+
97
+ observation = reset_resp.get("observation", {})
98
+ done = reset_resp.get("done", False)
99
+
100
+ total_reward = 0.0
101
+ step_count = 0
102
+ start_time = time.time()
103
+
104
+ audit_trail = {
105
+ "timestamp": datetime.utcnow().isoformat(),
106
+ "task_description": observation.get("task_description", "N/A"),
107
+ "trajectory": [],
108
+ }
109
+
110
+ console.print(f"[bold yellow]📋 Task:[/bold yellow] {audit_trail['task_description']}")
111
+
112
+ while not done and step_count < MAX_STEPS:
113
+ if (time.time() - start_time) / 60 > TIMEOUT_MINUTES:
114
+ console.print("[bold red]Timeout Reached![/bold red]")
115
+ break
116
+
117
+ step_count += 1
118
+ console.print(f"\n[bold magenta]── Step {step_count} ──────────────────────────────────[/bold magenta]")
119
+
120
+ action = {}
121
+ with console.status("[bold green]Agent Thinking...", spinner="dots"):
122
+ try:
123
+ history = [
124
+ {"role": "system", "content": SYSTEM_PROMPT},
125
+ {"role": "user", "content": f"Context: {json.dumps(observation)}"},
126
+ ]
127
+ response = client.chat.completions.create(
128
+ model=MODEL_NAME, messages=history, max_tokens=512, temperature=0.1
129
+ )
130
+ raw_content = response.choices[0].message.content.strip()
131
+ action = extract_json(raw_content)
132
+ except Exception as e:
133
+ console.print(f"[bold red]❌ LLM/Parse Error:[/bold red] {e}")
134
+ break
135
+
136
+ cmd_type = action.get("command_type", "UNKNOWN")
137
+ target = action.get("target_resource", "UNKNOWN")
138
+ console.print(f" [bold blue]🚀 Action Request:[/bold blue] {cmd_type} [white]on[/white] {target}")
139
+
140
+ step_resp = env_step(ENV_BASE_URL, action)
141
+ prev_obs = observation
142
+ observation = step_resp.get("observation", {})
143
+ reward = step_resp.get("reward", 0.0) or 0.0
144
+ done = step_resp.get("done", False)
145
+ total_reward += reward
146
+
147
+ audit_trail["trajectory"].append(
148
+ {
149
+ "step": step_count,
150
+ "observation_in": prev_obs,
151
+ "action": action,
152
+ "reward": reward,
153
+ "observation_out": observation,
154
+ }
155
+ )
156
+
157
+ msg = observation.get("message", "")
158
+ color = "green" if "Success" in msg or "restored" in msg else "yellow" if "Already" in msg else "red"
159
+ console.print(f" [bold {color}]► Result:[/bold {color}] {msg}")
160
+ console.print(
161
+ f" [bold cyan]► Reward:[/bold cyan] {reward:+.3f} | [bold cyan]Total:[/bold cyan] {total_reward:+.3f}"
162
+ )
163
+
164
+ score = observation.get("metadata", {}).get("grader_score", "N/A")
165
+ final_msg = observation.get("message", "")
166
+ if "GRADER_SCORE:" in final_msg:
167
+ try:
168
+ score = final_msg.split("GRADER_SCORE: ")[1].split("]")[0]
169
+ except Exception:
170
+ pass
171
+
172
+ panel = Panel.fit(
173
+ f"[bold white]Resolution Complete[/bold white]\n"
174
+ f"[bold green]Final Grade:[/bold green] {score}\n"
175
+ f"[bold green]Total Steps:[/bold green] {step_count}",
176
+ title="[bold cyan]End of Episode[/bold cyan]",
177
+ border_style="cyan",
178
+ )
179
+ console.print("\n", panel)
180
+
181
+ with open("agent_trace.json", "w") as f:
182
+ json.dump(audit_trail, f, indent=2)
183
+ console.print("[dim]Audit trail saved to agent_trace.json[/dim]")
184
+
185
+
186
+ if __name__ == "__main__":
187
+ main()
sre_env/models.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+ from typing import Any, Dict, List
3
+
4
+ from openenv.core.env_server.types import Action, Observation, State
5
+ from pydantic import Field
6
+
7
+
8
+ class CommandType(str, Enum):
9
+ """Available SRE command types."""
10
+
11
+ RESTART_POD = "restart_pod"
12
+ RUN_SQL = "run_sql"
13
+ SCALE_SERVERS = "scale_servers"
14
+ DIAGNOSE = "diagnose"
15
+ CHECK_LOGS = "check_logs"
16
+ ROLLBACK = "rollback"
17
+ NOOP = "noop"
18
+
19
+
20
+ class TaskDifficulty(str, Enum):
21
+ """Task difficulty tiers."""
22
+
23
+ EASY = "easy"
24
+ MEDIUM = "medium"
25
+ HARD = "hard"
26
+ EXTREME = "extreme"
27
+
28
+
29
+ class SREAction(Action):
30
+ """An SRE command issued by the agent.
31
+
32
+ Attributes:
33
+ command_type: The type of SRE command to execute. Validated dynamically in the environment.
34
+ target_resource: The resource to target (e.g. pod name, table name, cluster).
35
+ parameters: Additional key-value parameters for the command.
36
+ """
37
+
38
+ command_type: str = Field(
39
+ default="noop",
40
+ description="Type of SRE command to execute (restart_pod, run_sql, scale_servers, diagnose, check_logs, rollback, noop).",
41
+ )
42
+ target_resource: str = Field(
43
+ default="",
44
+ description="The target resource identifier (e.g. 'pod-web-3', 'orders_table', 'us-east-cluster').",
45
+ )
46
+ parameters: Dict[str, Any] = Field(
47
+ default_factory=dict,
48
+ description="Additional command parameters (e.g. {'replicas': 5} for scale_servers).",
49
+ )
50
+
51
+
52
+ class SystemMetrics(Action):
53
+ """Current system metrics snapshot — embedded inside observations.
54
+
55
+ Note: Inherits from Action (BaseModel) purely for Pydantic config;
56
+ semantically this is just a nested model.
57
+ """
58
+
59
+ model_config = Action.model_config.copy()
60
+ model_config["extra"] = "allow"
61
+
62
+ cpu_percent: float = Field(default=0.0, description="CPU utilisation 0-100")
63
+ memory_percent: float = Field(default=0.0, description="Memory utilisation 0-100")
64
+ latency_ms: float = Field(default=0.0, description="P99 latency in milliseconds")
65
+ uptime: float = Field(default=1.0, description="Service uptime ratio 0.0-1.0")
66
+ error_rate: float = Field(default=0.0, description="Error rate 0.0-1.0")
67
+ budget_used: float = Field(default=0.0, description="Cloud budget consumed in $")
68
+
69
+
70
+ class SREObservation(Observation):
71
+ """Observation returned after each environment step.
72
+
73
+ Combines human-readable messages, structured logs, success flags,
74
+ and real-time system metrics.
75
+ """
76
+
77
+ message: str = Field(
78
+ default="",
79
+ description="Human-readable status message describing the current situation.",
80
+ )
81
+ logs: List[str] = Field(
82
+ default_factory=list,
83
+ description="Simulated structured log lines from the SRE system.",
84
+ )
85
+ success: bool = Field(
86
+ default=False,
87
+ description="Whether the last action was successful.",
88
+ )
89
+ metrics: SystemMetrics = Field(
90
+ default_factory=SystemMetrics,
91
+ description="Current system metrics snapshot.",
92
+ )
93
+ available_actions: List[str] = Field(
94
+ default_factory=list,
95
+ description="List of contextually valid action types for the current state.",
96
+ )
97
+ task_description: str = Field(
98
+ default="",
99
+ description="Description of the current incident scenario.",
100
+ )
101
+
102
+
103
+ class SREState(State):
104
+ """Internal episode state tracked by the environment.
105
+
106
+ Extends the OpenEnv base State (which already provides episode_id and
107
+ step_count) with SRE-specific telemetry.
108
+ """
109
+
110
+ task_difficulty: TaskDifficulty = Field(
111
+ default=TaskDifficulty.EASY,
112
+ description="Difficulty tier of the current task.",
113
+ )
114
+ task_description: str = Field(
115
+ default="",
116
+ description="Human-readable description of the active incident.",
117
+ )
118
+ current_uptime: float = Field(
119
+ default=1.0,
120
+ description="Current service uptime ratio (0.0 to 1.0).",
121
+ )
122
+ budget_remaining: float = Field(
123
+ default=500.0,
124
+ description="Remaining simulated cloud budget in USD.",
125
+ )
126
+ max_steps: int = Field(
127
+ default=15,
128
+ description="Maximum steps allowed for this episode.",
129
+ )
130
+ incident_resolved: bool = Field(
131
+ default=False,
132
+ description="Whether the incident has been fully resolved.",
133
+ )
134
+ root_cause_found: bool = Field(
135
+ default=False,
136
+ description="Whether the agent has identified the root cause.",
137
+ )
138
+ total_reward: float = Field(
139
+ default=0.0,
140
+ description="Cumulative reward accumulated during the episode.",
141
+ )
sre_env/tests/test_environment.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from server.environment import SREEnvironment
2
+ from sre_env.models import SREAction
3
+
4
+
5
+ def test_easy_episode():
6
+ env = SREEnvironment()
7
+ env.reset(seed=42)
8
+ # Ensure it's easy by forcing it if needed, or by injecting state
9
+ env.state.task_difficulty = "easy"
10
+
11
+ # 1. Diagnose
12
+ obs = env.step(SREAction(command_type="diagnose"))
13
+ assert obs.success is True
14
+ assert "pod-web-3" in obs.message
15
+
16
+ # 2. Restart wrong pod
17
+ obs2 = env.step(SREAction(command_type="restart_pod", target_resource="pod-web-not-real"))
18
+ assert obs2.success is False
19
+ assert obs2.reward < 0
20
+
21
+ # 3. Restart correct pod
22
+ obs3 = env.step(SREAction(command_type="restart_pod", target_resource="pod-web-3"))
23
+ assert obs3.success is True
24
+ assert obs3.done is True
25
+ assert obs3.metadata["grader_score"] > 0.8
26
+
27
+
28
+ def test_medium_episode():
29
+ env = SREEnvironment()
30
+ env.reset(seed=42)
31
+ env.state.task_difficulty = "medium"
32
+
33
+ # 1. Diagnose
34
+ env.step(SREAction(command_type="diagnose"))
35
+
36
+ # 2. Run wrong SQL
37
+ obs = env.step(
38
+ SREAction(
39
+ command_type="run_sql",
40
+ target_resource="users",
41
+ parameters={"sql": "CREATE INDEX on users"},
42
+ )
43
+ )
44
+ assert obs.success is False
45
+
46
+ # 3. Run partial SQL (wrong column)
47
+ obs2 = env.step(
48
+ SREAction(
49
+ command_type="run_sql",
50
+ target_resource="orders_table",
51
+ parameters={"sql": "CREATE INDEX on orders_table(date)"},
52
+ )
53
+ )
54
+ assert obs2.success is True
55
+ assert obs2.done is False
56
+
57
+ # 4. Run correct SQL
58
+ obs3 = env.step(
59
+ SREAction(
60
+ command_type="run_sql",
61
+ target_resource="orders_table",
62
+ parameters={"sql": "CREATE INDEX idx ON orders_table(customer_id)"},
63
+ )
64
+ )
65
+ assert obs3.success is True
66
+ assert obs3.done is True
67
+ assert obs3.metadata["grader_score"] > 0.9
68
+
69
+
70
+ def test_hard_episode():
71
+ env = SREEnvironment()
72
+ env.reset(seed=42)
73
+ env.state.task_difficulty = "hard"
74
+
75
+ env.step(SREAction(command_type="diagnose"))
76
+
77
+ # Scale too many (break budget)
78
+ obs = env.step(
79
+ SREAction(
80
+ command_type="scale_servers",
81
+ target_resource="cluster",
82
+ parameters={"replicas": 20},
83
+ )
84
+ )
85
+ assert obs.success is False
86
+ assert obs.done is True # Out of budget
87
+ assert obs.metadata["grader_score"] < 0.5
88
+
89
+
90
+ def test_extreme_episode():
91
+ env = SREEnvironment()
92
+ env.reset(seed=42)
93
+ env.state.task_difficulty = "extreme"
94
+
95
+ # 1. Diagnose
96
+ env.step(SREAction(command_type="diagnose"))
97
+
98
+ # 2. Check Logs
99
+ obs_logs = env.step(SREAction(command_type="check_logs", target_resource="auth-service"))
100
+ assert obs_logs.success is True
101
+ assert "v1.4.2" in str(obs_logs.logs)
102
+
103
+ # 3. Rollback
104
+ obs_rollback = env.step(
105
+ SREAction(
106
+ command_type="rollback",
107
+ target_resource="auth-service",
108
+ parameters={"revision": "v1.4.2"},
109
+ )
110
+ )
111
+ assert obs_rollback.success is True
112
+ assert obs_rollback.done is True
113
+ assert obs_rollback.metadata["grader_score"] > 0.8
sre_env/utils/parser.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import re
3
+
4
+
5
+ def extract_json(text: str) -> dict:
6
+ """Finds the first { and last } to extract JSON from conversational filler."""
7
+ try:
8
+ match = re.search(r"(\{.*\})", text, re.DOTALL)
9
+ if match:
10
+ clean_content = match.group(1)
11
+ clean_content = clean_content.replace("```json", "").replace("```", "")
12
+ return json.loads(clean_content)
13
+ return json.loads(text)
14
+ except Exception:
15
+ raise ValueError(f"Failed to parse LLM response as JSON. Content: {text[:100]}...")
uv.lock ADDED
The diff for this file is too large to render. See raw diff