Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ jobs:
dockerfile: envs/opencode_env/server/Dockerfile
context: envs/opencode_env
- name: opencode-sandbox
dockerfile: envs/opencode_env/sandbox/hf_image/Dockerfile
dockerfile: envs/opencode_env/hf_image/Dockerfile
context: envs/opencode_env
- name: openapp-env
dockerfile: envs/openapp_env/server/Dockerfile
Expand Down
8 changes: 4 additions & 4 deletions docs/source/environments/opencode.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ session.close()
instead, swap the backend (`huggingface_hub>=1.22` ships with the package):

```python
from opencode_env.sandbox import HFSandboxBackend
from openenv.core.sandbox import HFSandboxBackend

factory = OpenCodeSessionFactory(
config=OpenCodeConfig(..., sandbox_home="/root"), # HF sandbox execs as root
Expand All @@ -144,14 +144,14 @@ factory = OpenCodeSessionFactory(

`image="python:3.12"` cold-installs opencode + the proxy deps on every rollout. For faster
rollouts use the pre-baked image (opencode + proxy deps already installed), built by CI from
`sandbox/hf_image/Dockerfile`:
`hf_image/Dockerfile`:

```python
sandbox_backend=HFSandboxBackend(image="ghcr.io/huggingface/openenv-opencode-sandbox:latest")
```

Any backend satisfying the `SandboxBackend` / `SandboxHandle` / `BgJob` protocols in
`opencode_env.sandbox.base` can be plugged in the same way.
`openenv.core.sandbox.base` can be plugged in the same way.

## Building the Docker Image

Expand Down Expand Up @@ -257,7 +257,7 @@ opencode and the proxy's Python deps. Build a one-time template that
ships those pre-installed:

```bash
.venv/bin/python envs/opencode_env/sandbox/build_template.py
.venv/bin/python src/openenv/core/sandbox/build_template.py
# → builds `opencode-rl` template in your E2B account (~1m20s, one-time)
```

Expand Down
8 changes: 4 additions & 4 deletions envs/opencode_env/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ session.close()
instead, swap the backend (`huggingface_hub>=1.22` ships with the package):

```python
from opencode_env.sandbox import HFSandboxBackend
from openenv.core.sandbox import HFSandboxBackend

factory = OpenCodeSessionFactory(
config=OpenCodeConfig(..., sandbox_home="/root"), # HF sandbox execs as root
Expand All @@ -157,14 +157,14 @@ factory = OpenCodeSessionFactory(

`image="python:3.12"` cold-installs opencode + the proxy deps on every rollout. For faster
rollouts use the pre-baked image (opencode + proxy deps already installed), built by CI from
`sandbox/hf_image/Dockerfile`:
`hf_image/Dockerfile`:

```python
sandbox_backend=HFSandboxBackend(image="ghcr.io/huggingface/openenv-opencode-sandbox:latest")
```

Any backend satisfying the `SandboxBackend` / `SandboxHandle` / `BgJob` protocols in
`opencode_env.sandbox.base` can be plugged in the same way.
`openenv.core.sandbox.base` can be plugged in the same way.

## Building the Docker Image

Expand Down Expand Up @@ -270,7 +270,7 @@ opencode and the proxy's Python deps. Build a one-time template that
ships those pre-installed:

```bash
.venv/bin/python envs/opencode_env/sandbox/build_template.py
.venv/bin/python src/openenv/core/sandbox/build_template.py
# → builds `opencode-rl` template in your E2B account (~1m20s, one-time)
```

Expand Down
2 changes: 1 addition & 1 deletion envs/opencode_env/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
RolloutResult,
RolloutTurn,
)
from .sandbox import E2BSandboxBackend, SandboxBackend, SandboxHandle
from openenv.core.sandbox import E2BSandboxBackend, SandboxBackend, SandboxHandle
Comment thread
cursor[bot] marked this conversation as resolved.
from .task import OpenCodeTask

__all__ = [
Expand Down
8 changes: 5 additions & 3 deletions envs/opencode_env/harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,17 @@
proxy_trace_path,
system_prompt_path,
)
from .sandbox.base import BgJob, SandboxBackend, SandboxHandle
from openenv.core import sandbox as _core_sandbox
from openenv.core.sandbox import BgJob, SandboxBackend, SandboxHandle
from .task import OpenCodeTask


# Mode B proxy port. In-sandbox paths derive from config.sandbox_home (opencode_runtime).
_PROXY_PORT = 7000

# Local proxy source, uploaded to proxy_source_path(config) unless already baked in.
_PROXY_SOURCE_PATH = Path(__file__).parent / "sandbox" / "interception.py"
# Proxy source (shared, in openenv.core.sandbox), uploaded to proxy_source_path(config)
# unless already baked into the image.
_PROXY_SOURCE_PATH = Path(_core_sandbox.__file__).parent / "interception.py"


Verifier = Callable[[SandboxHandle, OpenCodeTask], VerifyResult]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# time so rollouts skip the cold install. Use with OpenCodeConfig(sandbox_home="/root").
#
# Build context = envs/opencode_env:
# docker build -f sandbox/hf_image/Dockerfile -t <user>/opencode-rl:latest .
# docker build -f hf_image/Dockerfile -t <user>/opencode-rl:latest .
# docker push <user>/opencode-rl:latest

FROM python:3.12
Expand All @@ -18,13 +18,14 @@ RUN pip install --no-cache-dir "fastapi>=0.104" "uvicorn[standard]>=0.24" "httpx
RUN curl -fsSL https://opencode.ai/install | bash \
&& /root/.opencode/bin/opencode --version

# Directory layout the harness expects, plus the pre-copied proxy source.
# Directory layout the harness expects. The proxy source (interception.py) lives in
# openenv.core.sandbox and is uploaded by the harness at rollout start, so it is not
# baked here (matches the pi-sandbox image).
RUN mkdir -p /root/.config/opencode \
/root/logs/agent \
/root/logs/verifier \
/root/task \
/root/workdir \
/root/proxy
COPY sandbox/interception.py /root/proxy/interception.py

WORKDIR /root/workdir
3 changes: 1 addition & 2 deletions envs/opencode_env/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,9 @@ server = "opencode_env.server.app:main"
include-package-data = true
packages = [
"opencode_env",
"opencode_env.sandbox",
"opencode_env.server",
]
package-dir = { "opencode_env" = ".", "opencode_env.sandbox" = "sandbox", "opencode_env.server" = "server" }
package-dir = { "opencode_env" = ".", "opencode_env.server" = "server" }

[tool.setuptools.package-data]
opencode_env = ["**/*.md"]
4 changes: 2 additions & 2 deletions envs/opencode_env/server/opencode_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,12 @@ def __init__(self) -> None:
try:
from ..config import OpenCodeConfig
from ..harness import OpenCodeSessionFactory
from ..sandbox import E2BSandboxBackend
from openenv.core.sandbox import E2BSandboxBackend
Comment thread
cursor[bot] marked this conversation as resolved.
from ..task import OpenCodeTask
except ImportError: # pragma: no cover
from config import OpenCodeConfig # type: ignore
from harness import OpenCodeSessionFactory # type: ignore
from sandbox import E2BSandboxBackend # type: ignore
from openenv.core.sandbox import E2BSandboxBackend

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docker builds lose sandbox module

High Severity

Sandbox code was removed from the env package and both import paths now load E2BSandboxBackend from openenv.core.sandbox, but the env still depends on openenv>=0.3.0 with a frozen lock on 0.3.1. CI Docker builds use only the env context plus that lock, so the published image no longer contains the sandbox layer and rollouts fail on import.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f363315. Configure here.

from task import OpenCodeTask # type: ignore

self._CommandResult = CommandResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""Sandbox backends for the OpenCode harness.
"""Sandbox backends for OpenEnv harnesses.

The primitive ships with :class:`E2BSandboxBackend` as the default; any backend
that satisfies the :class:`SandboxBackend` / :class:`SandboxHandle` protocols
can be swapped in.
Shared sandbox layer (protocols + E2B / Hugging Face backends + the interception
proxy) used by loop-owning harness environments. Any backend that satisfies the
:class:`SandboxBackend` / :class:`SandboxHandle` protocols can be swapped in.

The ``e2b`` import is wrapped in ``try/except`` so this package can be loaded
in environments where ``e2b`` isn't installed (CI smoke tests, lint runs).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Protocol, runtime_checkable
from typing import Protocol, runtime_checkable


@dataclass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
import sys
from pathlib import Path

from e2b import Template, default_build_logger
from e2b import default_build_logger, Template


_ENV_DIR = Path(__file__).resolve().parent
Expand Down Expand Up @@ -126,8 +126,7 @@ def main(argv: list[str] | None = None) -> int:
print("ERROR: E2B_API_KEY required.", file=sys.stderr)
return 2

print(f"Building template '{args.name}' "
f"(proxy source: {_PROXY_SOURCE})")
print(f"Building template '{args.name}' (proxy source: {_PROXY_SOURCE})")
print(f"Skip cache: {args.skip_cache}")
print()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from e2b import Sandbox
from e2b.sandbox_sync.commands.command_handle import CommandHandle

from .base import BgJob, ExecResult, SandboxBackend, SandboxHandle
from .base import BgJob, ExecResult, SandboxHandle


class E2BBgJob:
Expand Down Expand Up @@ -46,9 +46,7 @@ def pid(self) -> int:
def wait(self, timeout: float | None = None) -> int:
self._thread.join(timeout)
if self._thread.is_alive():
raise TimeoutError(
f"Background command did not exit within {timeout}s"
)
raise TimeoutError(f"Background command did not exit within {timeout}s")
if self._error is not None:
# E2B raises CommandExitException on non-zero; treat as exit code.
code = getattr(self._error, "exit_code", None)
Expand Down
14 changes: 11 additions & 3 deletions envs/opencode_env/sandbox/hf.py → src/openenv/core/sandbox/hf.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,17 @@ def wait(self, timeout: float | None = None) -> int:
# Finished processes stay listed (running=False); a vanished pid means
# the sandbox was torn down mid-run, not a clean exit.
if proc is None:
raise RuntimeError(f"process {self.pid} vanished (sandbox torn down?)")
raise RuntimeError(
f"process {self.pid} vanished (sandbox torn down?)"
)
if not proc.running:
return int(proc.exit_code) if proc.exit_code is not None else 0
if deadline is not None:
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError(f"Background command did not exit within {timeout}s")
raise TimeoutError(
f"Background command did not exit within {timeout}s"
)
time.sleep(min(_WAIT_POLL_INTERVAL_S, remaining))
else:
time.sleep(_WAIT_POLL_INTERVAL_S)
Expand Down Expand Up @@ -134,7 +138,11 @@ def start_bg(

def write_text(self, path: str, content: str) -> None:
parent = str(PurePosixPath(path).parent)
if parent not in ("", "/", "."): # "." == relative path with no parent dir to create
if parent not in (
"",
"/",
".",
): # "." == relative path with no parent dir to create
self._sbx.files.mkdir(parent)
self._sbx.files.write(path, content)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,7 @@ async def chat_completions(request: Request) -> Response:
try:
body = json.loads(raw_body)
except json.JSONDecodeError:
return JSONResponse(
status_code=400, content={"error": "invalid json body"}
)
return JSONResponse(status_code=400, content={"error": "invalid json body"})

forwarded_body = _prepare_forwarded_body(body, cfg)
headers = {
Expand Down Expand Up @@ -338,7 +336,7 @@ async def _stream() -> Any:
yield line + "\n"
if not line.startswith("data:"):
continue
data = line[len("data:"):].strip()
data = line[len("data:") :].strip()
if data == "[DONE]":
continue
try:
Expand Down Expand Up @@ -381,7 +379,11 @@ def _accumulate_stream_chunk(chunk: dict[str, Any], acc: dict[str, Any]) -> None
tc_idx = tc.get("index", 0)
bucket = acc["tool_calls_by_idx"].setdefault(
(idx, tc_idx),
{"id": None, "type": "function", "function": {"name": "", "arguments": ""}},
{
"id": None,
"type": "function",
"function": {"name": "", "arguments": ""},
},
)
if tc.get("id"):
bucket["id"] = tc["id"]
Expand Down Expand Up @@ -487,8 +489,7 @@ def _strip_logprobs(response_json: dict[str, Any]) -> dict[str, Any]:
choices = out.get("choices")
if isinstance(choices, list):
out["choices"] = [
{k: v for k, v in (ch or {}).items() if k != "logprobs"}
for ch in choices
{k: v for k, v in (ch or {}).items() if k != "logprobs"} for ch in choices
]
return out

Expand Down Expand Up @@ -537,9 +538,7 @@ def start(self) -> None:
lifespan="on",
)
self._server = uvicorn.Server(config)
self._thread = threading.Thread(
target=self._run_server, daemon=True
)
self._thread = threading.Thread(target=self._run_server, daemon=True)
self._thread.start()
# Wait for the server to accept connections.
deadline = time.time() + 10
Expand Down
2 changes: 1 addition & 1 deletion tests/envs/test_opencode_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ def _exec_with_retry(self, *args, **kwargs):
def test_interception_cli_reads_upstream_key_from_env(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opencode_env.sandbox import interception
from openenv.core.sandbox import interception

captured = {}

Expand Down
6 changes: 3 additions & 3 deletions tests/envs/test_opencode_hf_sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
if not hasattr(huggingface_hub, "Sandbox"): # pragma: no cover - only on old hub
huggingface_hub.Sandbox = type("Sandbox", (), {}) # type: ignore[attr-defined]

import opencode_env.sandbox.hf as hf_mod # noqa: E402
from opencode_env.sandbox.hf import HFBgJob # noqa: E402
import openenv.core.sandbox.hf as hf_mod # noqa: E402
from openenv.core.sandbox.hf import HFBgJob # noqa: E402


class _FakeProcess:
Expand Down Expand Up @@ -204,7 +204,7 @@ def kill(self) -> None:
# HFSandboxHandle / HFSandboxBackend
# --------------------------------------------------------------------------

from opencode_env.sandbox.hf import HFSandboxBackend, HFSandboxHandle # noqa: E402
from openenv.core.sandbox.hf import HFSandboxBackend, HFSandboxHandle # noqa: E402


class _FakeCmdResult:
Expand Down
Loading