envs/opencode_env: OpenCode coding-agent harness primitive#603
Conversation
Harness primitive for running the OpenCode CLI agent in a sandbox against any OpenAI-compatible endpoint. Stacked on the PR huggingface#471 harness-runtime branch; implements the ResourceSession / ResourceSessionFactory contracts. Modes: - black_box: opencode talks directly to the configured endpoint. Verified end-to-end against real OpenAI (gpt-4o-mini) inside an E2B sandbox; the agent produces a correct fizzbuzz and the verifier scores reward=1.0. - transparent_proxy: a per-session FastAPI proxy runs inside the sandbox, forwards /v1/chat/completions to the configured upstream with logprobs=true injected, captures per-turn (messages, completion tokens, logprobs, finish reason) to a JSON-lines trace, and strips logprobs from what opencode sees. Handles both unary and streaming (SSE). Caps max_tokens and auto-translates to max_completion_tokens for gpt-5.x/o* models. Components: - config.py: OpenCodeConfig (generic provider/base_url/api_key/model fields supporting OpenAI, Anthropic, and OpenAI-compatible endpoints; proxy tuning knobs; sandbox_home override for non-E2B backends). - opencode_runtime.py: pure builders for opencode.json, install/run shell commands, and env vars. - task.py: OpenCodeTask pydantic model (instruction + optional setup_shell + file uploads + opaque metadata); coerces from str/dict. - sandbox/base.py: SandboxBackend / SandboxHandle / BgJob Protocols. - sandbox/e2b.py: E2BSandboxBackend with a threaded BgJob wrapper that provides timeout support over E2B's CommandHandle. - harness.py: OpenCodeSession + OpenCodeSessionFactory; in Mode B, installs proxy deps, uploads the proxy module, starts it as a bg job on localhost:7000, rewrites opencode.json to point at the proxy, and forces @ai-sdk/openai-compatible so routing goes through /v1/chat/completions. - interception.py: InterceptionProxy (FastAPI, unary + streaming), per-turn trace capture, CLI entry point for sandbox-side execution. Tests: 37 unit tests plus 3 live integration tests gated on E2B_API_KEY / OPENAI_API_KEY. Known limitation: OpenAI's gpt-5.x chat family refuses logprob requests, so Mode B live validation against OpenAI requires gpt-4o-mini or older. vLLM (the intended training-time upstream) returns logprobs natively.
End-to-end Mode B now verified against a live vLLM tunnel: - Qwen/Qwen3.5-4B on 2x A100 (tp=2, 16K ctx) via `vllm serve` - Cloudflared tunnel exposes the endpoint publicly - E2B sandbox runs opencode against the tunnel through the in-sandbox proxy - Proxy captures 4 turns, 36 tokens with real per-token logprobs - Agent produces correct fizzbuzz.py, verifier scores reward=1.0 - 55.86s total (sandbox + install + 4 LLM turns + verify) Changes: - config: add proxy_disable_thinking flag; plumbed through harness -> proxy via --disable-thinking CLI arg. - interception: inject chat_template_kwargs.enable_thinking=false on forwarded requests when enabled (Qwen3/Qwen3.5 tokenizer hook); also split the request handler into unary + streaming paths (SSE) and auto-translate max_tokens -> max_completion_tokens for gpt-5.x/o* models. - sandbox/e2b: E2B's commands.run(background=True) has a default server-side timeout=60 that kills long-running opencode bg jobs; pass timeout=0 to disable it. - live_watch: RolloutSummary / collect_rollout_summary / print_rollout_summary — post-rollout structured report reading proxy trace, opencode event log, and the workdir listing + file contents. - tests/test_harness_live_vllm: end-to-end Mode B test against a live vLLM tunnel (gated on VLLM_TUNNEL_URL + E2B_API_KEY), asserts logprobs are captured with shape matching completion tokens.
Fixes the server-mode rollout path. Under load E2B can return sandboxes that aren't yet accepting commands, curl-install can transiently fail, and opencode's internal title-generation call can emit a stripped model id (e.g. ``Qwen3.5-4B`` instead of ``Qwen/Qwen3.5-4B``) that vLLM rejects with 404. Previously the proxy silently swallowed upstream error bodies and returned an empty event-stream, which opencode interpreted as an empty assistant turn. Changes: - harness: ``_wait_for_sandbox_ready`` probes ``echo ok`` up to 15x before issuing commands. - harness: ``_exec_with_retry`` wraps install / pip-deps / extra-setup with exponential backoff, up to 3 attempts, bailing on deterministic errors (non-empty stderr). - interception: ``ProxyConfig.model_override`` rewrites the ``model`` field on every forwarded request to the exact upstream id, bypassing opencode's provider-prefix quirks. Plumbed through as ``--model-override`` on the CLI. - interception: ``_proxy_streaming`` now inspects upstream status before committing to an SSE response — non-2xx returns a JSON error response to opencode AND logs the full upstream body to proxy.log, so the caller sees the real failure reason. - harness: ``_start_proxy`` passes ``--model-override`` built from ``config.model`` so the upstream always sees the right id. - harness: proxy deps install now uses ``_exec_with_retry`` too. Verified via local uvicorn: fizzbuzz + fibonacci tasks both succeed end-to-end through the server path in 19-20s with reward=1.0, 3-4 productive turns, and real per-token logprobs captured on every turn.
…itive) Adds the infrastructure for driving opencode via its HTTP server instead of the CLI. This is the Phase 2b foundation — fine-grained MCP tools in the consumer server wrap these primitives. Changes: - opencode_client.OpenCodeServerClient: typed httpx wrapper over the OpenAPI spec at /doc. Sync and async methods for create_session, send_message / send_prompt_async, list_messages, get_session, get_all_status, abort, plus stream_events / astream_events (SSE) and a wait_for_ready helper. Base64 basic-auth when OPENCODE_SERVER_PASSWORD is set. - harness.OpenCodeSession: new ``driver: Literal["cli", "serve"]`` field. driver="cli" is today's `opencode run` path. driver="serve" stores serve_public_url + serve_client + serve_session_id on the session. start_agent() dispatches on driver; wait_for_completion() polls /session/:id for idle when driver="serve". New abort() method hits /session/:id/abort for cancellation. - harness.OpenCodeSessionFactory: new ``driver`` constructor arg plumbs through to create(). _start_serve() runs opencode serve bound to 0.0.0.0:4096 as a bg job, probes it internally via curl, then uses the sandbox backend's get_host(4096) to build a public URL (E2B returns https://4096-<sandbox_id>.e2b.app). Fails fast if the backend doesn't support get_host. - tests/test_opencode_client.py: 7 unit tests covering URL/method/body shape, auth header, prompt text extraction, abort bool, limit param, wait_for_ready polling, SSE event helpers. Uses httpx MockTransport patched via monkeypatch — no live opencode serve needed. - tests/test_harness.py: _FakeSandbox now responds to the health-probe "echo ok" command so the existing factory tests work after the Phase-1 reliability layer landed. Verified: 34 unit tests pass (7 new + 27 existing), driver=cli path unchanged. End-to-end E2B spike confirms: sandbox_id assigned in 0.4s opencode install 2.5s opencode serve --port 4096 --hostname 0.0.0.0 listening in 1s sandbox.get_host(4096) returns https://4096-<id>.e2b.app external /doc returns HTTP 200 with OpenAPI spec external POST /session returns real session metadata Next: wire 4 new MCP tools (start_rollout / get_state / abort_rollout / finalize_rollout) in the consumer env + SSE endpoint for live rollout events. Ship to HF Space.
Uvicorn+fastapi cold boot inside E2B can take >10s under load; the tight probe loop was producing false 'proxy did not start within 10s' errors. 60s cap at 0.5s intervals keeps the retry fast while tolerating the slow path.
…soning
Three audit fixes surfaced by end-to-end testing through the deployed
env server:
1. `_start_serve` now `cd`s into workdir_path(config) before launching
opencode serve. Without this, the agent writes files to $HOME and
RolloutResult.workdir_files (reading /home/user/workdir) comes back
empty — the "rollout succeeded but nothing appeared" symptom.
2. `wait_for_completion` idle check was `status.get("idle")` but
opencode's /session/status returns `{"type":"idle"}`, not
`{"idle":true}`. Every serve-driver rollout silently timed out at
agent_timeout_s. Now checks `status.get("type") == "idle"` and adds
structured logging on every tick.
3. Interception proxy now preserves `delta.reasoning` on streaming
chunks and surfaces it as `message.reasoning` on the assembled
response. HF Router's Qwen3.5 thinking mode returns reasoning as a
separate field from content; previously it was dropped.
4. `upstream_model` no longer strips the Qwen/ org prefix — full
`config.model` is forwarded as the model-override so both vLLM
(served as `Qwen/Qwen3.5-4B`) and HF Router (requires
`Qwen/<repo>:<provider>`) work.
5. Structured logging at every factory.create phase so operators can
see exactly which step is stuck (sandbox, bootstrap, proxy, serve,
wait_for_completion).
…crets The four live tests (OpenAI / vLLM / mode-B / E2B) required OPENAI_API_KEY, VLLM_URL, or E2B_API_KEY to execute and were development-time fixtures rather than CI checks. The core functionality is already covered offline by test_harness.py (end-to-end factory lifecycle against a mock sandbox + mock OpenAI endpoint), test_interception.py (proxy forward + per-turn record assembly), test_opencode_client.py (serve client over httpx mocks), and test_sandbox_base.py (E2BSandboxBackend key-required unit).
|
Hi @adithya-s-k! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
Greptile SummaryThis PR introduces
ALIGNMENT FLAG:
Confidence Score: 3/5Not safe to merge without addressing the API key exposure and the serve-driver idle-detection false-positive. Three P1 findings in harness.py: a security invariant violation (credential in process table), a correctness bug (premature idle detection for thinking models), and a broken abstraction for custom backends (get_host outside Protocol). These need resolution before merge. envs/opencode_env/harness.py requires the most attention for all three P1 issues; envs/opencode_env/sandbox/base.py needs a get_host addition to close the protocol gap.
|
| Filename | Overview |
|---|---|
| envs/opencode_env/harness.py | Core session/factory implementation; contains API key exposure in proxy CLI args (P1 security), fragile idle-detection heuristic for serve driver (P1 logic), and get_host protocol bypass (P1 design). |
| envs/opencode_env/interception.py | FastAPI proxy for transparent LLM interception; well-structured streaming accumulation, reasoning preservation, and error surfacing. |
| envs/opencode_env/sandbox/base.py | SandboxHandle/SandboxBackend Protocol definitions; missing get_host method that the serve driver depends on, creating an undocumented protocol extension requirement. |
| envs/opencode_env/config.py | Pydantic config model; clean, well-documented fields with sensible defaults. |
| envs/opencode_env/opencode_runtime.py | Pure builder functions for sandbox bootstrap artifacts; no IO coupling, well-tested. |
| envs/opencode_env/opencode_client.py | Thin typed httpx wrapper over opencode serve HTTP API; straightforward and well-structured. |
| envs/opencode_env/task.py | Simple Pydantic task model with coerce() helper; clean and well-covered by tests. |
| envs/opencode_env/live_watch.py | Post-rollout summary helpers; paths are hardcoded to /home/user/... rather than using config.sandbox_home, which could break non-default sandbox layouts. |
| envs/opencode_env/sandbox/e2b.py | E2B backend implementation; correctly wraps CommandHandle with thread-based timeout, handles CommandExitException cleanly. |
Sequence Diagram
sequenceDiagram
participant Caller
participant Factory as OpenCodeSessionFactory
participant Sandbox as SandboxBackend
participant Proxy as InterceptionProxy (Mode B)
participant OC as OpenCode CLI/serve
participant LLM as Upstream LLM
Caller->>Factory: create(task)
Factory->>Sandbox: create(timeout_s)
Factory->>Sandbox: exec(install_cmd)
Factory->>Sandbox: write_text(opencode.json, instruction.md)
alt mode=transparent_proxy
Factory->>Sandbox: start_bg(interception.py)
Factory->>Sandbox: write_text(opencode.json) rewrite baseURL to proxy
end
alt driver=serve
Factory->>Sandbox: start_bg(opencode serve)
Factory->>OC: POST /session
Factory->>OC: POST /session/:id/prompt_async
else driver=cli
Factory->>Sandbox: start_bg(opencode run)
end
Factory->>Caller: OpenCodeSession
OC->>Proxy: POST /v1/chat/completions
Proxy->>LLM: POST /v1/chat/completions (+logprobs)
LLM-->>Proxy: stream response
Proxy-->>OC: stream response (logprobs stripped)
Proxy->>Proxy: write proxy_trace.jsonl
Caller->>Factory: session.wait_for_completion()
Note over Factory: CLI: bg_job.wait() / Serve: poll GET /session/status
Caller->>Factory: session.verify(transcript)
Caller->>Factory: session.close()
Factory->>Sandbox: kill()
Prompt To Fix All With AI
This is a comment left during a code review.
Path: envs/opencode_env/harness.py
Line: 590-596
Comment:
**API key exposed in process table**
`self._config.api_key` is interpolated directly into the shell command as `--upstream-api-key <value>`. Any process listing tool (`ps aux`, `/proc/<pid>/cmdline`) inside or outside the sandbox will expose the upstream API key in plain text. This violates the "No credential exposure" security invariant.
Pass the key via an environment variable instead:
```python
proxy_env = {"UPSTREAM_API_KEY": self._config.api_key}
proxy_cmd = (
"cd /home/user/proxy && "
"python interception.py "
f"--upstream-url {self._config.base_url} "
"--upstream-api-key $UPSTREAM_API_KEY " # read from env, not argv
...
)
proxy_job = sandbox.start_bg(proxy_cmd, envs=proxy_env)
```
Then update `interception.py`'s CLI parser to fall back to `os.environ.get("UPSTREAM_API_KEY")`.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: envs/opencode_env/harness.py
Line: 231-244
Comment:
**False-idle detection for thinking models**
The `stable_ticks >= 12` heuristic (12 × 0.5 s = 6 s) treats any 6-second gap in new messages as "idle". A reasoning/thinking model (e.g. Qwen3 with `disable_thinking=False`) can pause for tens of seconds between outputting tokens without the session being finished. During these pauses `list_messages` returns the same count, stable_ticks climbs to 12, and `wait_for_completion` returns 0 — causing `verify()` to be called on an in-progress run and potentially reporting incorrect results.
Consider raising the threshold significantly (e.g. `stable_ticks >= 60` → 30 s) or replacing the heuristic with the `GET /event` SSE stream idle signal so completion detection is driven by the server's own state machine rather than a message-count derivative.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: envs/opencode_env/harness.py
Line: 693-699
Comment:
**`get_host` bypasses the SandboxHandle protocol**
`driver="serve"` reaches through `sandbox.raw.get_host()` — a property specific to `E2BSandboxHandle` and absent from the `SandboxHandle` Protocol. Any custom backend that correctly implements the Protocol but does not expose a `.raw` attribute with `get_host` will fail here at runtime with a non-obvious error.
The `SandboxHandle` Protocol should declare a `get_host(port: int) -> str` method (optional, raising `NotImplementedError` by default) so custom backend authors know the contract.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: envs/opencode_env/harness.py
Line: 289-295
Comment:
**`import json` inside a for loop**
`import json as _json` is placed inside the `for` loop body in `fetch_proxy_trace`. Python caches imports so this is not a correctness bug, but it's misleading and would fail a lint pass. Move the import to the top of the file alongside the other standard-library imports.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: envs/opencode_env/harness.py
Line: 707
Comment:
**Module-level import at bottom of file**
`from pathlib import Path` is placed after the class definitions with a `# noqa: E402` suppression. Move this to the top of the file with the other imports.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "test(opencode_env): drop live integratio..." | Re-trigger Greptile
| proxy_cmd = ( | ||
| "cd /home/user/proxy && " | ||
| "python interception.py " | ||
| f"--upstream-url {self._config.base_url} " | ||
| f"--upstream-api-key {self._config.api_key} " | ||
| f"--trace {_PROXY_TRACE_PATH} " | ||
| f"--port {_PROXY_PORT} " |
There was a problem hiding this comment.
API key exposed in process table
self._config.api_key is interpolated directly into the shell command as --upstream-api-key <value>. Any process listing tool (ps aux, /proc/<pid>/cmdline) inside or outside the sandbox will expose the upstream API key in plain text. This violates the "No credential exposure" security invariant.
Pass the key via an environment variable instead:
proxy_env = {"UPSTREAM_API_KEY": self._config.api_key}
proxy_cmd = (
"cd /home/user/proxy && "
"python interception.py "
f"--upstream-url {self._config.base_url} "
"--upstream-api-key $UPSTREAM_API_KEY " # read from env, not argv
...
)
proxy_job = sandbox.start_bg(proxy_cmd, envs=proxy_env)Then update interception.py's CLI parser to fall back to os.environ.get("UPSTREAM_API_KEY").
Prompt To Fix With AI
This is a comment left during a code review.
Path: envs/opencode_env/harness.py
Line: 590-596
Comment:
**API key exposed in process table**
`self._config.api_key` is interpolated directly into the shell command as `--upstream-api-key <value>`. Any process listing tool (`ps aux`, `/proc/<pid>/cmdline`) inside or outside the sandbox will expose the upstream API key in plain text. This violates the "No credential exposure" security invariant.
Pass the key via an environment variable instead:
```python
proxy_env = {"UPSTREAM_API_KEY": self._config.api_key}
proxy_cmd = (
"cd /home/user/proxy && "
"python interception.py "
f"--upstream-url {self._config.base_url} "
"--upstream-api-key $UPSTREAM_API_KEY " # read from env, not argv
...
)
proxy_job = sandbox.start_bg(proxy_cmd, envs=proxy_env)
```
Then update `interception.py`'s CLI parser to fall back to `os.environ.get("UPSTREAM_API_KEY")`.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Agreed. This is still valid on the current head: the proxy command still passes the upstream key via --upstream-api-key, so this should stay open. The fix should pass the key through sandbox.start_bg(..., envs={"OPENCODE_UPSTREAM_API_KEY": ...}) and have interception.py read it from the environment instead of argv. I would also handle model/URL shell quoting in the same cleanup pass.
| msgs = self.serve_client.list_messages(self.serve_session_id) | ||
| n = len(msgs) | ||
| if n == last_msg_count: | ||
| stable_ticks += 1 | ||
| # 6s without a new message OR status change — treat as idle. | ||
| if stable_ticks >= 12: | ||
| _log.info( | ||
| "wait_for_completion: stale msgs=%d ticks=%d — treating as idle", | ||
| n, stable_ticks, | ||
| ) | ||
| return 0 | ||
| else: | ||
| last_msg_count = n | ||
| stable_ticks = 0 |
There was a problem hiding this comment.
False-idle detection for thinking models
The stable_ticks >= 12 heuristic (12 × 0.5 s = 6 s) treats any 6-second gap in new messages as "idle". A reasoning/thinking model (e.g. Qwen3 with disable_thinking=False) can pause for tens of seconds between outputting tokens without the session being finished. During these pauses list_messages returns the same count, stable_ticks climbs to 12, and wait_for_completion returns 0 — causing verify() to be called on an in-progress run and potentially reporting incorrect results.
Consider raising the threshold significantly (e.g. stable_ticks >= 60 → 30 s) or replacing the heuristic with the GET /event SSE stream idle signal so completion detection is driven by the server's own state machine rather than a message-count derivative.
Prompt To Fix With AI
This is a comment left during a code review.
Path: envs/opencode_env/harness.py
Line: 231-244
Comment:
**False-idle detection for thinking models**
The `stable_ticks >= 12` heuristic (12 × 0.5 s = 6 s) treats any 6-second gap in new messages as "idle". A reasoning/thinking model (e.g. Qwen3 with `disable_thinking=False`) can pause for tens of seconds between outputting tokens without the session being finished. During these pauses `list_messages` returns the same count, stable_ticks climbs to 12, and `wait_for_completion` returns 0 — causing `verify()` to be called on an in-progress run and potentially reporting incorrect results.
Consider raising the threshold significantly (e.g. `stable_ticks >= 60` → 30 s) or replacing the heuristic with the `GET /event` SSE stream idle signal so completion detection is driven by the server's own state machine rather than a message-count derivative.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
This thread is outdated on the latest branch. The opencode serve path was removed; the harness now uses a single CLI/background-job path and waits on process completion rather than message-count idle detection.
| raw = getattr(sandbox, "raw", None) | ||
| if raw is None or not hasattr(raw, "get_host"): | ||
| raise RuntimeError( | ||
| "driver='serve' requires a sandbox backend that exposes " | ||
| "get_host(port) (e.g. E2B)." | ||
| ) | ||
| host = raw.get_host(_SERVE_PORT) |
There was a problem hiding this comment.
get_host bypasses the SandboxHandle protocol
driver="serve" reaches through sandbox.raw.get_host() — a property specific to E2BSandboxHandle and absent from the SandboxHandle Protocol. Any custom backend that correctly implements the Protocol but does not expose a .raw attribute with get_host will fail here at runtime with a non-obvious error.
The SandboxHandle Protocol should declare a get_host(port: int) -> str method (optional, raising NotImplementedError by default) so custom backend authors know the contract.
Prompt To Fix With AI
This is a comment left during a code review.
Path: envs/opencode_env/harness.py
Line: 693-699
Comment:
**`get_host` bypasses the SandboxHandle protocol**
`driver="serve"` reaches through `sandbox.raw.get_host()` — a property specific to `E2BSandboxHandle` and absent from the `SandboxHandle` Protocol. Any custom backend that correctly implements the Protocol but does not expose a `.raw` attribute with `get_host` will fail here at runtime with a non-obvious error.
The `SandboxHandle` Protocol should declare a `get_host(port: int) -> str` method (optional, raising `NotImplementedError` by default) so custom backend authors know the contract.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
This is outdated after the latest rewrite. The serve driver was removed, so the harness no longer reaches through sandbox.raw.get_host() for opencode serve exposure.
| records: list[dict[str, Any]] = [] | ||
| for line in content.splitlines(): | ||
| line = line.strip() | ||
| if not line: | ||
| continue | ||
| import json as _json | ||
| records.append(_json.loads(line)) |
There was a problem hiding this comment.
import json as _json is placed inside the for loop body in fetch_proxy_trace. Python caches imports so this is not a correctness bug, but it's misleading and would fail a lint pass. Move the import to the top of the file alongside the other standard-library imports.
Prompt To Fix With AI
This is a comment left during a code review.
Path: envs/opencode_env/harness.py
Line: 289-295
Comment:
**`import json` inside a for loop**
`import json as _json` is placed inside the `for` loop body in `fetch_proxy_trace`. Python caches imports so this is not a correctness bug, but it's misleading and would fail a lint pass. Move the import to the top of the file alongside the other standard-library imports.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Agreed. This is still present on current head in fetch_proxy_trace; the json import should be moved to the module import block and the loop should call json.loads(line) directly.
| return public_url, client | ||
|
|
||
|
|
||
| from pathlib import Path # noqa: E402 (used only inside _start_proxy) |
There was a problem hiding this comment.
Module-level import at bottom of file
from pathlib import Path is placed after the class definitions with a # noqa: E402 suppression. Move this to the top of the file with the other imports.
Prompt To Fix With AI
This is a comment left during a code review.
Path: envs/opencode_env/harness.py
Line: 707
Comment:
**Module-level import at bottom of file**
`from pathlib import Path` is placed after the class definitions with a `# noqa: E402` suppression. Move this to the top of the file with the other imports.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
This is fixed on the current head. Path is now imported in the top-level stdlib import block rather than at module bottom.
Darktex
left a comment
There was a problem hiding this comment.
Note: This is an automated review by Claude Code, not a human review.
Alignment Review
Automated Checks
- Lint: manual ruff pass shows one E402 (noqa'd) and one E731 (noqa'd) in the new files; no additional failures.
- Debug code: FOUND — one
print()call in production proxy code (see below).
Tier 1: Fixes Required
-
envs/opencode_env/interception.py(~line 1382) —print(...)left in_proxy_streamingerror path ([proxy] turn {turn_idx}: upstream ...). Should uselogging.getLogger(__name__).warning(...)to match the rest of the file's logging discipline. -
envs/opencode_env/harness.py(~line 942) — Theapi_keyis interpolated directly into the shell command string passed tosandbox.start_bg(proxy_cmd). Keys containing shell-special characters (spaces, quotes,$, backticks) will corrupt the command or enable injection. Pass it via theenvs=dict and read it from an environment variable ininterception.py. -
envs/opencode_env/harness.py(~line 1055) —from pathlib import Pathis at module scope after class definitions (E402, noqa'd). Move to the top of the file per PEP 8 and the project's usort/ruff pipeline. -
envs/opencode_env/tests/test_harness.py(~line 2957) —pytest.raises(RuntimeError, match="install failed")only accidentally matches the actual"opencode install failed after 3 attempts ...". Tighten the match string to something stable (e.g.,"opencode install").
Tier 2: Alignment Discussion
ALIGNMENT FLAG: external verifier callable breaks "rewards inside environment"
- Principle at stake: "Rewards inside environment" (RFC 002, PRINCIPLES.md). External reward augmentation should use the Transform pipeline.
- Concern:
OpenCodeSessionFactory(verifier=my_verifier)accepts an arbitrary callable from the caller and uses it to produceenv_reward, putting reward logic outside the environment. This can harm reproducibility and opens reward-hacking at the orchestration layer. - Suggested reviewer: @Darktex
ALIGNMENT FLAG: stacked on unmerged RFC 005 / PR #471
- Concern: This env imports
ResourceSession/ResourceSessionFactoryfromopenenv.core.harness, which does not exist onmainyet. Merging before #471 lands will break import/test against main. Please mark the dependency explicitly (e.g., "Depends on #471") rather than relying on description text alone. - Suggested reviewer: @Darktex
Summary
- 4 mechanical issues to fix (debug print, credential injection risk, misplaced import, fragile test match).
- 2 alignment points for human review.
Automated review by Claude Code | Learn more
Darktex
left a comment
There was a problem hiding this comment.
Note: This is an automated review by Claude Code, not a human review.
Alignment Review Report
Automated Checks
- Lint (pre-existing failures only): PASS for opencode_env — the 5 failing files are in
chat_env,repl_env, andtextarena_env, all pre-existing and unrelated to this PR. - Debug code: FOUND — see Tier 1 below.
Tier 1: Fixes Required
-
envs/opencode_env/harness.pyline ~942 — Credential exposed via CLI argument (Security Invariant)f"--upstream-api-key {self._config.api_key} "The API key is passed as a positional shell argument, making it visible in
/proc/<pid>/cmdlineandps auxinside the sandbox. Any process in the sandbox can read it. Fix: pass the key through an environment variable instead (e.g.,PROXY_API_KEY) and read it ininterception.py'smain()viaos.environ. The env-var injection path (build_env_vars) already setsOPENAI_API_KEY— the proxy key should follow the same pattern. -
envs/opencode_env/interception.pyline ~1382 — Bareprint()in production code path (Debug code invariant)print( f"[proxy] turn {turn_idx}: upstream {upstream.status_code}: " f"{str(error_json)[:400]}", flush=True, )
This
print()is in a production code path (_proxy_streaming) and fires on every upstream error response during live rollouts. Replace withlogging.getLogger(__name__).warning(...)so it respects the caller's log level and format. -
envs/opencode_env/harness.pyline 1055 — LatePathimport at module bottomfrom pathlib import Path # noqa: E402 (used only inside _start_proxy)
The
# noqa: E402suppresses the lint rule rather than fixing the structure. Move the import to the top of the file with the other stdlib imports. -
envs/opencode_env/harness.pyline ~937 — Shell injection viamodelfieldmodel_override_flag = f"--model-override '{upstream_model}' "
A
modelvalue containing a single quote (e.g.,openai/foo' && malicious_cmd #) breaks out of the shell argument. Useshlex.quote(upstream_model)or pass the value as an environment variable consumed by the proxy rather than embedding it directly in the command string. -
Missing
openenv.yamlmanifestEvery OpenEnv environment package requires an
openenv.yamlperPATTERNS.md. This PR has no manifest file. Add it (even a minimal one) to conform to the expected package structure.
Tier 2: Alignment Discussion
ALIGNMENT FLAG 1: Hard dependency on unmerged PR #471
- Principle at stake: From
PRINCIPLES.md"Be hands-on: Provide ready-to-use implementations, not just specs" and general integration hygiene. - The concern:
envs/opencode_env/harness.pyimportsResourceSession,ResourceSessionFactory,VerifyResult,ToolResult,Messagefromopenenv.core.harness. This module does not exist onmain— it lives in open PR #471. The PR description acknowledges this with "Stacked on #471," but the base contracts it depends on are themselves still in review. This PR cannot land until #471 merges and the core contracts it implements are stable. - Suggested reviewer: @Darktex
ALIGNMENT FLAG 2: Reward computed outside the environment boundary
- Principle at stake:
PRINCIPLES.md"Rewards inside environment" (RFC 002);INVARIANTS.md§Architectural §3 "Reward computation must stay inside environment boundary." - The concern:
OpenCodeSession.verify()delegates entirely to a caller-suppliedVerifier = Callable[[SandboxHandle, OpenCodeTask], VerifyResult]. The reward function is injected at factory construction time as arbitrary external code — it runs outside the environment's own logic. The question for the reviewer: does a post-hoc sandbox-execution verifier count as "inside the environment boundary" since it runs on the same sandbox object, or is it an invariant violation requiring an RFC discussion? - Suggested reviewer: @Darktex
ALIGNMENT FLAG 3: No Docker container — E2B-only sandbox model
- Principle at stake:
INVARIANTS.md§Security §2 "Container isolation — environments run in isolated Docker containers";PATTERNS.mdcanonical structure requiresserver/Dockerfile. - The concern: This environment has no
Dockerfile, noserver/directory, and no local Docker path. Isolation is provided entirely through E2B's cloud SDK, which is a paid external service. The PR usesSandboxBackendas a Protocol abstraction (so Docker could theoretically be added), but there is no Docker backend provided. The tradeoff may be justified, but should be explicitly called out and approved. - Suggested reviewer: @Darktex
ALIGNMENT FLAG 4: serve_client public attribute on OpenCodeSession
- Principle at stake:
INVARIANTS.md§Security §1 "Agents cannot access reset/simulation controls." - The concern:
OpenCodeSession.serve_clientis a public attribute of typeOpenCodeServerClient. That client has methods includingcreate_session(),send_prompt_async(), andabort()— these are simulation control operations. The attribute should be prefixed_serve_client(private) to signal it is not part of the public API. - Suggested reviewer: @Darktex
Summary
- 4 mechanical issues to fix (credential exposure via CLI arg, bare
print()in production, late import, shell injection via model field) plus 1 missingopenenv.yaml. - 4 alignment points for human review: stacked-on-unmerged-PR dependency, reward-outside-environment-boundary pattern, no Docker/container isolation, and
serve_clientpublic attribute exposure.
The code quality overall is high — test coverage is thorough and the two-driver/two-mode design is clean. The mechanical issues are fixable without design changes. The alignment issues (especially the reward boundary and container invariant) need explicit sign-off before this can land.
Automated review by Claude Code | Learn more
Darktex
left a comment
There was a problem hiding this comment.
Note: This is an automated review by Claude Code, not a human review.
Alignment Review Report
Automated Checks
- Lint: PASS for opencode_env (files not yet on disk; carla_env pre-existing failures are unrelated to this PR)
- Debug code: FOUND — one live
print()in production code path (see Tier 1)
Tier 1: Fixes Required
-
/envs/opencode_env/interception.py:1382—print()call in the production streaming error path (_proxy_streaming). This logs upstream error bodies directly to stdout in the running sandbox process. Should belogging.warning(...)or removed; stdout in a sandbox is not the right telemetry channel. -
/envs/opencode_env/harness.py:942—api_keypassed as a CLI argument on the proxy command line:f"--upstream-api-key {self._config.api_key}". This exposes the secret in the sandbox process list (ps aux) and in the proxy log file (_PROXY_LOG_PATH), violating the "no credential exposure" security invariant. Pass the key via an environment variable instead (e.g.OPENCODE_UPSTREAM_API_KEY) and read it inmain()/ProxyConfig. -
/envs/opencode_env/harness.py:1055—from pathlib import Pathis deferred to the bottom of the module (after the class definitions).E402placement is explicitly noted with anoqacomment, but the cleaner fix is to hoist it to the top-level imports block where it belongs.
Tier 2: Alignment Discussion
ALIGNMENT FLAG: Stacks on an unmerged PR
- Principle at stake: "Be hands-on — provide ready-to-use implementations" (PRINCIPLES.md); also general merge-stability
- The concern:
harness.pyimports fromopenenv.core.harness(theResourceSession,ResourceSessionFactory,Message,ToolResult,VerifyResultcontracts) that are defined only in PR #471, which is currently open. If this PR is merged before #471 the import path does not exist inmain, making the package entirely non-functional at import time. The README acknowledges this but there is no CI guard. - Suggested reviewer: @Darktex
ALIGNMENT FLAG: Verifier callable is caller-supplied, injected from outside the environment boundary
- Principle at stake: "Rewards inside environment" (RFC 002, PRINCIPLES.md)
- The concern:
OpenCodeSessionFactory(verifier=my_verifier)delegates reward computation to an arbitrary callable passed by the trainer/caller. This is structurally equivalent to external reward augmentation and differs from the pattern where reward logic is encapsulated inside the environment. The design rationale — that different tasks need different verifiers — may be sound, but it has not been documented in an RFC. If the intent is that this primitive is task-agnostic plumbing (akin to a Transform), the boundary needs to be explicit. - Suggested reviewer: @Darktex
ALIGNMENT FLAG: start_agent(), wait_for_completion(), and abort() are public on the session object
- Principle at stake: "Agents cannot reset" / dual API boundary (INVARIANTS.md §Security §Architectural)
- The concern: These methods control simulation lifecycle. They are called by the factory on behalf of the training orchestrator, which is correct. However, they are on the
ResourceSessionobject which is also handed toHarnessAdapter.run_white_box(session, ...). If a harness adapter or downstream code exposes the session reference to the agent being evaluated, the agent could callabort()or observe completion-state. This is the same class of risk as exposingreset()via MCP. The ABC contract in PR #471 (ResourceSession) does not include these methods, so they are extra-protocol additions. The fix would be either to not inherit them from the public ABC or to document explicitly that these must never be forwarded to agents. - Suggested reviewer: @Darktex
Summary
- 3 mechanical issues to fix (1 debug print, 1 credential-in-args, 1 import placement)
- 3 alignment points for human review (dependency ordering, reward-boundary, lifecycle method exposure)
The overall implementation quality is high — good test coverage, clean Protocol-based sandbox abstraction, well-commented internals. The Tier 1 items are straightforward to fix. The Tier 2 items are design questions that need human sign-off given the newness of both this primitive and the harness contract it builds on.
Automated review by Claude Code | Learn more
75b1878 to
aabcdbb
Compare
Darktex
left a comment
There was a problem hiding this comment.
Note: This is an automated review by Claude Code, not a human review.
Summary
This is a well-structured, documented, and tested harness primitive for running OpenCode inside E2B sandboxes. The code quality is high overall. However it has two categories of blocking issues: (1) it hard-depends on openenv.core.harness (PR #471) which does not exist on main, making it un-importable today; and (2) a credential exposure bug where api_key is interpolated into a shell command argument string. There are also several minor quality issues and one alignment flag that needs human sign-off before merge.
Tier 1 — Bugs / Quality
-
envs/opencode_env/harness.py:1-20— Hard dependency on unmerged PR #471.from openenv.core.harness import ResourceSession, ResourceSessionFactory, VerifyResult, Message, ToolResultfails withModuleNotFoundErroron currentmainbecauseopenenv.core.harnessdoes not exist yet. This PR must stack on #471 (e.g. via Graphite) and cannot merge before its parent lands. -
envs/opencode_env/harness.py(proxy_cmd construction, around diff line 942) — Credential exposure in shell command.self._config.api_keyis interpolated directly intoproxy_cmdas--upstream-api-key {self._config.api_key}. Shell commands can appear inps aux, audit logs, and E2B execution records. Pass it via an environment variable instead, consistent with theOPENAI_API_KEYpattern already used inbuild_env_vars. -
envs/opencode_env/interception.py(@app.on_event("shutdown")) —@app.on_eventis deprecated in FastAPI 0.93+ (PendingDeprecationWarning). The declared dependency isfastapi>=0.104. Replace with a@asynccontextmanagerlifespan passed toFastAPI(lifespan=...). -
envs/opencode_env/harness.py(final line,from pathlib import Path) — Module-level import placed after all class definitions with# noqa: E402to suppress the lint error. Movefrom pathlib import Pathto the top of the file alongside the other stdlib imports. -
envs/opencode_env/interception.py(error branch inside_proxy_streaming) — Bareprint(f"[proxy] turn {turn_idx}: upstream {upstream.status_code}: ..."). Production code should uselogging.warning(...)orlogging.error(...)instead ofprint()so operators can route, filter, and suppress log output. -
No
openenv.yamlpresent. Every env in the repo (echo_env, repl_env, etc.) ships anopenenv.yamlruntime descriptor. Add one even if the type isharnessrather thanspace. -
envs/opencode_env/harness.pymodule-level constants_PROXY_TRACE_PATH,_PROXY_LOG_PATH,_SERVE_LOG_PATH,_OPENCODE_BINare hardcoded to/home/user/....OpenCodeConfig.sandbox_homeexists to support non-E2B backends (e.g. Docker-as-root where home is/root). These paths should be derived fromconfig.sandbox_homevia helper functions inopencode_runtime.py(matching the pattern used byagent_log_path,workdir_path, etc.), otherwise non-E2B backends silently use the wrong paths.
Tier 2 — Alignment
ALIGNMENT FLAG: User-supplied verifier callable lives outside environment boundary
- Principle at stake: "Rewards inside environment" (RFC 002, PRINCIPLES.md)
- The concern:
OpenCodeSessionFactory(verifier=my_verifier)accepts an arbitrary callable that runs in the trainer process and returns a reward scalar. RFC 002 says "domain knowledge encapsulated in env, not external." The verifier here is deliberately external — it runs in the caller's process, inspects the sandbox viasandbox.exec(...), and produces the reward. This is a pragmatic choice for a closed-binary harness (the env has no hook point), but it departs from the principle without a documented trade-off. Either (a) add an RFC note or README section explaining why verifier-as-callback is the correct pattern for harness primitives, or (b) get explicit sign-off that this trade-off is intentional. - Suggested reviewer: @Darktex
ALIGNMENT FLAG: New contract (ResourceSession/ResourceSessionFactory) diverges from Gymnasium-style API
- Principle at stake: "Simple Gymnasium-style API" (PRINCIPLES.md); RFC 005 is "In Review" status
- The concern: This PR introduces a new top-level contract (
ResourceSession/ResourceSessionFactory) rather than extendingEnvironment[ActT, ObsT, StateT]. RFC 005 is still marked "In Review." Merging an env that depends on a still-in-review RFC could lock in a contract before the RFC is settled. The dependency structure (this PR stacked on #471) makes the order correct, but the alignment question is: has RFC 005's contract been finalized for theResourceSessionshape? - Suggested reviewer: @Darktex
Verdict
Request changes: the openenv.core.harness import dependency on unmerged PR #471 is a hard blocker, and the api_key in shell command is a security invariant violation — both must be fixed before this can merge.
Automated review by Claude Code | Learn more
sergiopaniego
left a comment
There was a problem hiding this comment.
Thanks!!!
Could be nice to add examples as it's common for other envs.
Additionally, you could link to a running remote env (I guess it's https://huggingface.co/spaces/AdithyaSK/opencode-env-rollout)
I think we can do a small round to clean some parts that are draft instead of intended to be part of the final version
| ```toml | ||
| [project.dependencies] | ||
| openenv-opencode_env = { | ||
| git = "https://github.com/adithya-s-k/OpenEnv.git", |
There was a problem hiding this comment.
should be updated/cleaned (and the rest of the appearances of similar patterns)
There was a problem hiding this comment.
This README thread looks outdated after the latest docs rewrite. The current README now uses the deployed Space endpoints and the task/setup/verify terminology rather than the earlier placeholder wording.
|
|
||
| ## References | ||
|
|
||
| - [OpenEnv PR #471](https://github.com/meta-pytorch/OpenEnv/pull/471) — harness session runtime we stack on |
There was a problem hiding this comment.
this doesn't make sense from the perspective of a final user since once this is live, #471 should also be merged
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
…dpoint catalog
Wraps the existing OpenCode harness primitive in a deployable OpenEnv
environment that can run as an HF Space, exposing a single MCP
``run_rollout`` tool plus a Gradio web UI at /web.
Highlights
----------
- Single MCP tool ``run_rollout`` accepting a uniform Task shape
(instruction + setup[] + verify[] bash commands), reward = passed_verify
/ total or override via /home/user/logs/verifier/reward.txt.
- Endpoint shorthand catalog (``vllm`` / ``openai`` / ``hf_router``) that
resolves base_url / api_key / model from env vars + sane defaults.
- In-sandbox FastAPI proxy (``transparent_proxy`` mode) injects
logprobs=true and captures per-token logprobs for GRPO training.
- Optional ``black_box`` mode skips the proxy for SFT / eval rollouts.
- Pre-baked E2B template (``opencode-rl``) drops sandbox cold start
from ~2min to ~6s by shipping opencode + proxy deps in the image.
- Streaming Gradio UI: /run handler is a generator that yields a live
phase log (sandbox boot → setup → agent → verify → collect) so the
user sees progress instead of a spinner.
- HF Space deployed at AdithyaSK/opencode-env, end-to-end verified
against vLLM, OpenAI, and HF Router (all 3 reward=1.0 on the
binary_search smoke task).
Layout
------
envs/opencode_env/
{client.py, models.py, __init__.py} # HTTP client + pydantic
{config.py, harness.py, opencode_runtime.py,
task.py} # primitive (CLI-only)
server/{app.py, opencode_environment.py,
gradio_ui.py, catalog.py, Dockerfile} # FastAPI + Gradio + MCP
sandbox/{base.py, e2b.py, interception.py,
build_template.py} # E2B + proxy + template
{pyproject.toml, openenv.yaml, uv.lock,
README.md, .dockerignore, .gitignore}
Removed (CLI-only refactor)
---------------------------
- harness.py: dropped the ``opencode serve`` driver path (~270 LOC).
- Deleted opencode_client.py, live_watch.py, env-local tests/.
CI / tests
----------
- New tests/envs/test_opencode_env.py: 14 unit tests (no E2B, no LLM,
no network) covering catalog resolution, model serialization, and
task coercion. Plus one @pytest.mark.integration test that runs
opencode end-to-end against the deployed Space (skipped by default).
- sandbox/__init__.py: e2b import wrapped in try/except so the package
loads cleanly without e2b installed (CI-friendly).
- Added opencode-env to .github/workflows/docker-build.yml matrix so
the image is built and pushed to GHCR alongside other envs.
openenv-core dependency
-----------------------
Currently pinned to the ``opencode-harness`` branch via git because
PyPI's ``openenv-core`` (0.2.x) does not yet ship the
``openenv.core.harness`` module that this env imports. Switch to
``openenv-core[core]>=0.2.2`` once RFC 5 / PR huggingface#471 ships in a
published release. The intended end-state is documented inline in
pyproject.toml.
Minimal end-to-end example: hits the deployed HF Space, runs a binary_search rollout via the MCP run_rollout tool, prints the reward + per-turn logprobs + the file the agent produced. Mirrors the per-env convention in ``examples/`` (echo_mcp_demo.py / coding_env_inference.py / atari_simple.py etc.). Defaults point at ``https://adithyask-opencode-env.hf.space``; override with ``OPENCODE_ENV_SPACE`` to target a different Space or local container. Requires ``OPENAI_API_KEY`` in the environment (passed in the request body, no Space secret required). Swap ``endpoint="openai"`` for ``"vllm"`` or ``"hf_router"`` to exercise the other backends.
1448944
into
huggingface:feature/harness-interface
* Add harness session runtime for training and evaluation * Handle MCP tool errors and lazy-load BrowserGym exports * Refine experimental harness APIs and reward forwarding * docs: mention environment_factory in harness tutorial * test: cover harness review regressions * feat(harness): rollout collection + openenv collect CLI (#560) * refactor(harness): move harness.py into harness/ package Preserves the public API (`from openenv.core.harness import X` keeps working) while making room for a ``collect`` submodule that layers synthetic-dataset generation on top of the runtime primitives from RFC 005 / PR #471. Relative imports in the module are adjusted from ``.client_types`` → ``..client_types``. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(harness): add rollout collection module Introduces ``openenv.core.harness.collect``, a thin layer on top of the harness runtime from RFC 005 / PR #471 for generating synthetic datasets from deployed environments: - ``EpisodeRecord`` — serializable view of one rollout + its verification. Uses ``_resolve_env_reward`` so any mismatch between a tool-result reward and ``verify.env_reward`` raises, preserving the "rewards in env" invariant end-to-end. - ``RolloutSerializer`` — append-only JSONL writer with a ``metadata.json`` sidecar. ``collected_episode_ids()`` enables resume. - ``CollectRunner.run()`` — orchestrates N episodes: session.create → harness.run_white_box → verify → optional rubric filter → serialize. Returns a ``CollectResult`` with aggregate stats. - ``build_model_step(llm_client)`` — adapts any ``LLMClient`` (OpenAI, Anthropic, and any OpenAI-compatible endpoint such as vLLM, TGI, Ollama, HF Inference, Together, Groq, Fireworks) into a ``ModelStep`` for the white-box harness. - ``push_to_hf_hub(output_dir, repo_id)`` — uploads ``results.jsonl``, ``metadata.json``, and an auto-generated dataset card to the Hub. The card's YAML front-matter tells the HF Dataset Viewer to treat the JSONL as ``split=train`` instead of trying to merge it with the metadata sidecar. 37 new tests (EpisodeRecord, RolloutSerializer, CollectRunner, build_model_step, push_to_hf_hub, build_dataset_readme). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(envs/openspiel): add TTT harness session factory ``envs/openspiel_env/harness.py`` exposes an ``OpenSpielSessionFactory`` that wraps ``OpenSpielEnv`` (via ``StepEnvSessionAdapter``) and lets a harness drive any OpenSpiel game — tic_tac_toe initially — through a single ``play_move(action_id)`` MCP-style tool. - Initial prompt renders legal actions plus a human-readable board for TTT so the LLM can reason about positions without needing to decode the 27-float info_state tensor. - ``render_tic_tac_toe_board`` decodes the OpenSpiel TTT info_state (empty/X/O planes) into a 3x3 grid. Empty cells show their action_id so the prompt doubles as an action legend. - Follows the pattern established by ``envs/browsergym_env/harness.py`` in PR #471 — no changes to the underlying env, client, or protocol. Tests cover the board rendering, tool dispatch, reset-kwargs forwarding, and an end-to-end collect run against a scripted client exercising the full ``MCPHarnessAdapter`` → ``CollectRunner`` → JSONL pipeline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): add openenv collect command Wraps ``CollectRunner`` + ``OpenSpielSessionFactory`` into a single command so a deployed OpenEnv environment can produce a dataset with one call: openenv collect openspiel:tic_tac_toe \\ --base-url https://user-space.hf.space \\ --output-dir /tmp/ttt-sft-v1 \\ -n 200 --provider openai --model gpt-5-mini \\ --push-to-hub user/ttt-sft-v1 Flags in short: - ``--provider scripted | openai | anthropic`` — teacher selection. Scripted picks the first legal action and requires no API key, making ``openenv collect`` smoke-testable out of the box. - ``--llm-endpoint / --llm-port`` — point at any OpenAI-compatible endpoint (vLLM, TGI, Ollama, HF Inference, Together, Groq, ...). - ``--push-to-hub REPO`` — upload the directory as a dataset after collect; ``--private``/``--commit-message`` available. - ``--resume / --no-resume`` — ``CollectRunner`` skips ``episode_ids`` already serialized on disk. - ``--keep-losses`` — by default filters rollouts with reward < 0 so the output is SFT-ready. Env dispatch is via ``"family:variant"`` strings (e.g. ``openspiel:tic_tac_toe``). Unknown families raise a typer ``BadParameter`` with the supported set. 7 new CLI tests mocking ``CollectRunner`` + ``OpenSpielEnv`` so the dispatch logic (scripted vs hosted provider, push-to-hub, filter defaults, bad ``--env``) is exercised without network. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(harness): add README and runnable examples - ``src/openenv/core/harness/README.md`` documents the collect module: quick-start (CLI + programmatic), output schema, and design notes covering the "thin envs" / "rewards in env" / "provider-agnostic teacher" choices. - ``examples/ttt_collect_demo.py`` — scripted teacher against either a built-in fake OpenSpiel client or a real deployed server (``--base-url``). Runs with zero setup for pipeline smoke-testing. - ``examples/ttt_collect_with_llm.py`` — provider-agnostic example that picks between hosted OpenAI/Anthropic (via ``--provider``) and any OpenAI-compatible self-hosted endpoint (via ``--llm-endpoint``), using the same collect pipeline unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address collect feedback --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: burtenshaw <ben.burtenshaw@gmail.com> * fix: address browsergym harness review comments * fix: format collect files * envs/opencode_env: OpenCode coding-agent harness primitive (#603) * feat(opencode_env): OpenCode harness primitive (M1.1-M1.5) Harness primitive for running the OpenCode CLI agent in a sandbox against any OpenAI-compatible endpoint. Stacked on the PR #471 harness-runtime branch; implements the ResourceSession / ResourceSessionFactory contracts. Modes: - black_box: opencode talks directly to the configured endpoint. Verified end-to-end against real OpenAI (gpt-4o-mini) inside an E2B sandbox; the agent produces a correct fizzbuzz and the verifier scores reward=1.0. - transparent_proxy: a per-session FastAPI proxy runs inside the sandbox, forwards /v1/chat/completions to the configured upstream with logprobs=true injected, captures per-turn (messages, completion tokens, logprobs, finish reason) to a JSON-lines trace, and strips logprobs from what opencode sees. Handles both unary and streaming (SSE). Caps max_tokens and auto-translates to max_completion_tokens for gpt-5.x/o* models. Components: - config.py: OpenCodeConfig (generic provider/base_url/api_key/model fields supporting OpenAI, Anthropic, and OpenAI-compatible endpoints; proxy tuning knobs; sandbox_home override for non-E2B backends). - opencode_runtime.py: pure builders for opencode.json, install/run shell commands, and env vars. - task.py: OpenCodeTask pydantic model (instruction + optional setup_shell + file uploads + opaque metadata); coerces from str/dict. - sandbox/base.py: SandboxBackend / SandboxHandle / BgJob Protocols. - sandbox/e2b.py: E2BSandboxBackend with a threaded BgJob wrapper that provides timeout support over E2B's CommandHandle. - harness.py: OpenCodeSession + OpenCodeSessionFactory; in Mode B, installs proxy deps, uploads the proxy module, starts it as a bg job on localhost:7000, rewrites opencode.json to point at the proxy, and forces @ai-sdk/openai-compatible so routing goes through /v1/chat/completions. - interception.py: InterceptionProxy (FastAPI, unary + streaming), per-turn trace capture, CLI entry point for sandbox-side execution. Tests: 37 unit tests plus 3 live integration tests gated on E2B_API_KEY / OPENAI_API_KEY. Known limitation: OpenAI's gpt-5.x chat family refuses logprob requests, so Mode B live validation against OpenAI requires gpt-4o-mini or older. vLLM (the intended training-time upstream) returns logprobs natively. * feat(opencode_env): live vLLM validation + post-rollout summary End-to-end Mode B now verified against a live vLLM tunnel: - Qwen/Qwen3.5-4B on 2x A100 (tp=2, 16K ctx) via `vllm serve` - Cloudflared tunnel exposes the endpoint publicly - E2B sandbox runs opencode against the tunnel through the in-sandbox proxy - Proxy captures 4 turns, 36 tokens with real per-token logprobs - Agent produces correct fizzbuzz.py, verifier scores reward=1.0 - 55.86s total (sandbox + install + 4 LLM turns + verify) Changes: - config: add proxy_disable_thinking flag; plumbed through harness -> proxy via --disable-thinking CLI arg. - interception: inject chat_template_kwargs.enable_thinking=false on forwarded requests when enabled (Qwen3/Qwen3.5 tokenizer hook); also split the request handler into unary + streaming paths (SSE) and auto-translate max_tokens -> max_completion_tokens for gpt-5.x/o* models. - sandbox/e2b: E2B's commands.run(background=True) has a default server-side timeout=60 that kills long-running opencode bg jobs; pass timeout=0 to disable it. - live_watch: RolloutSummary / collect_rollout_summary / print_rollout_summary — post-rollout structured report reading proxy trace, opencode event log, and the workdir listing + file contents. - tests/test_harness_live_vllm: end-to-end Mode B test against a live vLLM tunnel (gated on VLLM_TUNNEL_URL + E2B_API_KEY), asserts logprobs are captured with shape matching completion tokens. * docs(opencode_env): README with Mode A + Mode B quickstarts and full config * feat(opencode_env): retries, error surfacing, model override Fixes the server-mode rollout path. Under load E2B can return sandboxes that aren't yet accepting commands, curl-install can transiently fail, and opencode's internal title-generation call can emit a stripped model id (e.g. ``Qwen3.5-4B`` instead of ``Qwen/Qwen3.5-4B``) that vLLM rejects with 404. Previously the proxy silently swallowed upstream error bodies and returned an empty event-stream, which opencode interpreted as an empty assistant turn. Changes: - harness: ``_wait_for_sandbox_ready`` probes ``echo ok`` up to 15x before issuing commands. - harness: ``_exec_with_retry`` wraps install / pip-deps / extra-setup with exponential backoff, up to 3 attempts, bailing on deterministic errors (non-empty stderr). - interception: ``ProxyConfig.model_override`` rewrites the ``model`` field on every forwarded request to the exact upstream id, bypassing opencode's provider-prefix quirks. Plumbed through as ``--model-override`` on the CLI. - interception: ``_proxy_streaming`` now inspects upstream status before committing to an SSE response — non-2xx returns a JSON error response to opencode AND logs the full upstream body to proxy.log, so the caller sees the real failure reason. - harness: ``_start_proxy`` passes ``--model-override`` built from ``config.model`` so the upstream always sees the right id. - harness: proxy deps install now uses ``_exec_with_retry`` too. Verified via local uvicorn: fizzbuzz + fibonacci tasks both succeed end-to-end through the server path in 19-20s with reward=1.0, 3-4 productive turns, and real per-token logprobs captured on every turn. * feat(opencode_env): add serve driver + opencode_client (Phase 2b primitive) Adds the infrastructure for driving opencode via its HTTP server instead of the CLI. This is the Phase 2b foundation — fine-grained MCP tools in the consumer server wrap these primitives. Changes: - opencode_client.OpenCodeServerClient: typed httpx wrapper over the OpenAPI spec at /doc. Sync and async methods for create_session, send_message / send_prompt_async, list_messages, get_session, get_all_status, abort, plus stream_events / astream_events (SSE) and a wait_for_ready helper. Base64 basic-auth when OPENCODE_SERVER_PASSWORD is set. - harness.OpenCodeSession: new ``driver: Literal["cli", "serve"]`` field. driver="cli" is today's `opencode run` path. driver="serve" stores serve_public_url + serve_client + serve_session_id on the session. start_agent() dispatches on driver; wait_for_completion() polls /session/:id for idle when driver="serve". New abort() method hits /session/:id/abort for cancellation. - harness.OpenCodeSessionFactory: new ``driver`` constructor arg plumbs through to create(). _start_serve() runs opencode serve bound to 0.0.0.0:4096 as a bg job, probes it internally via curl, then uses the sandbox backend's get_host(4096) to build a public URL (E2B returns https://4096-<sandbox_id>.e2b.app). Fails fast if the backend doesn't support get_host. - tests/test_opencode_client.py: 7 unit tests covering URL/method/body shape, auth header, prompt text extraction, abort bool, limit param, wait_for_ready polling, SSE event helpers. Uses httpx MockTransport patched via monkeypatch — no live opencode serve needed. - tests/test_harness.py: _FakeSandbox now responds to the health-probe "echo ok" command so the existing factory tests work after the Phase-1 reliability layer landed. Verified: 34 unit tests pass (7 new + 27 existing), driver=cli path unchanged. End-to-end E2B spike confirms: sandbox_id assigned in 0.4s opencode install 2.5s opencode serve --port 4096 --hostname 0.0.0.0 listening in 1s sandbox.get_host(4096) returns https://4096-<id>.e2b.app external /doc returns HTTP 200 with OpenAPI spec external POST /session returns real session metadata Next: wire 4 new MCP tools (start_rollout / get_state / abort_rollout / finalize_rollout) in the consumer env + SSE endpoint for live rollout events. Ship to HF Space. * fix(opencode_env): bump proxy-start wait from 10s to 60s Uvicorn+fastapi cold boot inside E2B can take >10s under load; the tight probe loop was producing false 'proxy did not start within 10s' errors. 60s cap at 0.5s intervals keeps the retry fast while tolerating the slow path. * fix(opencode_env): cwd into workdir, fix idle detection, preserve reasoning Three audit fixes surfaced by end-to-end testing through the deployed env server: 1. `_start_serve` now `cd`s into workdir_path(config) before launching opencode serve. Without this, the agent writes files to $HOME and RolloutResult.workdir_files (reading /home/user/workdir) comes back empty — the "rollout succeeded but nothing appeared" symptom. 2. `wait_for_completion` idle check was `status.get("idle")` but opencode's /session/status returns `{"type":"idle"}`, not `{"idle":true}`. Every serve-driver rollout silently timed out at agent_timeout_s. Now checks `status.get("type") == "idle"` and adds structured logging on every tick. 3. Interception proxy now preserves `delta.reasoning` on streaming chunks and surfaces it as `message.reasoning` on the assembled response. HF Router's Qwen3.5 thinking mode returns reasoning as a separate field from content; previously it was dropped. 4. `upstream_model` no longer strips the Qwen/ org prefix — full `config.model` is forwarded as the model-override so both vLLM (served as `Qwen/Qwen3.5-4B`) and HF Router (requires `Qwen/<repo>:<provider>`) work. 5. Structured logging at every factory.create phase so operators can see exactly which step is stuck (sandbox, bootstrap, proxy, serve, wait_for_completion). * test(opencode_env): drop live integration tests requiring external secrets The four live tests (OpenAI / vLLM / mode-B / E2B) required OPENAI_API_KEY, VLLM_URL, or E2B_API_KEY to execute and were development-time fixtures rather than CI checks. The core functionality is already covered offline by test_harness.py (end-to-end factory lifecycle against a mock sandbox + mock OpenAI endpoint), test_interception.py (proxy forward + per-turn record assembly), test_opencode_client.py (serve client over httpx mocks), and test_sandbox_base.py (E2BSandboxBackend key-required unit). * feat(opencode_env): deployable env with HF Space, Gradio UI, and 3-endpoint catalog Wraps the existing OpenCode harness primitive in a deployable OpenEnv environment that can run as an HF Space, exposing a single MCP ``run_rollout`` tool plus a Gradio web UI at /web. Highlights ---------- - Single MCP tool ``run_rollout`` accepting a uniform Task shape (instruction + setup[] + verify[] bash commands), reward = passed_verify / total or override via /home/user/logs/verifier/reward.txt. - Endpoint shorthand catalog (``vllm`` / ``openai`` / ``hf_router``) that resolves base_url / api_key / model from env vars + sane defaults. - In-sandbox FastAPI proxy (``transparent_proxy`` mode) injects logprobs=true and captures per-token logprobs for GRPO training. - Optional ``black_box`` mode skips the proxy for SFT / eval rollouts. - Pre-baked E2B template (``opencode-rl``) drops sandbox cold start from ~2min to ~6s by shipping opencode + proxy deps in the image. - Streaming Gradio UI: /run handler is a generator that yields a live phase log (sandbox boot → setup → agent → verify → collect) so the user sees progress instead of a spinner. - HF Space deployed at AdithyaSK/opencode-env, end-to-end verified against vLLM, OpenAI, and HF Router (all 3 reward=1.0 on the binary_search smoke task). Layout ------ envs/opencode_env/ {client.py, models.py, __init__.py} # HTTP client + pydantic {config.py, harness.py, opencode_runtime.py, task.py} # primitive (CLI-only) server/{app.py, opencode_environment.py, gradio_ui.py, catalog.py, Dockerfile} # FastAPI + Gradio + MCP sandbox/{base.py, e2b.py, interception.py, build_template.py} # E2B + proxy + template {pyproject.toml, openenv.yaml, uv.lock, README.md, .dockerignore, .gitignore} Removed (CLI-only refactor) --------------------------- - harness.py: dropped the ``opencode serve`` driver path (~270 LOC). - Deleted opencode_client.py, live_watch.py, env-local tests/. CI / tests ---------- - New tests/envs/test_opencode_env.py: 14 unit tests (no E2B, no LLM, no network) covering catalog resolution, model serialization, and task coercion. Plus one @pytest.mark.integration test that runs opencode end-to-end against the deployed Space (skipped by default). - sandbox/__init__.py: e2b import wrapped in try/except so the package loads cleanly without e2b installed (CI-friendly). - Added opencode-env to .github/workflows/docker-build.yml matrix so the image is built and pushed to GHCR alongside other envs. openenv-core dependency ----------------------- Currently pinned to the ``opencode-harness`` branch via git because PyPI's ``openenv-core`` (0.2.x) does not yet ship the ``openenv.core.harness`` module that this env imports. Switch to ``openenv-core[core]>=0.2.2`` once RFC 5 / PR #471 ships in a published release. The intended end-state is documented inline in pyproject.toml. * docs(opencode_env): add examples/opencode_env_simple.py Minimal end-to-end example: hits the deployed HF Space, runs a binary_search rollout via the MCP run_rollout tool, prints the reward + per-turn logprobs + the file the agent produced. Mirrors the per-env convention in ``examples/`` (echo_mcp_demo.py / coding_env_inference.py / atari_simple.py etc.). Defaults point at ``https://adithyask-opencode-env.hf.space``; override with ``OPENCODE_ENV_SPACE`` to target a different Space or local container. Requires ``OPENAI_API_KEY`` in the environment (passed in the request body, no Space secret required). Swap ``endpoint="openai"`` for ``"vllm"`` or ``"hf_router"`` to exercise the other backends. * fix: secure opencode proxy command --------- Co-authored-by: burtenshaw <ben.burtenshaw@gmail.com> * fix: address ci docs and formatting * feat(tutorials): add SFT warm-up tutorial with reasoning_gym collect support (#636) * feat(collect): add reasoning_gym support + --dataset-config + --system-prompt Extends `openenv collect` to support reasoning_gym environments: - Register ReasoningGymSessionFactory in _build_session_factory - Add --dataset-config option (JSON string) for env-specific config - Add --system-prompt option to override the default system prompt - Add ReasoningGymSessionFactory in envs/reasoning_gym_env/harness.py Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(harness): generate random seed per episode when none provided reasoning_gym server requires seed when dataset_name is specified. CollectRunner never passes a seed, so generate one per episode to ensure variety across collected rollouts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(llm_client): use max_completion_tokens for OpenAI client Newer OpenAI models (gpt-5-mini, o1, o3) reject max_tokens and require max_completion_tokens instead. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(llm_client): use max_completion_tokens only for OpenAI API, not self-hosted Newer OpenAI models require max_completion_tokens; self-hosted OpenAI-compatible endpoints (vLLM, Ollama, TGI) only support max_tokens. Add use_max_completion_tokens flag to OpenAIClient, enabled automatically by create_llm_client for the openai provider and left off for self-hosted endpoints via --llm-endpoint. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(llm_client): omit temperature for OpenAI API newer models gpt-5-mini and other newer OpenAI models only accept the default temperature (1) and reject any explicit value. Omit temperature entirely when use_max_completion_tokens is set; self-hosted endpoints are unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(collect): add rich progress bar to CollectRunner Shows episode count, collected count, running avg reward, and elapsed time during collection — previously the loop ran silently. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(tutorials): add SFT warm-up tutorial with end-to-end validated notebook Adds docs/source/tutorials/sft-warmup.md and examples/sft_warmup.ipynb. Tutorial collects rollouts via CollectRunner Python API, pushes to Hub, filters by reward, and fine-tunes Qwen3-1.7B with SFTTrainer. Validated end-to-end: 0% → 64% format compliance, 4% → 60% accuracy on chain_sum. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(tutorials,collect): address PR review findings - Scope use_max_completion_tokens to models that require it (gpt-5-mini, o1, o3, o4-mini) — was incorrectly applied to all OpenAI models, breaking gpt-4o and other standard models - Remove duplicate YOUR_HF_USERNAME re-declaration in tutorial section 10 - Add missing asyncio import in tutorial section 10 code block - Fix prose: max_seq_length → max_length (correct SFTConfig field name) - Fix usort import order in collect.py and harness/collect.py Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(collect,tutorials): address Greptile review findings - Move _MAX_COMPLETION_TOKENS_PREFIXES to module level as frozenset - Use prefix matching for versioned model names (o1-2024-12-17, etc.) - Replace asyncio.run() with await in tutorial section 10 (Jupyter compat) - Use json.dumps for tool_call text in to_qwen3_messages (safe escaping) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: burtenshaw <ben.burtenshaw@gmail.com> * fix: address harness review feedback * fix: format harness lint * fix: address harness review comments --------- Co-authored-by: Sergio Paniego Blanco <sergiopaniegoblanco@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Adithya S K <adithyaskolavi@gmail.com>
* feat(opencode_env): OpenCode coding-agent harness environment Re-introduces the OpenCode env that was developed in PR #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. * fix: address opencode env ci lint --------- Co-authored-by: burtenshaw <ben.burtenshaw@gmail.com>
Stacked on #471.
Adds
envs/opencode_env/, a harness primitive that runs the OpenCodecoding agent inside a sandbox against an OpenAI-compatible LLM endpoint
and exposes it via the
ResourceSessionFactory/ResourceSessioncontracts from
openenv.core.harness.Functionality
Session + factory
OpenCodeSession(ResourceSession)implements:initial_messages() -> list[Message]— returns the task instructionas the single user message.
list_tools() -> list[Tool]— returns[]. OpenCode owns its owntool loop internally; no tools are exposed to the harness.
call_tool(name, arguments) -> ToolResult— returns an errorToolResult; direct tool invocation is not supported (by design).verify(transcript, final_state=None) -> VerifyResult— delegatesto an optional user-supplied verifier callable; defaults to
VerifyResult(env_reward=None, done=True).close()— kills any background jobs and the sandbox.OpenCodeSessionFactory(ResourceSessionFactory)implements:create(task, seed=None, episode_id=None) -> OpenCodeSession—creates a sandbox, bootstraps it, optionally starts the proxy,
optionally starts
opencode serve, fires the task, returns thesession.
Session also exposes agentic-runtime methods (not part of the ABC):
start_agent(),wait_for_completion(timeout_s),abort(),fetch_trace(),fetch_proxy_trace().Two drivers
driver=\"cli\"— runsopencode run --format jsonas a backgroundshell job. Completion is detected by subprocess exit.
driver=\"serve\"— runsopencode serve --port 4096 --hostname 0.0.0.0as a background job, exposes the port viasandbox.get_host(4096), creates a session throughPOST /session,fires the instruction via
POST /session/:id/prompt_async.Completion is detected by polling
GET /session/statusfor{\"type\": \"idle\"}.Two modes
mode=\"black_box\"— opencode talks directly to the caller-suppliedLLM endpoint.
mode=\"transparent_proxy\"— a FastAPI/uvicorn proxy runs onlocalhost:7000inside the sandbox. The primitive rewritesopencode.jsonso the agent talks to the proxy. The proxy:logprobs=trueandtop_logprobs=Non every forwardedchat-completions request.
max_tokens/max_completion_tokenstoproxy_max_tokens_cap.chat_template_kwargs.enable_thinkingwhenproxy_disable_thinkingis set.modelfield.per-token logprobs, reasoning) into a per-turn record and writes
it to
proxy_trace.jsonl.Interception proxy details
accumulating per-turn state.
delta.reasoningfrom streamed chunks and reassembles itas
message.reasoningon the final response.{turn_index, request_body, response_body, completion_tokens, per_token_logps, finish_reason, reasoning, latency_s, timestamp}.Configuration
OpenCodeConfig(Pydantic) fields:provider,base_url,api_key,model,request_timeout_ms.opencode_version,disabled_tools,enabled_tools,system_prompt,extra_opencode_json.run_format,agent_timeout_s,extra_env,extra_setup_shell.sandbox_home.proxy_max_tokens_cap,proxy_top_logprobs,proxy_disable_thinking.opencode_runtime.py— pure builder functions called duringbootstrap:
build_opencode_json,build_install_cmd,build_run_cmd,build_env_vars, plus path helpers(
opencode_config_path,instruction_path,agent_log_path,system_prompt_path,verifier_reward_path,workdir_path).opencode_client.py—OpenCodeServerClient, a typed httpx wrapperover the opencode serve HTTP API:
create_session,send_message(sync),
send_prompt_async,list_messages,get_all_status,abort,stream_events(sync and async SSE).Sandbox abstraction
sandbox/base.pydefines aSandboxBackendProtocol (create,exec,start_bg,write_text,read_text,get_host,kill)plus dataclasses (
SandboxHandle,BgJob,ExecResult).sandbox/e2b.pyprovidesE2BSandboxBackend, a thin wrapper overthe E2B Code Interpreter SDK.
OpenCodeSessionFactory(backend=...).Task payload
OpenCodeTask(instruction, setup_shell=None, upload_files={}, metadata={})is the payload shapefactory.create(task)accepts.OpenCodeTask.coerce(value)accepts a bare string, a dict, or anexisting
OpenCodeTask.Public API
Tests
envs/opencode_env/tests/, ~40 tests, all runnable offline (nonetwork, no external secrets):
test_config.py— pydantic validation.test_task.py—OpenCodeTask.coercevariants.test_opencode_runtime.py— builder outputs for opencode.json,install/run commands, env vars, path helpers.
test_interception.py— request/response transformations against afake upstream, streaming-chunk assembly,
reasoningpreservation.test_opencode_client.py— serve-API client against mocked HTTP.test_sandbox_base.py— Protocol conformance and E2B-key requiredcheck.
test_harness.py— end-to-end factory lifecycle against a mocksandbox and mock OpenAI endpoint.
File layout
Test plan
pytest envs/opencode_env/tests/ -vpasses.