# OpenEnv Compliance Guide
OpenRange implements the OpenEnv 0.2.x environment contract. This doc maps every requirement.
## Checklist
| Requirement | Status | Implementation |
|-------------|--------|----------------|
| `Environment` subclass | Done | `RangeEnvironment` extends `Environment[RangeAction, RangeObservation, RangeState]` |
| `reset()` returns `ObsT` | Done | Returns `RangeObservation` with episode briefing |
| `step()` returns `ObsT` | Done | Returns `RangeObservation` with stdout/stderr/reward/done |
| `state` property returns `StateT` | Done | Returns `RangeState` (episode_id, step_count, mode, flags_found, services_status, tier) |
| `Action` subclass (Pydantic, extra=forbid) | Done | `RangeAction(Action)` with `command: str`, `mode: Literal["red", "blue"]` |
| `Observation` subclass (Pydantic, extra=forbid) | Done | `RangeObservation(Observation)` — inherits `done`, `reward` from base; adds `stdout`, `stderr`, `flags_captured`, `alerts` |
| `State` subclass (Pydantic, extra=allow) | Done | `RangeState(State)` — inherits `episode_id`, `step_count` from base; adds `mode`, `flags_found`, `services_status`, `tier` |
| `create_app(Class, ActionType, ObsType)` | Done | `open_range.server.app:create_app()` delegates directly to `openenv.core.env_server.create_app(...)` |
| `EnvClient` subclass | Done | `OpenRangeEnv(EnvClient[RangeAction, RangeObservation, RangeState])` |
| `_step_payload()` | Done | Returns `{"command": action.command, "mode": action.mode}` |
| `_parse_result()` | Done | Parses server response to `StepResult[RangeObservation]` |
| `_parse_state()` | Done | Parses server response to `RangeState` |
| `/health` endpoint | Done | Provided by `create_app(...)` |
| `/metadata` endpoint | Done | Provided by `create_app(...)` |
| `/schema` endpoint | Done | Provided by `create_app(...)` |
| `/ws` WebSocket | Done | Provided by `create_app(...)` |
| `/reset`, `/step`, `/state` HTTP | Done | Provided by `create_app(...)` |
| `Rubric` for rewards | Done | `CompositeRedReward`, `CompositeBlueReward` (lazy-loaded in `RangeEnvironment._apply_rewards`) |
| `openenv.yaml` manifest | Done | Root `openenv.yaml` with `spec_version`, `type`, `runtime`, `app`, and `port` |
| `Dockerfile` | Done | Root `Dockerfile` plus `server/Dockerfile`, both launching `uvicorn server.app:app` |
| `python -m open_range.server` entry point | Done | `open_range.server.__main__` plus `server` console script |
## Server Mode
The server entrypoint is the standard OpenEnv app factory:
- `open_range.server.app:create_app()` returns `create_app(RangeEnvironment, RangeAction, RangeObservation, env_name="open_range")`
- `server.app:app` is the repository-level wrapper referenced by `openenv.yaml`
- The OpenEnv-generated HTTP and WebSocket endpoints are the only public runtime contract
## Deployment
The OpenEnv server runs as a **container in the same Docker Compose stack** as the enterprise range. It reaches range containers via the Docker SDK (mounted `/var/run/docker.sock`).
```mermaid
flowchart TD
subgraph compose [docker-compose.yml]
subgraph server [OpenEnv Server Container]
APP[FastAPI app
/health, /metadata, /schema,
/reset, /step, /state, /ws]
end
subgraph range [Enterprise Range - 8 containers]
ATK[attacker] --- FW[firewall]
FW --- WEB[web] --- MAIL[mail]
WEB --- DB[db] --- FILES[files]
DB --- LDAP[ldap] --- SIEM[siem]
end
SOCK[docker.sock mount]
end
APP -->|docker SDK| SOCK
SOCK -->|docker exec| range
APP -->|port 8000| EXT[External clients]
style server fill:#6bcb7722,stroke:#6bcb77
style range fill:#7c73e622,stroke:#7c73e6
```
`reset()` selects a pre-validated frozen snapshot from the snapshot store. No LLM calls in the hot path -- snapshot generation is asynchronous.
## Common Mistakes to Avoid
1. **Don't redeclare `done` or `reward` on Observation.** The base class already has them. `RangeObservation` correctly inherits them.
2. **Don't redeclare `episode_id` or `step_count` on State.** The base class already has them. `RangeState` correctly inherits them.
3. **Pass the CLASS or factory to `create_app()`, not an instance.** Each WebSocket session gets its own instance.
4. **Action uses `extra="forbid"` (via openenv base).** Unknown fields cause validation errors. Keep actions minimal.
5. **State uses `extra="allow"`.** You can add any fields you want.
6. **`reset()` returns ObsT (server-side), `StepResult[ObsT]` (client-side).** The server wraps it.
7. **Shared models live outside `server/`.** Clients import `open_range.models`, not `open_range.server.*`.
## API Signatures (Exact)
```python
# Server-side (src/open_range/server/environment.py)
class RangeEnvironment(Environment[RangeAction, RangeObservation, RangeState]):
SUPPORTS_CONCURRENT_SESSIONS = False
def __init__(self, max_steps: int = 100, exec_timeout: float = 30.0,
docker_available: bool | None = None) -> None: ...
def reset(self, seed: int | None = None,
episode_id: str | None = None, **kwargs) -> RangeObservation: ...
def step(self, action: RangeAction,
timeout_s: float | None = None, **kwargs) -> RangeObservation: ...
@property
def state(self) -> RangeState: ...
# Client-side (src/open_range/client/client.py)
class OpenRangeEnv(EnvClient[RangeAction, RangeObservation, RangeState]):
def _step_payload(self, action: RangeAction) -> dict: ...
def _parse_result(self, payload: dict) -> StepResult[RangeObservation]: ...
def _parse_state(self, payload: dict) -> RangeState: ...
# App factory (src/open_range/server/app.py)
app = create_app(RangeEnvironment, RangeAction, RangeObservation, env_name="open_range")
# Entry point (src/open_range/server/__main__.py)
# python -m open_range.server [--host HOST] [--port PORT] [--reload] [--log-level LEVEL]
```
## Reference Implementations
Study these OpenEnv environments as patterns:
- **`envs/coding_env/`** — closest analog (execute code, get stdout/stderr). Uses `Environment` base.
- **`envs/echo_env/`** — simplest possible environment. Uses `MCPEnvironment` base.
- **`envs/finqa_env/`** — MCP tool-based with complex rewards. Uses `MCPEnvironment` base.