Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,29 @@ jobs:
- name: Run pytest
run: uv run pytest -q

openenv:
name: OpenEnv runtime tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.0

- name: Set up uv
uses: astral-sh/setup-uv@v8.3.0
with:
enable-cache: true
python-version: "3.12"

- name: Install project + dev dependencies
run: uv sync --group dev --frozen

# The `openenv` extra stays out of the lock so the default install and the
# 3.13/3.14 matrix legs stay lean; --with pulls it just for this job, which
# is what makes the runtime invariants (agent path confinement, the
# agent/orchestration split, the client wire contract) actually run in CI
# instead of skipping.
- name: Run OpenEnv export + runtime tests
run: uv run --with openenv --with docker pytest tests/test_openenv_export.py -q

build:
name: Build sdist + wheel
runs-on: ubuntu-latest
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ A dataset that:
- **Is verifiable** — every task carries an executable test (`test_execution`) or a stored oracle diff (`diff_similarity`); your trainer picks the reward type.
- **Is content-addressed** — a `content_hash` over each task; identical artifacts ⇒ identical hash.
- **Trains anywhere via Harbor** — TRL, SkyRL, Prime-RL, Tinker, Miles, Slime, harbor.rl.
- **Runs on OpenEnv unchanged** — `repo2rlenv export --format openenv` wraps a dataset in a deployable [OpenEnv](https://github.com/huggingface/OpenEnv) environment (Docker image or HF Space), and OpenEnv's own `harbor_env` serves the task dirs directly. Either way the verifier's reward is forwarded, not recomputed. See [OpenEnv compatibility](./docs/reference/OPENENV.md).
- **Evaluates with any agent harness** — Claude Code, OpenHands, Codex CLI, Gemini CLI, …
- **Is language-agnostic by spec** — runtime pipelines emit a Dockerfile + shell verifier; `pr_diff` is pure text and works for any language.
- **Publishes natively** to the Hub — `repo2rlenv push` writes a Harbor-compatible `registry.json` so consumers `harbor download` (or `repo2rlenv pull`) with zero glue.
Expand Down Expand Up @@ -193,7 +194,8 @@ Fastest jumps:
- [**RepoLaunch**](https://github.com/microsoft/RepoLaunch) (Microsoft) — LLM-agent env setup; our `bootstrap` is an independent reimplementation
- [**OpenReward**](https://docs.openreward.ai) — ORS protocol + extra trainer integrations above Harbor
- [**SWE-Gym**](https://github.com/SWE-Gym/SWE-Gym) — RL-environment framing for SWE-bench-style tasks
- [**verifiers**](https://github.com/willccbb/verifiers) (Prime Intellect), [**OpenEnv**](https://github.com/meta-pytorch/OpenEnv) (Meta + HF) — adjacent standardization efforts
- [**verifiers**](https://github.com/willccbb/verifiers) (Prime Intellect) — adjacent standardization effort
- [**OpenEnv**](https://github.com/huggingface/OpenEnv) (Meta + HF) — Gymnasium-style env standard; we **emit it as a second target** (`repo2rlenv export --format openenv`) and its `harbor_env` also **runs our task dirs directly** ([compatibility guide](./docs/reference/OPENENV.md))

Every pipeline that draws from external work carries an Acknowledgment block in its `.py` file. No code is copied — implementations are independent and Apache-2.0 licensed. See [`docs/reference/RELATED_WORK.md`](./docs/reference/RELATED_WORK.md) for the full per-pipeline provenance plus adjacent papers, datasets, and frameworks (incl. recent Microsoft and NVIDIA code-RL work).

Expand Down
226 changes: 226 additions & 0 deletions docs/reference/OPENENV.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
# Running Repo2RLEnv datasets on OpenEnv

[OpenEnv](https://github.com/huggingface/OpenEnv) is a Gymnasium-style standard
for containerized agentic environments — `reset()` / `step()` / `state()` over a
WebSocket, deployable as a Docker image or a Hugging Face Space.

Repo2RLEnv datasets run on it with **no change to the task data**. There are two
ways to get there, and both serve the exact directories `generate` emitted:

| | What it is | Use it when |
|---|---|---|
| **`repo2rlenv export --format openenv`** | We emit a deployable environment package around your dataset — `Dockerfile`, `openenv.yaml`, Space card, server. Runs on the `repo2rlenv[openenv]` runtime. | You want a standalone image or a Hugging Face Space, with no OpenEnv checkout involved. |
| **OpenEnv's `harbor_env`** | OpenEnv's own generic runtime for Harbor task directories. Point it at your dataset. | You already work inside an OpenEnv checkout, or you mix our tasks with other Harbor producers. |

Either way the reward comes from your task's own `tests/test.sh` and is only
forwarded. Neither path rewrites, re-authors, or copies the task format.

## Quick start — export a deployable environment

```bash
# 1. Synthesize tasks from a repository
repo2rlenv generate --repo pallets/click --pipeline pr_runtime \
--pipeline-opt limit=10 --llm anthropic/claude-sonnet-4-6 --out ./tasks

# 2. Wrap them in an OpenEnv environment
pip install 'repo2rlenv[openenv]'
repo2rlenv export --format openenv ./tasks --out ./click-env

# 3. Run it. Task containers start on the host daemon, so mount the socket.
docker build -t click-env ./click-env
docker run --rm -p 8000:8000 -v /var/run/docker.sock:/var/run/docker.sock click-env
```

```python
import asyncio
from repo2rlenv.openenv import Repo2RLEnvClient

async def main():
env = Repo2RLEnvClient(base_url="http://localhost:8000")
start = await env.reset(task_id="pallets__click-2951")
print(start.observation.instruction) # instruction.md

await env.write_file("src/click/core.py", patched) # the agent's edit
result = await env.evaluate() # runs tests/test.sh

print(result.reward) # the verifier's score
print(result.observation.info["reward_details"]) # F2P/P2P breakdown
await env.close()

asyncio.run(main())
```

The emitted package is thin — `server/app.py` is two lines over
`repo2rlenv.openenv.build_app`, and `tasks/` holds your task directories byte for
byte. The runtime lives in `repo2rlenv.openenv`, so it is versioned, tested and
upgraded like the rest of the library rather than generated into your output.

`export` writes `README.md` with Hugging Face Space front-matter (`sdk: docker`),
so the emitted directory can be pushed straight to a Space.

### Options

| Flag | Default | Meaning |
|---|---|---|
| `--out` | `<dataset>-openenv` | Where to write the package |
| `--name` | dataset directory name | Environment name in `openenv.yaml` and the Space card |
| `--requirement` | `repo2rlenv[openenv]>=<installed>` | The requirement the image installs — pin a release, a git ref, or a local wheel |
| `--base-image` | `python:3.12-slim` | Base image for the server |
| `--port` | `8000` | Port the server listens on |

## Why there is nothing to convert

The projects sit at different layers and agree on the one contract that
matters — **the reward is produced inside the environment and only forwarded**.

| Layer | Repo2RLEnv | Harbor | OpenEnv |
|---|---|---|---|
| Makes the task | ✅ synthesis pipelines | — | — |
| Defines the task format | emits it | ✅ owns it | consumes it |
| Runs a batch evaluation | — | ✅ `harbor run` | — |
| Serves an episode loop for training | ✅ `export --format openenv` | — | ✅ `reset` / `step` / `state` |

What the runtime does with each file is identical on both sides:

| Task file | Under `harbor run` | Under an OpenEnv runtime |
|---|---|---|
| `instruction.md` | the agent's prompt | the observation returned by `reset()` |
| working directory | what the agent edits | `step(exec / read / write)` |
| `tests/test.sh` | the verifier phase | `step(evaluate)` |
| `/logs/verifier/reward.{json,txt}` | the trial's reward | `observation.reward`, forwarded verbatim |
| `solution/solve.sh` | the oracle agent | `step(solve)` |
| `environment/Dockerfile` | the sandbox image | the sandbox image |

## Serving with OpenEnv's `harbor_env` instead

OpenEnv ships `harbor_env`, a generic runtime for any Harbor task directory. It
needs no export step at all — point it at the dataset:

```bash
# 1. Confirm every task is solvable by its own oracle before training on it
# (from an OpenEnv checkout; `--with docker` supplies the Docker SDK)
PYTHONPATH=src:envs uv run --with docker python envs/harbor_env/examples/validate_taskset.py \
--tasks ./tasks --mode docker

# 2. Serve the task set
HARBOR_TASKS_DIR=./tasks HARBOR_MODE=docker uv run --project envs/harbor_env server
```

```python
# 3. Drive it like any OpenEnv environment
import asyncio
from harbor_env import HarborEnv

async def main():
env = HarborEnv(base_url="http://localhost:8000")
start = await env.reset(task_id="pallets__click-2951")
print(start.observation.instruction) # instruction.md

await env.write_file("src/click/core.py", patched) # the agent's edit
result = await env.evaluate() # runs tests/test.sh

print(result.reward) # the verifier's score
print(result.observation.info["reward_details"]) # F2P/P2P breakdown
await env.close()

asyncio.run(main())
```

Datasets published with `repo2rlenv push` keep their tasks under `tasks/<id>/`;
that layout is discovered automatically, so a Hub dataset can be served directly:

```bash
HARBOR_TASKS_DIR=hf://datasets/<owner>/<name> HARBOR_MODE=docker \
uv run --project envs/harbor_env server
```

## Which tasks can actually run

**Every runtime task needs Docker**, because every pipeline puts the repository
state inside the image the task carries — that is the whole point of the
`environment/Dockerfile` we emit.

| Pipeline | Emits `environment/Dockerfile` | Runnable |
|---|:-:|---|
| `pr_runtime`, `commit_runtime`, `cve_patches` | ✅ | yes, Docker |
| `code_instruct`, `equivalence_tests` | ✅ | yes, Docker |
| `pr_diff` (default `emit_harbor_env=True`) | ✅ | yes, Docker |
| `pr_diff` with `emit_harbor_env=False` | ❌ (no `tests/` either) | no — score the stored diff client-side with `repo2rlenv.reward.calculate_diff_similarity_reward` |

`repo2rlenv export` reports how many of the bundled tasks are runnable and says
so in the emitted Space card; the runtime refuses a task with no image and no
verifier rather than grading an empty directory. `harbor_env`'s Docker-free
`local` backend is for *self-contained* tasks that ship their starting files in
`environment/`, which is not the shape any of our pipelines emit.

Because a Hugging Face Space cannot run Docker-in-Docker, a Space built from
`export` serves the API but cannot start task containers — run Repo2RLEnv task
sets on a Docker-capable host, or use a Space only as a front end.

When a dataset has been published with `repo2rlenv push`,
`[metadata.repo2env.reproducibility]` records `mode = "registry"` and a pullable
`image_ref`. Our runtime pulls that image instead of rebuilding the Dockerfile,
so a pushed dataset starts episodes without a build step.

## Rewards

Our verifiers write the files Harbor specifies, and both runtimes read them in
Harbor's order — `reward.json` first, then `reward.txt`:

| Pipeline | `reward.txt` | Also written |
|---|---|---|
| `pr_runtime`, `commit_runtime`, `cve_patches` | `f2p_rate × p2p_rate` | `reward-details.json` — `resolved`, F2P/P2P counts, regressions, `parse_status` |
| `pr_diff` | 6-component `diff_similarity` | `reward-details.json` — per-component scores, weights, judge status |

Both runtimes surface the sidecar as `observation.info["reward_details"]` and any
flat metrics from `reward.json` as `observation.info["reward_metrics"]`, so the
training signal and the diagnostic breakdown both survive the trip.

A verifier that writes no reward file produces **no reward** on the OpenEnv side
(`reward=None` plus an explicit error), not a `0.0` — the same rule that makes
our `reward.txt` contract trustworthy in the first place.

See [SPEC](./SPEC.md) and [REWARD_SCHEMA](./REWARD_SCHEMA.md) for the full
emitted contract.

## Verified

The task-directory shape we emit — `version = "1.0"`, `[metadata.repo2env]`,
`environment/Dockerfile` with `WORKDIR /workspace`, a `tests/test.sh` writing
`/logs/verifier/reward.txt` — was built with our own emitter and run through all
three runtimes. They agree:

| Runtime | No-op agent | Oracle (`solution/solve.sh`) |
|---|---|---|
| `harbor run` (Harbor 0.20.0) | 0.0 | 1.0 |
| `repo2rlenv export --format openenv` | 0.0 | 1.0 |
| OpenEnv `harbor_env`, `docker` mode | 0.0 | 1.0 |

On the OpenEnv side the working directory resolves to `/workspace` from the
image, the scalar is read from `reward.txt`, and `reward-details.json` arrives
intact as `observation.info["reward_details"]` (F2P/P2P counts, `resolved`,
`parse_status`).

The exported environment was verified as a *built container*, not just in
process: `docker build` on the emitted Dockerfile, run with the host Docker
socket mounted, then driven over the WebSocket API — `reset` → `write_file` →
`evaluate` — taking the same task from 0.0 to 1.0 through an agent edit rather
than the oracle.

## Where the code lives

| Piece | Module |
|---|---|
| Task emission (Harbor format) | `repo2rlenv.emitter.harbor` |
| Environment emission (OpenEnv package) | `repo2rlenv.emitter.openenv` |
| Reading an emitted dataset | `repo2rlenv.openenv.dataset` |
| Reward-file contract | `repo2rlenv.openenv.reward` |
| Docker sandbox | `repo2rlenv.openenv.sandbox` |
| Gymnasium environment | `repo2rlenv.openenv.environment` |
| Trainer-facing client | `repo2rlenv.openenv.Repo2RLEnvClient` |

Only the serving modules need the extra; `repo2rlenv.openenv.dataset` and
`repo2rlenv.emitter.openenv` work with a plain `pip install repo2rlenv`, so
`export` runs without pulling in the OpenEnv stack.

See also [Related work](./RELATED_WORK.md).
2 changes: 1 addition & 1 deletion docs/reference/RELATED_WORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ our datasets into a trainer.
- **NeMo Gym** (NVIDIA) — [NVIDIA-NeMo/Gym](https://github.com/NVIDIA-NeMo/Gym) (Apache-2.0). 100+ RLVR environments for LLMs incl. Mini-SWE-Agent / OpenHands SWE agents and a Harbor harness; the closest analogue to Repo2RLEnv's goal, sharing the Harbor ecosystem.
- **SkyRL** (UC Berkeley NovaSky) — [NovaSky-AI/SkyRL](https://github.com/NovaSky-AI/SkyRL) (Apache-2.0) · *SkyRL-Agent* [arXiv:2511.16108](https://arxiv.org/abs/2511.16108). Full-stack RL training framework targeting long-horizon SWE-bench-style agentic tasks; references Harbor.
- **verifiers** (Prime Intellect) — [PrimeIntellect-ai/verifiers](https://github.com/PrimeIntellect-ai/verifiers). Defines "environment = dataset + harness + rubric/reward", the closest competing spec to our task+verifier emission.
- **OpenEnv** (Meta PyTorch + Hugging Face) — [meta-pytorch/OpenEnv](https://github.com/meta-pytorch/OpenEnv). Gymnasium-style `step()/reset()/state()` standard for containerized agentic environments with a shared Hub; adjacent standardization effort to our Harbor emission + HF Hub bridge.
- **OpenEnv** (Meta PyTorch + Hugging Face) — [huggingface/OpenEnv](https://github.com/huggingface/OpenEnv) (pip `openenv`; also mirrored at [meta-pytorch/OpenEnv](https://github.com/meta-pytorch/OpenEnv)). Gymnasium-style `step()/reset()/state()` standard for containerized agentic environments with a shared Hub. **We target it as a first-class emission format**: `repo2rlenv export --format openenv` wraps a dataset in a deployable environment (`repo2rlenv.openenv` runtime), and OpenEnv's own `harbor_env` serves our Harbor task dirs directly — `instruction.md` becomes the first observation, `tests/test.sh` becomes `step(evaluate)`, and the reward is forwarded from `/logs/verifier/reward.{json,txt}` rather than recomputed. See [OpenEnv compatibility](./OPENENV.md).
- **rLLM** (Agentica) — [rllm-org/rllm](https://github.com/rllm-org/rllm). "Run agent → collect traces → reward → update" post-training stack; used to train DeepSWE on R2E-Gym environments.

### Automated environment setup / dockerization (the `bootstrap/` problem)
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ nav:
- Environment variables: reference/ENV.md
- Registry auth: reference/REGISTRY_AUTH.md
- Related work: reference/RELATED_WORK.md
- OpenEnv compatibility: reference/OPENENV.md
- Contributing:
- Adding a pipeline: contributing/ADDING_A_PIPELINE.md
- Release notes:
Expand Down
10 changes: 10 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ dependencies = [
"tomli-w>=1.2.0",
]

[project.optional-dependencies]
# Serve emitted datasets as OpenEnv environments (`repo2rlenv export --format
# openenv`, `repo2rlenv.openenv`). Kept optional so the default install stays
# lean — reading a dataset needs neither of these.
openenv = [
"openenv>=0.4.1",
"docker>=7.1.0",
"uvicorn[standard]>=0.30.0",
]

[project.scripts]
repo2rlenv = "repo2rlenv.cli:main"

Expand Down
Loading