Spaces:
Runtime error
Runtime error
Lars Talian commited on
Commit ·
fda4cbc
1
Parent(s): 7529adc
fix: align package with openenv contract
Browse files- Dockerfile +23 -0
- README.md +19 -4
- docs/openenv-compliance.md +17 -28
- examples/remote_client_demo.py +31 -0
- openenv.yaml +4 -0
- pyproject.toml +2 -2
- server/Dockerfile +23 -0
- server/__init__.py +6 -0
- server/app.py +18 -0
- server/environment.py +5 -0
- src/open_range/__init__.py +8 -2
- src/open_range/builder/builder.py +10 -1
- src/open_range/client/__init__.py +1 -1
- src/open_range/client/client.py +19 -53
- src/open_range/server/Dockerfile +2 -1
- src/open_range/server/__init__.py +6 -0
- src/open_range/server/app.py +18 -281
- uv.lock +6 -4
Dockerfile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
RUN apt-get update && \
|
| 6 |
+
apt-get install -y --no-install-recommends \
|
| 7 |
+
docker.io \
|
| 8 |
+
curl \
|
| 9 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
+
|
| 11 |
+
COPY pyproject.toml .
|
| 12 |
+
COPY openenv.yaml .
|
| 13 |
+
COPY server/ server/
|
| 14 |
+
COPY src/ src/
|
| 15 |
+
|
| 16 |
+
RUN pip install --no-cache-dir -e .
|
| 17 |
+
|
| 18 |
+
EXPOSE 8000
|
| 19 |
+
|
| 20 |
+
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
| 21 |
+
CMD curl -f http://localhost:8000/health || exit 1
|
| 22 |
+
|
| 23 |
+
CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8000"]
|
README.md
CHANGED
|
@@ -31,14 +31,27 @@ Red and Blue operate on the **same infrastructure simultaneously**. Red's stealt
|
|
| 31 |
## Quick Start
|
| 32 |
|
| 33 |
```bash
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
# End-to-end demo (no Docker, no LLM)
|
| 38 |
uv run python examples/demo.py
|
| 39 |
|
| 40 |
-
# Run the server
|
| 41 |
-
python -
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
# Tests
|
| 44 |
uv run pytest tests/ -v --tb=short
|
|
@@ -50,6 +63,8 @@ uv run pytest tests/ -v --tb=short
|
|
| 50 |
|
| 51 |
**Builder** — Takes a manifest + curriculum context, outputs a `SnapshotSpec`: topology graph, truth graph (planted vulns + exploit chain), evidence graph (what Blue can find), flags, golden path, NPC traffic, and task briefings. Three implementations: `LLMSnapshotBuilder` (production, via litellm), `TemplateOnlyBuilder` (deterministic, for tests), `FileBuilder` (load from disk).
|
| 52 |
|
|
|
|
|
|
|
| 53 |
**Validator** — 10-check admission pipeline. 8 mechanical checks (build/boot, exploitability, patchability, evidence sufficiency, reward grounding, isolation, task feasibility, difficulty calibration) + 2 LLM advisory checks (NPC consistency, realism review). Inverse mutation: patching each planted vuln must break its exploit step.
|
| 54 |
|
| 55 |
**Environment** — `RangeEnvironment(Environment)` following the OpenEnv contract. `reset()` picks a frozen snapshot + samples a task. `step(action)` routes commands to the appropriate container — Red runs on the attacker box, Blue runs on the SIEM. No artificial command allowlists; the container's installed tools are the constraint.
|
|
|
|
| 31 |
## Quick Start
|
| 32 |
|
| 33 |
```bash
|
| 34 |
+
# Install
|
| 35 |
+
git clone https://github.com/open-cybernauts/open-range.git
|
| 36 |
+
cd open-range
|
| 37 |
+
uv sync
|
| 38 |
+
|
| 39 |
+
# Optional: enable the LiteLLM-backed builder pipeline
|
| 40 |
+
uv sync --extra builder
|
| 41 |
|
| 42 |
# End-to-end demo (no Docker, no LLM)
|
| 43 |
uv run python examples/demo.py
|
| 44 |
|
| 45 |
+
# Run the OpenEnv client against a running server
|
| 46 |
+
uv run python examples/remote_client_demo.py --base-url http://localhost:8000
|
| 47 |
+
|
| 48 |
+
# Run the FastAPI server
|
| 49 |
+
uv run server # default: 127.0.0.1:8000
|
| 50 |
+
uv run server --port 9000 # custom port
|
| 51 |
+
uv run server --host 0.0.0.0 # bind all interfaces
|
| 52 |
+
|
| 53 |
+
# Or via uvicorn directly
|
| 54 |
+
uv run uvicorn server.app:app --host 0.0.0.0 --port 8000 --reload
|
| 55 |
|
| 56 |
# Tests
|
| 57 |
uv run pytest tests/ -v --tb=short
|
|
|
|
| 63 |
|
| 64 |
**Builder** — Takes a manifest + curriculum context, outputs a `SnapshotSpec`: topology graph, truth graph (planted vulns + exploit chain), evidence graph (what Blue can find), flags, golden path, NPC traffic, and task briefings. Three implementations: `LLMSnapshotBuilder` (production, via litellm), `TemplateOnlyBuilder` (deterministic, for tests), `FileBuilder` (load from disk).
|
| 65 |
|
| 66 |
+
The deployed package exposes the standard OpenEnv `reset()`, `step()`, and `state()` contract through `server.app:app`, which is the entrypoint referenced by `openenv.yaml`.
|
| 67 |
+
|
| 68 |
**Validator** — 10-check admission pipeline. 8 mechanical checks (build/boot, exploitability, patchability, evidence sufficiency, reward grounding, isolation, task feasibility, difficulty calibration) + 2 LLM advisory checks (NPC consistency, realism review). Inverse mutation: patching each planted vuln must break its exploit step.
|
| 69 |
|
| 70 |
**Environment** — `RangeEnvironment(Environment)` following the OpenEnv contract. `reset()` picks a frozen snapshot + samples a task. `step(action)` routes commands to the appropriate container — Red runs on the attacker box, Blue runs on the SIEM. No artificial command allowlists; the container's installed tools are the constraint.
|
docs/openenv-compliance.md
CHANGED
|
@@ -6,46 +6,35 @@ OpenRange implements the OpenEnv 0.2.x environment contract. This doc maps every
|
|
| 6 |
|
| 7 |
| Requirement | Status | Implementation |
|
| 8 |
|-------------|--------|----------------|
|
| 9 |
-
| `Environment` subclass | Done | `RangeEnvironment` extends `Environment[RangeAction, RangeObservation, RangeState]`
|
| 10 |
| `reset()` returns `ObsT` | Done | Returns `RangeObservation` with episode briefing |
|
| 11 |
| `step()` returns `ObsT` | Done | Returns `RangeObservation` with stdout/stderr/reward/done |
|
| 12 |
| `state` property returns `StateT` | Done | Returns `RangeState` (episode_id, step_count, mode, flags_found, services_status, tier) |
|
| 13 |
-
| `Action` subclass (Pydantic, extra=forbid) | Done | `RangeAction(Action)` with `command: str`, `mode: Literal["red", "blue"]`
|
| 14 |
| `Observation` subclass (Pydantic, extra=forbid) | Done | `RangeObservation(Observation)` — inherits `done`, `reward` from base; adds `stdout`, `stderr`, `flags_captured`, `alerts` |
|
| 15 |
| `State` subclass (Pydantic, extra=allow) | Done | `RangeState(State)` — inherits `episode_id`, `step_count` from base; adds `mode`, `flags_found`, `services_status`, `tier` |
|
| 16 |
-
| `create_app(Class, ActionType, ObsType)` | Done | `
|
| 17 |
-
| `EnvClient` subclass | Done | `OpenRangeEnv(EnvClient[RangeAction, RangeObservation, RangeState])`
|
| 18 |
| `_step_payload()` | Done | Returns `{"command": action.command, "mode": action.mode}` |
|
| 19 |
| `_parse_result()` | Done | Parses server response to `StepResult[RangeObservation]` |
|
| 20 |
| `_parse_state()` | Done | Parses server response to `RangeState` |
|
| 21 |
-
| `/health` endpoint | Done |
|
| 22 |
-
| `/metadata` endpoint | Done |
|
| 23 |
-
| `/schema` endpoint | Done |
|
| 24 |
-
| `/ws` WebSocket | Done |
|
| 25 |
-
| `/reset`, `/step`, `/state` HTTP | Done |
|
| 26 |
| `Rubric` for rewards | Done | `CompositeRedReward`, `CompositeBlueReward` (lazy-loaded in `RangeEnvironment._apply_rewards`) |
|
| 27 |
-
| `openenv.yaml` manifest | Done | `openenv.yaml`
|
| 28 |
-
| `Dockerfile` | Done | `
|
| 29 |
-
| `python -m open_range.server` entry point | Done | `server
|
| 30 |
|
| 31 |
-
##
|
| 32 |
|
| 33 |
-
The server
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
Both modes serve the same contract. The standalone fallback ensures the server works without the openenv package installed.
|
| 39 |
-
|
| 40 |
-
### WebSocket Protocol
|
| 41 |
-
|
| 42 |
-
The `/ws` endpoint accepts JSON messages with a `type` field:
|
| 43 |
-
|
| 44 |
-
- `{"type": "reset"}` or `{"type": "reset", "seed": 42, "episode_id": "ep1"}` — reset the environment
|
| 45 |
-
- `{"type": "step", "command": "nmap -sV 10.0.1.2", "mode": "red"}` — execute an action
|
| 46 |
-
- `{"type": "state"}` — get current episode state
|
| 47 |
-
|
| 48 |
-
Responses include a `type` field: `"observation"`, `"state"`, or `"error"`.
|
| 49 |
|
| 50 |
## Deployment
|
| 51 |
|
|
|
|
| 6 |
|
| 7 |
| Requirement | Status | Implementation |
|
| 8 |
|-------------|--------|----------------|
|
| 9 |
+
| `Environment` subclass | Done | `RangeEnvironment` extends `Environment[RangeAction, RangeObservation, RangeState]` |
|
| 10 |
| `reset()` returns `ObsT` | Done | Returns `RangeObservation` with episode briefing |
|
| 11 |
| `step()` returns `ObsT` | Done | Returns `RangeObservation` with stdout/stderr/reward/done |
|
| 12 |
| `state` property returns `StateT` | Done | Returns `RangeState` (episode_id, step_count, mode, flags_found, services_status, tier) |
|
| 13 |
+
| `Action` subclass (Pydantic, extra=forbid) | Done | `RangeAction(Action)` with `command: str`, `mode: Literal["red", "blue"]` |
|
| 14 |
| `Observation` subclass (Pydantic, extra=forbid) | Done | `RangeObservation(Observation)` — inherits `done`, `reward` from base; adds `stdout`, `stderr`, `flags_captured`, `alerts` |
|
| 15 |
| `State` subclass (Pydantic, extra=allow) | Done | `RangeState(State)` — inherits `episode_id`, `step_count` from base; adds `mode`, `flags_found`, `services_status`, `tier` |
|
| 16 |
+
| `create_app(Class, ActionType, ObsType)` | Done | `open_range.server.app:create_app()` delegates directly to `openenv.core.env_server.create_app(...)` |
|
| 17 |
+
| `EnvClient` subclass | Done | `OpenRangeEnv(EnvClient[RangeAction, RangeObservation, RangeState])` |
|
| 18 |
| `_step_payload()` | Done | Returns `{"command": action.command, "mode": action.mode}` |
|
| 19 |
| `_parse_result()` | Done | Parses server response to `StepResult[RangeObservation]` |
|
| 20 |
| `_parse_state()` | Done | Parses server response to `RangeState` |
|
| 21 |
+
| `/health` endpoint | Done | Provided by `create_app(...)` |
|
| 22 |
+
| `/metadata` endpoint | Done | Provided by `create_app(...)` |
|
| 23 |
+
| `/schema` endpoint | Done | Provided by `create_app(...)` |
|
| 24 |
+
| `/ws` WebSocket | Done | Provided by `create_app(...)` |
|
| 25 |
+
| `/reset`, `/step`, `/state` HTTP | Done | Provided by `create_app(...)` |
|
| 26 |
| `Rubric` for rewards | Done | `CompositeRedReward`, `CompositeBlueReward` (lazy-loaded in `RangeEnvironment._apply_rewards`) |
|
| 27 |
+
| `openenv.yaml` manifest | Done | Root `openenv.yaml` with `spec_version`, `type`, `runtime`, `app`, and `port` |
|
| 28 |
+
| `Dockerfile` | Done | Root `Dockerfile` plus `server/Dockerfile`, both launching `uvicorn server.app:app` |
|
| 29 |
+
| `python -m open_range.server` entry point | Done | `open_range.server.__main__` plus `server` console script |
|
| 30 |
|
| 31 |
+
## Server Mode
|
| 32 |
|
| 33 |
+
The server entrypoint is the standard OpenEnv app factory:
|
| 34 |
|
| 35 |
+
- `open_range.server.app:create_app()` returns `create_app(RangeEnvironment, RangeAction, RangeObservation, env_name="open_range")`
|
| 36 |
+
- `server.app:app` is the repository-level wrapper referenced by `openenv.yaml`
|
| 37 |
+
- The OpenEnv-generated HTTP and WebSocket endpoints are the only public runtime contract
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
## Deployment
|
| 40 |
|
examples/remote_client_demo.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Minimal OpenEnv client demo for a running OpenRange server."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
|
| 7 |
+
from open_range import OpenRangeEnv, RangeAction
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def main() -> None:
|
| 11 |
+
parser = argparse.ArgumentParser(description="Connect to a running OpenRange server")
|
| 12 |
+
parser.add_argument(
|
| 13 |
+
"--base-url",
|
| 14 |
+
default="http://localhost:8000",
|
| 15 |
+
help="OpenEnv server base URL",
|
| 16 |
+
)
|
| 17 |
+
args = parser.parse_args()
|
| 18 |
+
|
| 19 |
+
with OpenRangeEnv(base_url=args.base_url).sync() as env:
|
| 20 |
+
result = env.reset()
|
| 21 |
+
print(result.observation.stdout)
|
| 22 |
+
|
| 23 |
+
result = env.step(
|
| 24 |
+
RangeAction(command="nmap -sV 10.0.1.0/24", mode="red")
|
| 25 |
+
)
|
| 26 |
+
print(result.observation.stdout)
|
| 27 |
+
print(f"reward={result.reward} done={result.done}")
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
if __name__ == "__main__":
|
| 31 |
+
main()
|
openenv.yaml
CHANGED
|
@@ -1,5 +1,9 @@
|
|
| 1 |
spec_version: 1
|
| 2 |
name: open_range
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
version: 0.1.0
|
| 4 |
type: space
|
| 5 |
runtime: fastapi
|
|
|
|
| 1 |
spec_version: 1
|
| 2 |
name: open_range
|
| 3 |
+
type: space
|
| 4 |
+
runtime: fastapi
|
| 5 |
+
app: server.app:app
|
| 6 |
+
port: 8000
|
| 7 |
version: 0.1.0
|
| 8 |
type: space
|
| 9 |
runtime: fastapi
|
pyproject.toml
CHANGED
|
@@ -11,13 +11,13 @@ dependencies = [
|
|
| 11 |
"pyyaml>=6.0",
|
| 12 |
"docker>=7.0",
|
| 13 |
"jinja2>=3.1",
|
| 14 |
-
"
|
| 15 |
-
"uvicorn>=0.24",
|
| 16 |
]
|
| 17 |
|
| 18 |
[project.optional-dependencies]
|
| 19 |
dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "httpx>=0.27"]
|
| 20 |
training = ["trl>=0.8", "unsloth"]
|
|
|
|
| 21 |
|
| 22 |
[build-system]
|
| 23 |
requires = ["hatchling"]
|
|
|
|
| 11 |
"pyyaml>=6.0",
|
| 12 |
"docker>=7.0",
|
| 13 |
"jinja2>=3.1",
|
| 14 |
+
"uvicorn>=0.27",
|
|
|
|
| 15 |
]
|
| 16 |
|
| 17 |
[project.optional-dependencies]
|
| 18 |
dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "httpx>=0.27"]
|
| 19 |
training = ["trl>=0.8", "unsloth"]
|
| 20 |
+
builder = ["litellm>=1.30"]
|
| 21 |
|
| 22 |
[build-system]
|
| 23 |
requires = ["hatchling"]
|
server/Dockerfile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
RUN apt-get update && \
|
| 6 |
+
apt-get install -y --no-install-recommends \
|
| 7 |
+
docker.io \
|
| 8 |
+
curl \
|
| 9 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
+
|
| 11 |
+
COPY pyproject.toml .
|
| 12 |
+
COPY openenv.yaml .
|
| 13 |
+
COPY server/ server/
|
| 14 |
+
COPY src/ src/
|
| 15 |
+
|
| 16 |
+
RUN pip install --no-cache-dir -e .
|
| 17 |
+
|
| 18 |
+
EXPOSE 8000
|
| 19 |
+
|
| 20 |
+
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
| 21 |
+
CMD curl -f http://localhost:8000/health || exit 1
|
| 22 |
+
|
| 23 |
+
CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8000"]
|
server/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Repository-level OpenEnv server entrypoints."""
|
| 2 |
+
|
| 3 |
+
from .app import app, create_app
|
| 4 |
+
from .environment import RangeEnvironment
|
| 5 |
+
|
| 6 |
+
__all__ = ["RangeEnvironment", "app", "create_app"]
|
server/app.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""OpenEnv app entrypoint expected by ``openenv.yaml``."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from open_range.server.app import app, create_app
|
| 6 |
+
|
| 7 |
+
__all__ = ["app", "create_app"]
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def main() -> None:
|
| 11 |
+
"""Run the repository-level server entrypoint via uvicorn."""
|
| 12 |
+
import uvicorn
|
| 13 |
+
|
| 14 |
+
uvicorn.run("server.app:app", host="0.0.0.0", port=8000)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
if __name__ == "__main__":
|
| 18 |
+
main()
|
server/environment.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Repository-level environment wrapper for OpenEnv tooling."""
|
| 2 |
+
|
| 3 |
+
from open_range.server.environment import RangeEnvironment
|
| 4 |
+
|
| 5 |
+
__all__ = ["RangeEnvironment"]
|
src/open_range/__init__.py
CHANGED
|
@@ -1,11 +1,17 @@
|
|
| 1 |
-
"""OpenRange
|
| 2 |
|
| 3 |
from open_range.client.client import OpenRangeEnv
|
| 4 |
-
from open_range.server.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
__all__ = [
|
| 7 |
"OpenRangeEnv",
|
| 8 |
"RangeAction",
|
|
|
|
| 9 |
"RangeObservation",
|
| 10 |
"RangeState",
|
| 11 |
]
|
|
|
|
| 1 |
+
"""OpenRange public package surface."""
|
| 2 |
|
| 3 |
from open_range.client.client import OpenRangeEnv
|
| 4 |
+
from open_range.server.environment import RangeEnvironment
|
| 5 |
+
from open_range.server.models import (
|
| 6 |
+
RangeAction,
|
| 7 |
+
RangeObservation,
|
| 8 |
+
RangeState,
|
| 9 |
+
)
|
| 10 |
|
| 11 |
__all__ = [
|
| 12 |
"OpenRangeEnv",
|
| 13 |
"RangeAction",
|
| 14 |
+
"RangeEnvironment",
|
| 15 |
"RangeObservation",
|
| 16 |
"RangeState",
|
| 17 |
]
|
src/open_range/builder/builder.py
CHANGED
|
@@ -14,7 +14,10 @@ import random
|
|
| 14 |
from pathlib import Path
|
| 15 |
from typing import Any
|
| 16 |
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
from open_range.protocols import (
|
| 20 |
BuildContext,
|
|
@@ -66,6 +69,12 @@ class LLMSnapshotBuilder:
|
|
| 66 |
context: BuildContext,
|
| 67 |
) -> SnapshotSpec:
|
| 68 |
"""Call LLM to generate a candidate snapshot spec."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
user_payload = json.dumps(
|
| 70 |
{
|
| 71 |
"manifest": manifest,
|
|
|
|
| 14 |
from pathlib import Path
|
| 15 |
from typing import Any
|
| 16 |
|
| 17 |
+
try:
|
| 18 |
+
import litellm
|
| 19 |
+
except ImportError: # pragma: no cover - exercised only without builder extra
|
| 20 |
+
litellm = None
|
| 21 |
|
| 22 |
from open_range.protocols import (
|
| 23 |
BuildContext,
|
|
|
|
| 69 |
context: BuildContext,
|
| 70 |
) -> SnapshotSpec:
|
| 71 |
"""Call LLM to generate a candidate snapshot spec."""
|
| 72 |
+
if litellm is None:
|
| 73 |
+
raise RuntimeError(
|
| 74 |
+
"LLMSnapshotBuilder requires the optional builder extra. "
|
| 75 |
+
"Install with `pip install open-range[builder]`."
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
user_payload = json.dumps(
|
| 79 |
{
|
| 80 |
"manifest": manifest,
|
src/open_range/client/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
from open_range.client.client import OpenRangeEnv
|
| 4 |
|
|
|
|
| 1 |
+
"""Typed OpenEnv client exports."""
|
| 2 |
|
| 3 |
from open_range.client.client import OpenRangeEnv
|
| 4 |
|
src/open_range/client/client.py
CHANGED
|
@@ -1,64 +1,30 @@
|
|
| 1 |
-
"""OpenEnv client for OpenRange.
|
| 2 |
-
|
| 3 |
-
Provides OpenRangeEnv which wraps the typed EnvClient with
|
| 4 |
-
OpenRange-specific action/observation/state parsing.
|
| 5 |
-
"""
|
| 6 |
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
-
from
|
|
|
|
| 10 |
|
| 11 |
from open_range.server.models import RangeAction, RangeObservation, RangeState
|
| 12 |
|
| 13 |
-
try:
|
| 14 |
-
from openenv.core.env_client import EnvClient
|
| 15 |
-
from openenv.core.client_types import StepResult
|
| 16 |
-
|
| 17 |
-
class OpenRangeEnv(EnvClient[RangeAction, RangeObservation, RangeState]):
|
| 18 |
-
"""Typed OpenEnv client for OpenRange."""
|
| 19 |
-
|
| 20 |
-
def _step_payload(self, action: RangeAction) -> dict:
|
| 21 |
-
return {"command": action.command, "mode": action.mode}
|
| 22 |
-
|
| 23 |
-
def _parse_result(self, payload: dict) -> StepResult[RangeObservation]:
|
| 24 |
-
obs = RangeObservation(**payload.get("observation", {}))
|
| 25 |
-
return StepResult(
|
| 26 |
-
observation=obs,
|
| 27 |
-
reward=payload.get("reward"),
|
| 28 |
-
done=bool(payload.get("done", False)),
|
| 29 |
-
)
|
| 30 |
-
|
| 31 |
-
def _parse_state(self, payload: dict) -> RangeState:
|
| 32 |
-
return RangeState(**payload)
|
| 33 |
-
|
| 34 |
-
except ImportError:
|
| 35 |
-
# Stub for development without openenv installed
|
| 36 |
-
from dataclasses import dataclass
|
| 37 |
-
|
| 38 |
-
@dataclass
|
| 39 |
-
class StepResult: # type: ignore[no-redef]
|
| 40 |
-
"""Minimal StepResult stub matching openenv.core.client_types."""
|
| 41 |
-
|
| 42 |
-
observation: RangeObservation
|
| 43 |
-
reward: float | None = None
|
| 44 |
-
done: bool = False
|
| 45 |
|
| 46 |
-
|
| 47 |
-
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
|
|
|
| 51 |
|
| 52 |
-
|
| 53 |
-
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
|
| 63 |
-
|
| 64 |
-
|
|
|
|
| 1 |
+
"""Typed OpenEnv client for OpenRange."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
from openenv.core.client_types import StepResult
|
| 6 |
+
from openenv.core.env_client import EnvClient
|
| 7 |
|
| 8 |
from open_range.server.models import RangeAction, RangeObservation, RangeState
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
class OpenRangeEnv(EnvClient[RangeAction, RangeObservation, RangeState]):
|
| 12 |
+
"""Typed OpenEnv client that speaks the standard reset/step/state contract."""
|
| 13 |
|
| 14 |
+
def sync(self) -> "OpenRangeEnv":
|
| 15 |
+
"""Compatibility wrapper matching the documented OpenEnv sync pattern."""
|
| 16 |
+
return self
|
| 17 |
|
| 18 |
+
def _step_payload(self, action: RangeAction) -> dict:
|
| 19 |
+
return {"command": action.command, "mode": action.mode}
|
| 20 |
|
| 21 |
+
def _parse_result(self, payload: dict) -> StepResult[RangeObservation]:
|
| 22 |
+
obs = RangeObservation(**payload.get("observation", {}))
|
| 23 |
+
return StepResult(
|
| 24 |
+
observation=obs,
|
| 25 |
+
reward=payload.get("reward"),
|
| 26 |
+
done=bool(payload.get("done", False)),
|
| 27 |
+
)
|
| 28 |
|
| 29 |
+
def _parse_state(self, payload: dict) -> RangeState:
|
| 30 |
+
return RangeState(**payload)
|
src/open_range/server/Dockerfile
CHANGED
|
@@ -31,6 +31,7 @@ COPY --from=builder /app/src /app/src
|
|
| 31 |
COPY --from=builder /app/pyproject.toml /app/pyproject.toml
|
| 32 |
COPY --from=builder /app/openenv.yaml /app/openenv.yaml
|
| 33 |
COPY --from=builder /app/manifests /app/manifests
|
|
|
|
| 34 |
|
| 35 |
ENV PATH="/app/.venv/bin:$PATH"
|
| 36 |
ENV PYTHONPATH="/app/src:$PYTHONPATH"
|
|
@@ -40,4 +41,4 @@ EXPOSE 8000
|
|
| 40 |
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
| 41 |
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
|
| 42 |
|
| 43 |
-
CMD ["uvicorn", "
|
|
|
|
| 31 |
COPY --from=builder /app/pyproject.toml /app/pyproject.toml
|
| 32 |
COPY --from=builder /app/openenv.yaml /app/openenv.yaml
|
| 33 |
COPY --from=builder /app/manifests /app/manifests
|
| 34 |
+
COPY server/ server/
|
| 35 |
|
| 36 |
ENV PATH="/app/.venv/bin:$PATH"
|
| 37 |
ENV PYTHONPATH="/app/src:$PYTHONPATH"
|
|
|
|
| 41 |
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
| 42 |
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
|
| 43 |
|
| 44 |
+
CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8000"]
|
src/open_range/server/__init__.py
CHANGED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Server-side exports for OpenRange."""
|
| 2 |
+
|
| 3 |
+
from open_range.server.app import app, create_app
|
| 4 |
+
from open_range.server.environment import RangeEnvironment
|
| 5 |
+
|
| 6 |
+
__all__ = ["RangeEnvironment", "app", "create_app"]
|
src/open_range/server/app.py
CHANGED
|
@@ -1,295 +1,32 @@
|
|
| 1 |
-
"""FastAPI application
|
| 2 |
-
|
| 3 |
-
If openenv is installed, delegates to ``openenv.core.env_server.create_app``
|
| 4 |
-
which provides /health, /reset, /step, /state, /ws, /metadata, /schema
|
| 5 |
-
endpoints automatically.
|
| 6 |
-
|
| 7 |
-
Otherwise falls back to a manual FastAPI app with equivalent HTTP endpoints
|
| 8 |
-
plus a WebSocket endpoint at ``/ws`` for persistent sessions.
|
| 9 |
-
"""
|
| 10 |
|
| 11 |
from __future__ import annotations
|
| 12 |
|
| 13 |
-
import
|
| 14 |
-
import
|
| 15 |
-
from typing import Any
|
| 16 |
|
| 17 |
-
from
|
| 18 |
-
from pydantic import BaseModel, ValidationError
|
| 19 |
-
|
| 20 |
-
from open_range.server.console import clear_history, console_router, record_action
|
| 21 |
from open_range.server.environment import RangeEnvironment
|
| 22 |
-
from open_range.server.models import RangeAction, RangeObservation
|
| 23 |
-
|
| 24 |
-
logger = logging.getLogger(__name__)
|
| 25 |
-
|
| 26 |
-
_APP_VERSION = "0.1.0"
|
| 27 |
-
|
| 28 |
-
# ---------------------------------------------------------------------------
|
| 29 |
-
# Try the OpenEnv app factory first
|
| 30 |
-
# ---------------------------------------------------------------------------
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def _try_openenv_app() -> FastAPI | None:
|
| 34 |
-
"""Attempt to create the app via openenv.create_app.
|
| 35 |
-
|
| 36 |
-
Returns None if openenv is not installed or the import fails.
|
| 37 |
-
"""
|
| 38 |
-
try:
|
| 39 |
-
from openenv.core.env_server import create_app
|
| 40 |
-
|
| 41 |
-
openenv_app = create_app(
|
| 42 |
-
RangeEnvironment,
|
| 43 |
-
RangeAction,
|
| 44 |
-
RangeObservation,
|
| 45 |
-
env_name="open_range",
|
| 46 |
-
)
|
| 47 |
-
openenv_app.include_router(console_router)
|
| 48 |
-
return openenv_app
|
| 49 |
-
except ImportError:
|
| 50 |
-
logger.info("openenv not installed -- using standalone FastAPI app")
|
| 51 |
-
return None
|
| 52 |
-
except Exception as exc:
|
| 53 |
-
logger.warning("openenv create_app failed (%s) -- falling back", exc)
|
| 54 |
-
return None
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
# ---------------------------------------------------------------------------
|
| 58 |
-
# Request/Response models matching OpenEnv HTTP protocol
|
| 59 |
-
# ---------------------------------------------------------------------------
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
class ResetRequest(BaseModel):
|
| 63 |
-
"""Matches openenv.core.env_server.types.ResetRequest."""
|
| 64 |
-
|
| 65 |
-
seed: int | None = None
|
| 66 |
-
episode_id: str | None = None
|
| 67 |
|
| 68 |
|
| 69 |
-
|
| 70 |
-
"""
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
# ---------------------------------------------------------------------------
|
| 78 |
-
# Standalone FastAPI fallback
|
| 79 |
-
# ---------------------------------------------------------------------------
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
def _create_standalone_app() -> FastAPI:
|
| 83 |
-
"""Build a FastAPI app that mirrors the OpenEnv endpoint contract.
|
| 84 |
-
|
| 85 |
-
Endpoints
|
| 86 |
-
---------
|
| 87 |
-
GET /health -- liveness check
|
| 88 |
-
GET /metadata -- environment metadata
|
| 89 |
-
GET /schema -- JSON schemas for action, observation, state
|
| 90 |
-
POST /reset -- reset environment, returns initial observation
|
| 91 |
-
POST /step -- execute an action, returns observation + reward + done
|
| 92 |
-
GET /state -- current episode state
|
| 93 |
-
WS /ws -- persistent WebSocket session (JSON messages)
|
| 94 |
-
"""
|
| 95 |
-
|
| 96 |
-
fastapi_app = FastAPI(
|
| 97 |
-
title="OpenRange",
|
| 98 |
-
description="Multi-agent cybersecurity gymnasium",
|
| 99 |
-
version=_APP_VERSION,
|
| 100 |
)
|
|
|
|
|
|
|
| 101 |
|
| 102 |
-
# Shared environment instance for HTTP endpoints.
|
| 103 |
-
# Each WebSocket session creates its own isolated instance.
|
| 104 |
-
env = RangeEnvironment()
|
| 105 |
-
|
| 106 |
-
# Store env on app.state so the console router can access it
|
| 107 |
-
fastapi_app.state.env = env
|
| 108 |
-
|
| 109 |
-
# Include the operator console router
|
| 110 |
-
fastapi_app.include_router(console_router)
|
| 111 |
-
|
| 112 |
-
# ---------------------------------------------------------------
|
| 113 |
-
# HTTP endpoints (matches OpenEnv HTTPEnvServer contract)
|
| 114 |
-
# ---------------------------------------------------------------
|
| 115 |
-
|
| 116 |
-
@fastapi_app.get("/health")
|
| 117 |
-
async def health() -> dict[str, str]:
|
| 118 |
-
return {"status": "healthy"}
|
| 119 |
-
|
| 120 |
-
@fastapi_app.get("/metadata")
|
| 121 |
-
async def metadata() -> dict[str, Any]:
|
| 122 |
-
return {
|
| 123 |
-
"name": "open_range",
|
| 124 |
-
"version": _APP_VERSION,
|
| 125 |
-
"description": "Multi-agent cybersecurity gymnasium built on OpenEnv",
|
| 126 |
-
"supports_concurrent_sessions": False,
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
@fastapi_app.get("/schema")
|
| 130 |
-
async def schema() -> dict[str, Any]:
|
| 131 |
-
return {
|
| 132 |
-
"action": RangeAction.model_json_schema(),
|
| 133 |
-
"observation": RangeObservation.model_json_schema(),
|
| 134 |
-
"state": RangeState.model_json_schema(),
|
| 135 |
-
}
|
| 136 |
-
|
| 137 |
-
@fastapi_app.post("/reset")
|
| 138 |
-
async def reset(req: ResetRequest | None = None) -> dict[str, Any]:
|
| 139 |
-
req = req or ResetRequest()
|
| 140 |
-
clear_history()
|
| 141 |
-
obs = env.reset(seed=req.seed, episode_id=req.episode_id)
|
| 142 |
-
return {
|
| 143 |
-
"observation": obs.model_dump(),
|
| 144 |
-
"reward": obs.reward,
|
| 145 |
-
"done": obs.done,
|
| 146 |
-
}
|
| 147 |
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
body = await request.json()
|
| 153 |
-
# Accept both StepRequest {"action": {...}} and direct RangeAction {"command": ..., "mode": ...}
|
| 154 |
-
if "action" in body:
|
| 155 |
-
action = RangeAction(**body["action"])
|
| 156 |
-
timeout_s = body.get("timeout_s")
|
| 157 |
-
else:
|
| 158 |
-
action = RangeAction(**body)
|
| 159 |
-
timeout_s = None
|
| 160 |
-
obs = env.step(action, timeout_s=timeout_s)
|
| 161 |
-
record_action({
|
| 162 |
-
"step": env.state.step_count,
|
| 163 |
-
"command": action.command,
|
| 164 |
-
"mode": action.mode,
|
| 165 |
-
"time": _time.time(),
|
| 166 |
-
})
|
| 167 |
-
return {
|
| 168 |
-
"observation": obs.model_dump(),
|
| 169 |
-
"reward": obs.reward,
|
| 170 |
-
"done": obs.done,
|
| 171 |
-
}
|
| 172 |
-
|
| 173 |
-
@fastapi_app.get("/state")
|
| 174 |
-
async def get_state() -> dict[str, Any]:
|
| 175 |
-
return env.state.model_dump()
|
| 176 |
-
|
| 177 |
-
# ---------------------------------------------------------------
|
| 178 |
-
# WebSocket endpoint (matches OpenEnv WebSocket protocol)
|
| 179 |
-
# ---------------------------------------------------------------
|
| 180 |
-
|
| 181 |
-
@fastapi_app.websocket("/ws")
|
| 182 |
-
async def ws_endpoint(websocket: WebSocket) -> None:
|
| 183 |
-
"""Persistent WebSocket session with per-connection environment.
|
| 184 |
-
|
| 185 |
-
Message protocol (matches OpenEnv WebSocket contract):
|
| 186 |
-
|
| 187 |
-
Client sends:
|
| 188 |
-
{"type": "reset", "data": {"seed": 42, "episode_id": "ep1"}}
|
| 189 |
-
{"type": "step", "data": {"command": "...", "mode": "red"}}
|
| 190 |
-
{"type": "state"}
|
| 191 |
-
{"type": "close"}
|
| 192 |
-
|
| 193 |
-
Server responds:
|
| 194 |
-
{"type": "observation", "data": {...}}
|
| 195 |
-
{"type": "state", "data": {...}}
|
| 196 |
-
{"type": "error", "data": {"message": "...", "code": "..."}}
|
| 197 |
-
"""
|
| 198 |
-
await websocket.accept()
|
| 199 |
-
|
| 200 |
-
# Each WebSocket session gets its own environment instance
|
| 201 |
-
ws_env = RangeEnvironment()
|
| 202 |
-
|
| 203 |
-
try:
|
| 204 |
-
while True:
|
| 205 |
-
raw = await websocket.receive_text()
|
| 206 |
-
try:
|
| 207 |
-
msg = json.loads(raw)
|
| 208 |
-
except json.JSONDecodeError:
|
| 209 |
-
await websocket.send_json({
|
| 210 |
-
"type": "error",
|
| 211 |
-
"data": {"message": "Invalid JSON", "code": "parse_error"},
|
| 212 |
-
})
|
| 213 |
-
continue
|
| 214 |
-
|
| 215 |
-
msg_type = msg.get("type", "")
|
| 216 |
-
msg_data = msg.get("data", {})
|
| 217 |
-
|
| 218 |
-
if msg_type == "reset":
|
| 219 |
-
seed = msg_data.get("seed") if msg_data else msg.get("seed")
|
| 220 |
-
episode_id = msg_data.get("episode_id") if msg_data else msg.get("episode_id")
|
| 221 |
-
obs = ws_env.reset(seed=seed, episode_id=episode_id)
|
| 222 |
-
await websocket.send_json({
|
| 223 |
-
"type": "observation",
|
| 224 |
-
"data": obs.model_dump(),
|
| 225 |
-
})
|
| 226 |
-
|
| 227 |
-
elif msg_type == "step":
|
| 228 |
-
try:
|
| 229 |
-
action = RangeAction(
|
| 230 |
-
command=msg_data.get("command", msg.get("command", "")),
|
| 231 |
-
mode=msg_data.get("mode", msg.get("mode", "red")),
|
| 232 |
-
)
|
| 233 |
-
except ValidationError as ve:
|
| 234 |
-
await websocket.send_json({
|
| 235 |
-
"type": "error",
|
| 236 |
-
"data": {"message": str(ve), "code": "validation_error"},
|
| 237 |
-
})
|
| 238 |
-
continue
|
| 239 |
-
|
| 240 |
-
obs = ws_env.step(action)
|
| 241 |
-
await websocket.send_json({
|
| 242 |
-
"type": "observation",
|
| 243 |
-
"data": obs.model_dump(),
|
| 244 |
-
})
|
| 245 |
-
|
| 246 |
-
elif msg_type == "state":
|
| 247 |
-
await websocket.send_json({
|
| 248 |
-
"type": "state",
|
| 249 |
-
"data": ws_env.state.model_dump(),
|
| 250 |
-
})
|
| 251 |
-
|
| 252 |
-
elif msg_type == "close":
|
| 253 |
-
await websocket.close()
|
| 254 |
-
break
|
| 255 |
-
|
| 256 |
-
else:
|
| 257 |
-
await websocket.send_json({
|
| 258 |
-
"type": "error",
|
| 259 |
-
"data": {
|
| 260 |
-
"message": f"Unknown message type: {msg_type!r}",
|
| 261 |
-
"code": "unknown_type",
|
| 262 |
-
},
|
| 263 |
-
})
|
| 264 |
-
|
| 265 |
-
except WebSocketDisconnect:
|
| 266 |
-
ws_env.close()
|
| 267 |
-
logger.debug("WebSocket client disconnected")
|
| 268 |
-
|
| 269 |
-
return fastapi_app
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
# ---------------------------------------------------------------------------
|
| 273 |
-
# Module-level app instance (used by uvicorn)
|
| 274 |
-
# ---------------------------------------------------------------------------
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
def create_app() -> FastAPI:
|
| 278 |
-
"""Create the OpenRange FastAPI application.
|
| 279 |
|
| 280 |
-
|
| 281 |
-
"""
|
| 282 |
-
openenv_app = _try_openenv_app()
|
| 283 |
-
if openenv_app is not None:
|
| 284 |
-
return openenv_app
|
| 285 |
-
return _create_standalone_app()
|
| 286 |
|
| 287 |
|
| 288 |
app = create_app()
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
def main() -> None:
|
| 292 |
-
"""Entry point for ``uv run server`` or ``python -m open_range.server``."""
|
| 293 |
-
import uvicorn
|
| 294 |
-
|
| 295 |
-
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
|
|
| 1 |
+
"""FastAPI application wired through the OpenEnv app factory."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
from fastapi import FastAPI
|
| 6 |
+
from openenv.core.env_server import create_app as create_openenv_app
|
|
|
|
| 7 |
|
| 8 |
+
from open_range.server.console import console_router
|
|
|
|
|
|
|
|
|
|
| 9 |
from open_range.server.environment import RangeEnvironment
|
| 10 |
+
from open_range.server.models import RangeAction, RangeObservation
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
|
| 13 |
+
def create_app() -> FastAPI:
|
| 14 |
+
"""Create the OpenRange app using the standard OpenEnv contract."""
|
| 15 |
+
app = create_openenv_app(
|
| 16 |
+
RangeEnvironment,
|
| 17 |
+
RangeAction,
|
| 18 |
+
RangeObservation,
|
| 19 |
+
env_name="open_range",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
)
|
| 21 |
+
app.include_router(console_router)
|
| 22 |
+
return app
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
+
def main() -> None:
|
| 26 |
+
"""Run the installed package entrypoint via uvicorn."""
|
| 27 |
+
import uvicorn
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
+
uvicorn.run("open_range.server.app:app", host="0.0.0.0", port=8000)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
|
| 32 |
app = create_app()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
uv.lock
CHANGED
|
@@ -1870,7 +1870,6 @@ dependencies = [
|
|
| 1870 |
{ name = "docker" },
|
| 1871 |
{ name = "fastapi" },
|
| 1872 |
{ name = "jinja2" },
|
| 1873 |
-
{ name = "litellm" },
|
| 1874 |
{ name = "openenv-core", extra = ["core"] },
|
| 1875 |
{ name = "pydantic" },
|
| 1876 |
{ name = "pyyaml" },
|
|
@@ -1878,6 +1877,9 @@ dependencies = [
|
|
| 1878 |
]
|
| 1879 |
|
| 1880 |
[package.optional-dependencies]
|
|
|
|
|
|
|
|
|
|
| 1881 |
dev = [
|
| 1882 |
{ name = "httpx" },
|
| 1883 |
{ name = "pytest" },
|
|
@@ -1894,7 +1896,7 @@ requires-dist = [
|
|
| 1894 |
{ name = "fastapi", specifier = ">=0.115" },
|
| 1895 |
{ name = "httpx", marker = "extra == 'dev'", specifier = ">=0.27" },
|
| 1896 |
{ name = "jinja2", specifier = ">=3.1" },
|
| 1897 |
-
{ name = "litellm", specifier = ">=1.30" },
|
| 1898 |
{ name = "openenv-core", extras = ["core"], specifier = ">=0.2.1" },
|
| 1899 |
{ name = "pydantic", specifier = ">=2.0" },
|
| 1900 |
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
|
|
@@ -1902,9 +1904,9 @@ requires-dist = [
|
|
| 1902 |
{ name = "pyyaml", specifier = ">=6.0" },
|
| 1903 |
{ name = "trl", marker = "extra == 'training'", specifier = ">=0.8" },
|
| 1904 |
{ name = "unsloth", marker = "extra == 'training'" },
|
| 1905 |
-
{ name = "uvicorn", specifier = ">=0.
|
| 1906 |
]
|
| 1907 |
-
provides-extras = ["dev", "training"]
|
| 1908 |
|
| 1909 |
[[package]]
|
| 1910 |
name = "openai"
|
|
|
|
| 1870 |
{ name = "docker" },
|
| 1871 |
{ name = "fastapi" },
|
| 1872 |
{ name = "jinja2" },
|
|
|
|
| 1873 |
{ name = "openenv-core", extra = ["core"] },
|
| 1874 |
{ name = "pydantic" },
|
| 1875 |
{ name = "pyyaml" },
|
|
|
|
| 1877 |
]
|
| 1878 |
|
| 1879 |
[package.optional-dependencies]
|
| 1880 |
+
builder = [
|
| 1881 |
+
{ name = "litellm" },
|
| 1882 |
+
]
|
| 1883 |
dev = [
|
| 1884 |
{ name = "httpx" },
|
| 1885 |
{ name = "pytest" },
|
|
|
|
| 1896 |
{ name = "fastapi", specifier = ">=0.115" },
|
| 1897 |
{ name = "httpx", marker = "extra == 'dev'", specifier = ">=0.27" },
|
| 1898 |
{ name = "jinja2", specifier = ">=3.1" },
|
| 1899 |
+
{ name = "litellm", marker = "extra == 'builder'", specifier = ">=1.30" },
|
| 1900 |
{ name = "openenv-core", extras = ["core"], specifier = ">=0.2.1" },
|
| 1901 |
{ name = "pydantic", specifier = ">=2.0" },
|
| 1902 |
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
|
|
|
|
| 1904 |
{ name = "pyyaml", specifier = ">=6.0" },
|
| 1905 |
{ name = "trl", marker = "extra == 'training'", specifier = ">=0.8" },
|
| 1906 |
{ name = "unsloth", marker = "extra == 'training'" },
|
| 1907 |
+
{ name = "uvicorn", specifier = ">=0.27" },
|
| 1908 |
]
|
| 1909 |
+
provides-extras = ["dev", "training", "builder"]
|
| 1910 |
|
| 1911 |
[[package]]
|
| 1912 |
name = "openai"
|