envs/opencode_env: OpenCode coding-agent harness environment#656
Conversation
Re-introduces the OpenCode env that was developed in PR huggingface#603 (merged into the stacked feature/harness-interface branch and dropped before that branch landed on main). With openenv-core 0.3.0 now published with the harness runtime, this lands cleanly on main without the stack dependency. Environment - envs/opencode_env/: deployable env wrapping the OpenCode CLI agent in an E2B sandbox against any OpenAI-compatible LLM endpoint. - Single MCP tool run_rollout (instruction + setup[] + verify[] bash; reward = passed_verify / total or override via reward.txt). - Endpoint shorthand catalog (vllm / openai / hf_router) resolves base_url / api_key / model from env vars. - transparent_proxy mode runs an in-sandbox FastAPI proxy that injects logprobs=true on forwarded /v1/chat/completions, captures per-turn logprobs to a JSON-lines trace (for GRPO), and strips logprobs from what opencode sees. - black_box mode skips the proxy for SFT / eval rollouts. - Pre-baked E2B template support drops sandbox cold start ~2min -> ~6s. - Streaming Gradio UI at /web with live phase log. - Implements ResourceSession / ResourceSessionFactory from openenv.core.harness. Pinned to openenv-core[core]>=0.3.0 (no more git-ref pin to a fork). Tests - tests/envs/test_opencode_env.py: 14 offline unit tests covering catalog resolution, model serialization, task coercion. No E2B / LLM / network. Plus one integration test gated on a deployed Space. Example - examples/opencode_env_simple.py: end-to-end binary_search rollout against the deployed HF Space, prints reward + per-turn logprobs. CI - .github/workflows/docker-build.yml: adds opencode-env to the matrix so the image is built and pushed to GHCR alongside other envs.
Greptile SummaryThis PR re-introduces the
Confidence Score: 3/5The core rollout logic works end-to-end, but three issues in the new code produce incorrect behavior on realistic inputs: the streaming proxy path doesn't strip logprobs as documented, setup commands race with an already-running agent, and the server's import path pulls in the MCP client module contrary to the project's architectural contract. The streaming-vs-unary asymmetry in logprob stripping means OpenCode receives logprobs in streaming mode contrary to the module's stated contract. The setup/agent race is self-documented as a known limitation but can cause real workspace corruption for slower setup workloads on pre-baked templates that boot in ~6 s, well under the assumed 20 s safety margin. The transitive client import through init.py is an architectural invariant violation that the team explicitly tracks in INVARIANTS.md. Together these three issues warrant a closer pass before merge. envs/opencode_env/sandbox/interception.py (streaming logprob stripping), envs/opencode_env/server/opencode_environment.py (setup/agent ordering), envs/opencode_env/init.py (client re-export pulled into server process) Important Files Changed
Sequence DiagramsequenceDiagram
participant Trainer as Trainer / Client
participant Server as OpenCodeEnvironment (MCP Server)
participant Factory as OpenCodeSessionFactory
participant Sandbox as E2B Sandbox
participant Proxy as InterceptionProxy (port 7000)
participant LLM as Upstream LLM
Trainer->>Server: run_rollout(instruction, setup[], verify[])
Server->>Factory: create(task)
Factory->>Sandbox: create(timeout_s)
Factory->>Sandbox: bootstrap (install opencode, write config)
Factory->>Proxy: start_bg(interception.py --upstream-url ...)
Note over Factory,Proxy: API key via env var OPENCODE_UPSTREAM_API_KEY
Factory->>Sandbox: start_bg(opencode run ...) [agent starts HERE]
Factory-->>Server: OpenCodeSession
Note over Server,Sandbox: Race - setup commands run AFTER agent already started
Server->>Sandbox: exec(setup[0..n]) sequentially
Server->>Server: wait_for_completion(agent_timeout_s)
loop each LLM turn
Sandbox->>Proxy: POST /v1/chat/completions
Proxy->>LLM: "forward + inject logprobs=true"
LLM-->>Proxy: response (with logprobs)
Proxy->>Proxy: write TurnRecord to proxy_trace.jsonl
Proxy-->>Sandbox: "unary=stripped / streaming=NOT stripped"
end
Server->>Sandbox: exec(verify[0..n]) sequentially
Server->>Server: "compute reward = passed/total or reward.txt"
Server->>Sandbox: kill()
Server-->>Trainer: JSON(RolloutResult)
|
| from .client import OpenCodeEnv | ||
| from .config import OpenCodeConfig, Provider | ||
| from .harness import OpenCodeSession, OpenCodeSessionFactory | ||
| from .models import ( | ||
| CommandResult, | ||
| OpenCodeState, | ||
| RolloutResult, | ||
| RolloutTurn, | ||
| ) |
There was a problem hiding this comment.
ALIGNMENT FLAG: Server code transitively imports client code
server/opencode_environment.py does from opencode_env import (E2BSandboxBackend, OpenCodeConfig, OpenCodeSessionFactory, OpenCodeTask). This resolves through envs/opencode_env/__init__.py, which unconditionally executes from .client import OpenCodeEnv — pulling the MCP client into the server process. INVARIANTS.md states: "Server code must never import client code." The fix is either to have the server import the symbols it needs directly from their defining modules (harness.py, config.py, sandbox/) rather than from __init__, or to restructure __init__ so the client re-export is deferred.
Principle at stake: Client-server separation (INVARIANTS.md § Architectural Invariants)
Suggested reviewer: @darktex
Context Used: .claude/docs/INVARIANTS.md (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: envs/opencode_env/__init__.py
Line: 24-32
Comment:
**ALIGNMENT FLAG: Server code transitively imports client code**
`server/opencode_environment.py` does `from opencode_env import (E2BSandboxBackend, OpenCodeConfig, OpenCodeSessionFactory, OpenCodeTask)`. This resolves through `envs/opencode_env/__init__.py`, which unconditionally executes `from .client import OpenCodeEnv` — pulling the MCP client into the server process. `INVARIANTS.md` states: "Server code must never import client code." The fix is either to have the server import the symbols it needs directly from their defining modules (`harness.py`, `config.py`, `sandbox/`) rather than from `__init__`, or to restructure `__init__` so the client re-export is deferred.
**Principle at stake**: Client-server separation (INVARIANTS.md § Architectural Invariants)
**Suggested reviewer**: `@darktex`
**Context Used:** .claude/docs/INVARIANTS.md ([source](https://app.greptile.com/review/custom-context?memory=dbd1ab5e-bd4d-4701-9de0-9817404155a9))
How can I resolve this? If you propose a fix, please make it concise.| import asyncio | ||
| import os | ||
| import sys | ||
|
|
There was a problem hiding this comment.
Private helper exposed in example
from opencode_env.client import _extract_text — the leading underscore signals this is not part of the public API. If the internal implementation changes, this example (and any user code that copies it) silently breaks. Consider promoting _extract_text to a public name or having OpenCodeEnv.run_rollout return a RolloutResult directly.
Prompt To Fix With AI
This is a comment left during a code review.
Path: examples/opencode_env_simple.py
Line: 47
Comment:
**Private helper exposed in example**
`from opencode_env.client import _extract_text` — the leading underscore signals this is not part of the public API. If the internal implementation changes, this example (and any user code that copies it) silently breaks. Consider promoting `_extract_text` to a public name or having `OpenCodeEnv.run_rollout` return a `RolloutResult` directly.
How can I resolve this? If you propose a fix, please make it concise.
Summary
Re-introduces the OpenCode env originally developed in #603. That PR was merged into the stacked
feature/harness-interfacebranch, and theenvs/opencode_env/contents were dropped before that branch landed onmain. With #652 (harness runtime) and #654 (openenv-core 0.3.0 release) now on main, this lands cleanly onmaindirectly — no more stack dependency.This branch is built fresh off the current
maintip and contains only the 27 opencode-specific files from the tip ofadithya-s-k:opencode-harness(f216c19). Theopenenv-coredependency pin is updated from the dev-time git ref toopenenv-core[core]>=0.3.0.What's in
envs/opencode_env/Deployable env that wraps the OpenCode CLI agent in an E2B sandbox against any OpenAI-compatible LLM endpoint.
run_rolloutwith a uniform Task shape (instruction+setup[]+verify[]bash commands). Reward =passed_verify / totalor override via/home/user/logs/verifier/reward.txt.vllm/openai/hf_router) — resolvesbase_url/api_key/modelfrom env vars with sane defaults.transparent_proxymode: in-sandbox FastAPI proxy injectslogprobs=trueon forwarded/v1/chat/completionsand captures per-turn logprobs to a JSONL trace (for GRPO). Strips logprobs from what opencode sees.black_boxmode: skips the proxy for SFT / eval rollouts./webwith a live phase log (sandbox boot → setup → agent → verify → collect).ResourceSession/ResourceSessionFactoryfromopenenv.core.harness.Tests
tests/envs/test_opencode_env.py— 14 offline unit tests covering catalog resolution, model serialization, and task coercion. No E2B / LLM / network. Plus one@pytest.mark.integrationtest against the deployed HF Space (skipped by default).Example
examples/opencode_env_simple.py— end-to-endbinary_searchrollout against the deployed HF Space, prints reward + per-turn logprobs.CI
Adds
opencode-envto.github/workflows/docker-build.ymlso the image is built and pushed to GHCR alongside other envs.Provenance
feature/harness-interface, opencode files dropped before merge to main)adithya-s-k:opencode-harness(tipf216c19) — kept as backup, this PR pulls the same final file stateTest plan
PYTHONPATH=src:envs uv run pytest tests/envs/test_opencode_env.py -vpasses offlineopencode-envand pushes to GHCRAdithyaSK/opencode-env) still works against this code (binary_search smoke task, all 3 endpoints)