Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
eee41a7
capture: dialect-agnostic token capture for agent rollouts
adithya-s-k Aug 2, 2026
5976ac9
harbor: run Harbor tasks as an OpenEnv environment
adithya-s-k Aug 2, 2026
f1ae0e4
cli: openenv harbor info / rollout / serve / push
adithya-s-k Aug 2, 2026
341eebe
harbor_env: deployment packaging for a Space
adithya-s-k Aug 2, 2026
1b9b324
tests: port ownership, request normalisation, hosted serving
adithya-s-k Aug 2, 2026
d44f42c
docs: add the harbor_env stub and link it
adithya-s-k Aug 2, 2026
c684348
tests: cover the graph, rewards, seams, discovery and rendering
adithya-s-k Aug 2, 2026
af14859
harbor: fix the process-env lock breaking all concurrent rollouts
adithya-s-k Aug 2, 2026
17e080c
harbor: a FATAL capture finding must make the rollout unusable
adithya-s-k Aug 2, 2026
173acd8
harbor: cross-check pi against its own session log
adithya-s-k Aug 3, 2026
49dbdfa
harbor: recover the ATIF trajectory for hermes and openclaw
adithya-s-k Aug 3, 2026
559b30d
capture: name the upstream project in the dialect provenance
adithya-s-k Aug 3, 2026
33516cd
docs: rewrite the harbor_env guide, and make its example importable
adithya-s-k Aug 3, 2026
af2459e
docs: lead on multi-harness training, and document the CLI properly
adithya-s-k Aug 3, 2026
0d5c1de
harbor: stop calling asyncio.run on server-reachable paths
adithya-s-k Aug 3, 2026
24e7bcc
capture: fix the cloudflare forwarder, which never worked
adithya-s-k Aug 3, 2026
6ef13b5
capture: set the served-model marker before transforming, not after
adithya-s-k Aug 3, 2026
f924b40
capture: export every agent root, not only the first
adithya-s-k Aug 3, 2026
ecfb90a
harbor: restore the process env after a rollout, and fix the install …
adithya-s-k Aug 3, 2026
eb6d09e
harbor: give every turn its own prompt ids, and stop deriving turn bo…
adithya-s-k Aug 3, 2026
a18b592
capture: leave the caller's messages alone, and accept an engine URL …
adithya-s-k Aug 3, 2026
628059d
harbor: export a forked prefix once, and never leave a capture server…
adithya-s-k Aug 3, 2026
b592acc
harbor: sort imports the way usort does
adithya-s-k Aug 3, 2026
b2145d3
capture: accept SGLang as an engine, not just vLLM
adithya-s-k Aug 4, 2026
ad059f5
harbor: stop the tool-output box from eating the page scroll
adithya-s-k Aug 4, 2026
902699f
capture: authenticate to the upstream, and negotiate what it accepts
adithya-s-k Aug 5, 2026
84ac60c
capture: probe what an endpoint returns, instead of demanding token ids
adithya-s-k Aug 5, 2026
bde46be
capture: an eval rollout keeps its trace, and never a training contract
adithya-s-k Aug 5, 2026
59a09f5
harbor: carry the key and the capture level through a rollout
adithya-s-k Aug 5, 2026
125288b
harbor: fix the harnesses a compatibility matrix caught
adithya-s-k Aug 5, 2026
4d10afd
harbor ui: an API key field, and say plainly what you are about to get
adithya-s-k Aug 5, 2026
67597d8
tests: cover the negotiation, the eval path, and whether the logprobs…
adithya-s-k Aug 5, 2026
1a5014c
docs: two kinds of rollout, and what each endpoint gives you
adithya-s-k Aug 5, 2026
95ceb5c
Merge remote-tracking branch 'upstream/main' into harbor-integration
adithya-s-k Aug 6, 2026
e69e935
capture: link turns symmetrically, and catch retries of the first call
adithya-s-k Aug 6, 2026
846d805
capture: refuse to present an untrainable turn as a trainable one
adithya-s-k Aug 6, 2026
1a15811
harbor: keep a bad reward or a bad sequence from reaching the trainer
adithya-s-k Aug 6, 2026
14fc268
capture: gate the session routes, and survive odd upstream replies
adithya-s-k Aug 6, 2026
9838206
capture: dialect and transport fixes from review
adithya-s-k Aug 6, 2026
77bcd15
harbor: assume the weaker capture tier until the probe says otherwise
adithya-s-k Aug 6, 2026
7f0e26a
tests: cover the review findings
adithya-s-k Aug 6, 2026
3357767
harbor: one upstream rename should degrade one agent, not eight
adithya-s-k Aug 6, 2026
31d0939
capture: record the sampling parameters each turn was drawn with
adithya-s-k Aug 6, 2026
ad14f55
harbor: mask the aux node's own tokens, not its neighbours'
adithya-s-k Aug 6, 2026
3c71f5e
capture: count tokens in Google's request shape too
adithya-s-k Aug 6, 2026
1c8479c
harbor: don't strand the capture proxy when the tunnel fails
adithya-s-k Aug 6, 2026
4b857b6
capture: sample from the policy when the rollout is trainable
adithya-s-k Aug 6, 2026
5986b79
scripts: measure the top_p truncation bias in logprob_parity
adithya-s-k Aug 6, 2026
d2fc5f5
capture: the engine belongs to the rollout, not to the server
adithya-s-k Aug 20, 2026
5e82fe2
harbor: name the engine per rollout, alongside the harness and the sa…
adithya-s-k Aug 20, 2026
7a2056a
harbor: carry each turn's request on the wire
adithya-s-k Aug 20, 2026
9c209af
harbor: serve datasets without an engine, and cap agent output budgets
adithya-s-k Aug 20, 2026
1483c5d
Merge remote-tracking branch 'upstream/main' into harbor-integration
adithya-s-k Aug 20, 2026
9dcde4b
harbor: a served deployment does not need an engine to be useful
adithya-s-k Aug 20, 2026
c8535e5
capture: rewrite the outgoing model from the session's engine, not th…
adithya-s-k Aug 20, 2026
3924da8
capture: formatting (usort + ruff), as CI runs it
adithya-s-k Aug 20, 2026
c03e50e
harbor: let the caller bound a rollout
adithya-s-k Aug 20, 2026
33b57cc
harbor_env: loop-owning sessions, so a stock TRL worker can train on …
adithya-s-k Aug 20, 2026
e5d15fb
harbor_env: four review findings on the session factory
adithya-s-k Aug 20, 2026
6c0ad30
core: close() on an MCP client must actually close it
adithya-s-k Aug 24, 2026
33e2df1
harbor: n_trainable_tokens must agree with the turns beside it
adithya-s-k Aug 24, 2026
c2dac3b
harbor ui: run against the endpoint the user validated
adithya-s-k Aug 24, 2026
770ea8d
harbor: compute the token total after the turns exist, and let Run re…
adithya-s-k Aug 24, 2026
acd3c78
harbor_env: measure_prompt_skew has to decode tool-call arguments, li…
adithya-s-k Aug 24, 2026
b8b90e9
harbor: remove hermes support
adithya-s-k Aug 24, 2026
7e4688e
harbor harness: one client per rollout, not one per factory
adithya-s-k Aug 24, 2026
50e21cb
harbor: release both halves on teardown, and do not leak the proc-env…
adithya-s-k Aug 24, 2026
2854581
harbor tests: drop the asyncio import the hermes deletion orphaned
adithya-s-k Aug 24, 2026
ef11f95
harbor: let a caller bound how many steps an agent takes
adithya-s-k Aug 24, 2026
0ef184a
harbor harness: say when a rollout's packed row is big enough to OOM …
adithya-s-k Aug 24, 2026
5be8cb1
harbor harness: refuse an empty indices list instead of silently sele…
adithya-s-k Aug 24, 2026
9bf5e7f
harbor: one E2B template per distinct environment, not per task
adithya-s-k Aug 25, 2026
ca4d2e5
harbor: stop credential-by-env harnesses serialising, with context-lo…
adithya-s-k Aug 25, 2026
0308ac0
Merge upstream/main into harbor-integration
adithya-s-k Aug 27, 2026
77e6920
harbor: run the repo's own usort+ruff pipeline over three stragglers
adithya-s-k Sep 2, 2026
962ffcb
harbor: attribute a re-rendered system prompt to its parent, and let …
adithya-s-k Sep 2, 2026
c8bc449
harbor: 10 validated harnesses, and say why the other five are not
adithya-s-k Sep 2, 2026
664a93c
Merge upstream/main into harbor-integration (brings the RFC 008 rever…
adithya-s-k Sep 2, 2026
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,6 @@ docs/source/_env_assets/
# Sphinx-gallery generated output
docs/source/auto_getting_started/
docs/source/sg_execution_times.rst

# Gradio UI build artifacts
.gradio/
2 changes: 2 additions & 0 deletions docs/source/_toctree.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
title: Terminus
- local: environments/coding_tools
title: Coding Tools
- local: environments/harbor
title: Harbor
- local: environments/chat
title: Chat
- local: environments/atari
Expand Down
54 changes: 54 additions & 0 deletions docs/source/environments/harbor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<!-- openenv-source: harbor_env -->
# harbor_env

Run a Harbor task with a coding agent and capture every token id and per-token logprob it produced,
ready to train on.

Harbor supplies the task datasets, sandbox backends, agents and verifiers. This environment adds the
OpenEnv surface: dataset discovery over the Task API, one `run_rollout` MCP tool, and a capture proxy
between the agent and your model.

## Configure

| variable | meaning |
|---|---|
| `OPENENV_LLM_URL` | OpenAI-spec endpoint (vLLM). **Required.** |
| `OPENENV_DATASETS` | comma-separated dataset specs — HF repo id, local dir, or Harbor `name@version` |
| `OPENENV_MODEL` | served model id; read from the engine when it serves exactly one |
| `E2B_API_KEY` | offer the `e2b` sandbox |
| `MODAL_TOKEN_ID`, `MODAL_TOKEN_SECRET` | offer the `modal` sandbox |

The engine **must** be started with:

```
--return-tokens-as-token-ids --logprobs-mode processed_logprobs
```

Without them it answers every request normally and returns no token ids, so captured rollouts are
empty and nothing reports an error. The server refuses to start rather than let that happen.

## Use

```python
from harbor_env import HarborEnv

with HarborEnv(base_url="http://localhost:8000") as env:
split = env.splits()[0]["name"]
result = env.run_rollout(split=split, task_index=0, harness="opencode", sandbox="e2b")
print(result.reward, len(result.turns))
```

`harness` and `sandbox` are per-call, so consecutive rollouts can use different agents and different
backends against the same server.

## Notes

Two ports when run locally: the env server faces trainers and browsers, the capture proxy faces the
sandbox and is the only thing published. On a hosted platform there is one port and one public URL,
so the proxy is mounted on the env server's own app at `/capture` and nothing is forwarded. It still
refuses callers without a registered session id, which is what keeps a public mount from being an
open relay.

Capture quality and task success are independent. A rollout can be captured perfectly and score 0
because the model was wrong; a reward of 1 with unusable capture is worse than useless for training.
Both are reported separately.
62 changes: 62 additions & 0 deletions envs/harbor_env/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: Harbor
emoji: ⚓
colorFrom: blue
colorTo: indigo
sdk: docker
app_port: 8000
---

# harbor_env

Run a Harbor task with a coding agent and capture every token id and per-token logprob it produced,
ready to train on.

Harbor supplies the task datasets, sandbox backends, agents and verifiers. This environment adds the
OpenEnv surface: dataset discovery over the Task API, one `run_rollout` MCP tool, and a capture proxy
between the agent and your model.

## Configure

| variable | meaning |
|---|---|
| `OPENENV_LLM_URL` | OpenAI-spec endpoint (vLLM). **Required.** |
| `OPENENV_DATASETS` | comma-separated dataset specs — HF repo id, local dir, or Harbor `name@version` |
| `OPENENV_MODEL` | served model id; read from the engine when it serves exactly one |
| `E2B_API_KEY` | offer the `e2b` sandbox |
| `MODAL_TOKEN_ID`, `MODAL_TOKEN_SECRET` | offer the `modal` sandbox |

The engine **must** be started with:

```
--return-tokens-as-token-ids --logprobs-mode processed_logprobs
```

Without them it answers every request normally and returns no token ids, so captured rollouts are
empty and nothing reports an error. The server refuses to start rather than let that happen.

## Use

```python
from harbor_env import HarborEnv

with HarborEnv(base_url="http://localhost:8000") as env:
split = env.splits()[0]["name"]
result = env.run_rollout(split=split, task_index=0, harness="opencode", sandbox="e2b")
print(result.reward, len(result.turns))
```

`harness` and `sandbox` are per-call, so consecutive rollouts can use different agents and different
backends against the same server.

## Notes

Two ports when run locally: the env server faces trainers and browsers, the capture proxy faces the
sandbox and is the only thing published. On a hosted platform there is one port and one public URL,
so the proxy is mounted on the env server's own app at `/capture` and nothing is forwarded. It still
refuses callers without a registered session id, which is what keeps a public mount from being an
open relay.

Capture quality and task success are independent. A rollout can be captured perfectly and score 0
because the model was wrong; a reward of 1 with unusable capture is worse than useless for training.
Both are reported separately.
9 changes: 9 additions & 0 deletions envs/harbor_env/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""harbor_env — run Harbor tasks with token-level capture.

The implementation lives in `openenv.harbor` and `openenv.core.harness.capture`; this package is
deployment packaging only (manifest, Dockerfile, ASGI entry point).
"""

from openenv.harbor.models import HarborRolloutResult, HarborTaskRef, HarborTurn

__all__ = ["HarborRolloutResult", "HarborTaskRef", "HarborTurn"]
5 changes: 5 additions & 0 deletions envs/harbor_env/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Typed client for a deployed harbor_env."""

from openenv.harbor.client import HarborEnv

__all__ = ["HarborEnv"]
17 changes: 17 additions & 0 deletions envs/harbor_env/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Wire types, re-exported so `from harbor_env.models import ...` works like other envs."""

from openenv.harbor.models import (
HarborRolloutResult,
HarborState,
HarborStepResult,
HarborTaskRef,
HarborTurn,
)

__all__ = [
"HarborRolloutResult",
"HarborState",
"HarborStepResult",
"HarborTaskRef",
"HarborTurn",
]
6 changes: 6 additions & 0 deletions envs/harbor_env/openenv.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
spec_version: 1
name: harbor_env
type: space
runtime: fastapi
app: server.app:app
port: 8000
29 changes: 29 additions & 0 deletions envs/harbor_env/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[project]
name = "openenv-harbor-env"
version = "0.1.0"
description = "Run Harbor tasks with a coding agent and capture token-level training data"
requires-python = ">=3.12"
dependencies = [
"openenv",
# Every sandbox backend, listed individually rather than via `harbor[cloud]`.
# `harbor[cloud]` cannot be installed at all: it pulls both `langsmith[sandbox]`, which
# requires `websockets>=15`, and `tensorlake`, which requires `websockets>=13,<14`. uv
# reports the pair as unsatisfiable and the image build fails. Neither is a sandbox backend
# we offer, so both are dropped and everything else kept. Re-check on a Harbor upgrade.
"harbor[e2b,modal,daytona,gke,ec2,runloop,novita,blaxel,beam,islo,opensandbox,cwsandbox,use-computer,cua]>=0.20.0",
"huggingface_hub>=1.12",
"fastapi>=0.104",
"uvicorn[standard]>=0.24",
"httpx>=0.27",
"gradio>=5",
]

[project.scripts]
server = "server.app:main"

[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
packages = ["server"]
36 changes: 36 additions & 0 deletions envs/harbor_env/server/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
ARG BASE_IMAGE=ghcr.io/huggingface/openenv-base:latest
FROM ${BASE_IMAGE} AS builder

# Harbor requires Python >= 3.12 while openenv-base ships 3.11, so `uv sync` downloads its own
# interpreter and the venv's bin/python becomes a symlink into uv's install dir. Pinning that dir
# (and creating it up front, so the COPY below cannot fail when uv reuses a system interpreter)
# is what lets the runtime stage carry the interpreter the venv actually points at. Without it the
# venv arrives with a dangling bin/python and the container dies with "not found".
ENV UV_PYTHON_INSTALL_DIR=/opt/uv-python
RUN mkdir -p /opt/uv-python

WORKDIR /app/env
COPY . /app/env
RUN --mount=type=cache,target=/root/.cache/uv \
if [ -f uv.lock ]; then uv sync --frozen --no-editable; else uv sync --no-editable; fi

FROM ${BASE_IMAGE}
COPY --from=builder /opt/uv-python /opt/uv-python
COPY --from=builder /app/env/.venv /app/.venv
COPY --from=builder /app/env /app/env

# Fail at build time rather than at startup if the interpreter did not survive the stage boundary.
RUN /app/.venv/bin/python -c "import sys; print('venv python', sys.version)"

ENV PATH="/app/.venv/bin:$PATH"
# `harbor push` bundles the working tree's openenv/ into /app/env when pushing from a source
# checkout; PYTHONPATH puts it ahead of the released wheel in site-packages, which has no
# `openenv.harbor` until this lands upstream.
ENV PYTHONPATH="/app/env:$PYTHONPATH"
ENV ENABLE_WEB_INTERFACE=true

HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD /app/.venv/bin/python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1

EXPOSE 8000
CMD ["sh", "-c", "cd /app/env && exec /app/.venv/bin/python -m uvicorn server.app:app --host 0.0.0.0 --port 8000"]
Empty file.
87 changes: 87 additions & 0 deletions envs/harbor_env/server/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""ASGI entry point for a deployed harbor_env.

Everything is read from the environment so the same image serves any dataset and any engine without
a rebuild — which is what makes this deployable to a Space:

OPENENV_DATASETS comma-separated dataset specs (HF repo id, local dir, harbor name@version)
OPENENV_LLM_URL OpenAI-spec inference endpoint
OPENENV_MODEL served model id; read from the engine when it serves exactly one
E2B_API_KEY / MODAL_TOKEN_ID+MODAL_TOKEN_SECRET whichever sandboxes you want offered

The capture proxy rides on this same app rather than on a second port. A Space publishes exactly one
port and one URL, so the proxy is mounted at `/capture` and the sandbox reaches it at
`https://<space>.hf.space/capture`. Nothing is forwarded and no second listener is opened.
"""

from __future__ import annotations

import os

from openenv.harbor.serving import HarborService, build_app

_DATASETS = [
d.strip() for d in os.environ.get("OPENENV_DATASETS", "").split(",") if d.strip()
]
_LLM_URL = os.environ.get("OPENENV_LLM_URL", "")
_MODEL = os.environ.get("OPENENV_MODEL", "")
_LLM: dict = {}
Comment thread
adithya-s-k marked this conversation as resolved.

# Ask the endpoint what it serves when `OPENENV_MODEL` was not set, the same way `harbor serve` does.
# Without this the proxy has no served model id and stops rewriting `model` on the way upstream, so
# whatever name the harness happened to use is forwarded verbatim and the engine rejects it. The
# report is kept so `capabilities()` can state whether capture is actually supported here.
if _LLM_URL:
try:
from openenv.core.harness.capture.validate_llm import list_models, validate_llm

if not _MODEL:
served = list_models(_LLM_URL)
_MODEL = served[0] if len(served) == 1 else ""
if _MODEL:
_report = validate_llm(_LLM_URL, _MODEL)
_LLM = {
"url": _LLM_URL,
"model": _report.model,
"ok": _report.ok,
"findings": _report.findings,
"served_models": _report.served_models,
}
except Exception as exc: # noqa: BLE001 - a Space must still boot so the UI can show the fault
_LLM = {
"url": _LLM_URL,
"model": _MODEL,
"ok": False,
"findings": [
f"could not reach the LLM at startup: {type(exc).__name__}: {exc}"
],
}

# Resolve capture before the app is built. A Space gives no separate boot hook, the UI needs the
# proxy's public URL to exist by the time anyone presses Run, and `build_app` has to see the service
# in order to mount it.
if _LLM_URL:
_service = HarborService(
llm_url=_LLM_URL,
model=_MODEL,
datasets=_DATASETS,
capture_port=int(os.environ.get("OPENENV_CAPTURE_PORT", "8100")),
expose=os.environ.get("OPENENV_EXPOSE", "gradio"),
)
# On a Space this only computes the public URL and flags the app for mounting; off one it
# publishes the capture port the usual way.
_service.start()
HarborService.set_current(_service)

os.environ.setdefault("ENABLE_WEB_INTERFACE", "true")

app = build_app(datasets=_DATASETS, llm_url=_LLM_URL, model=_MODEL, llm=_LLM)


def main() -> None:
import uvicorn

uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "8000")))


if __name__ == "__main__":
main()
11 changes: 11 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ modal = [
inspect = [
"inspect-ai>=0.3.0",
]
harbor = [
# Every sandbox backend. Bare `harbor` gives one that lists all 23 backends and can
# instantiate none of them: each raises MissingExtraError from its constructor, which
# surfaces as a failed rollout rather than as a missing dependency.
#
# Not `harbor[cloud]`, which is unsatisfiable: it pulls `langsmith[sandbox]`
# (websockets>=15) and `tensorlake` (websockets>=13,<14) together.
#
# Harbor requires Python >= 3.12, so this extra is excluded from the 3.11 CI leg.
"harbor[e2b,modal,daytona,gke,ec2,runloop,novita,blaxel,beam,islo,opensandbox,cwsandbox,use-computer,cua]>=0.20.0; python_version >= '3.12'",
]

[project.scripts]
openenv = "openenv.cli.__main__:main"
Expand Down
6 changes: 6 additions & 0 deletions src/openenv/cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
build,
collect,
fork,
harbor,
import_env,
init,
push,
Expand All @@ -37,6 +38,11 @@
app.command(name="build", help="Build Docker images for OpenEnv environments")(
build.build
)
app.add_typer(
harbor.app,
name="harbor",
help="Run Harbor tasks with token-level capture (requires: pip install openenv[harbor])",
)
app.command(
name="validate", help="Validate environment structure and deployment readiness"
)(validate.validate)
Expand Down
Loading
Loading