-
Notifications
You must be signed in to change notification settings - Fork 580
Add reusable OpenEnv taskset support #1885
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| # openenv-echo-v1 | ||
|
|
||
| OpenEnv's official Echo image as a minimal v1 example. The reusable image lifecycle and | ||
| JSON-RPC-to-MCP bridge live in `verifiers`; this package only pins the image, prompt, and | ||
| resources. | ||
|
|
||
| ## Develop | ||
|
|
||
| 1. From the Verifiers checkout root, add both the current framework and this example to the | ||
| ephemeral `uv run` environment. | ||
| 2. Run it with any MCP-capable Verifiers harness and a container runtime: | ||
|
|
||
| ```bash | ||
| uv run --with-editable . --with-editable environments/openenv_echo_v1 \ | ||
| eval openenv-echo-v1 -n 1 --harness.runtime.type docker | ||
| uv run --with-editable . --with-editable environments/openenv_echo_v1 \ | ||
| eval openenv-echo-v1 -n 1 --harness.runtime.type docker --harness.id rlm | ||
| uv run --with-editable . --with-editable environments/openenv_echo_v1 \ | ||
| eval openenv-echo-v1 -n 1 --harness.runtime.type docker --harness.id codex \ | ||
| -m nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B | ||
| ``` | ||
|
|
||
| ## Layout | ||
|
|
||
| - `openenv_echo_v1/taskset.py` — a thin config over `vf`'s reusable `OpenEnvTaskset`. | ||
|
|
||
| Echo's production MCP contract is unscored, so the reward is neutral while the tool call and | ||
| result remain in the Verifiers trace. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from openenv_echo_v1.taskset import OpenEnvEchoTaskset | ||
|
|
||
| __all__ = ["OpenEnvEchoTaskset"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| """OpenEnv's official Echo image as a zero-config example taskset.""" | ||
|
|
||
| import verifiers.v1 as vf | ||
| from verifiers.v1.tasksets.openenv import OpenEnvConfig, OpenEnvTaskset | ||
|
|
||
| ECHO_IMAGE = ( | ||
| "ghcr.io/meta-pytorch/openenv-echo-env@" | ||
| "sha256:56c55669c00b23a6af6adbcd8dd1fb5da3a276aec186b5c46cb4abeb708afa9c" | ||
| ) | ||
|
|
||
|
|
||
| class OpenEnvEchoConfig(OpenEnvConfig): | ||
| image: str = ECHO_IMAGE | ||
| prompt: str = ( | ||
| 'Call the echo_message tool with the message "Hello, World!", then return ' | ||
| "the echoed text." | ||
| ) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Echo prompt wrong tool nameMedium Severity The example task prompt tells the model to call Reviewed by Cursor Bugbot for commit 0a2178f. Configure here. |
||
| resources: vf.TaskResources = vf.TaskResources(cpu=2, memory=4, disk=10) | ||
|
|
||
|
|
||
| class OpenEnvEchoTaskset(OpenEnvTaskset, vf.Taskset[vf.Task, OpenEnvEchoConfig]): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In this inheritance list, Useful? React with 👍 / 👎. |
||
| pass | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| [project] | ||
| name = "openenv-echo-v1" | ||
| version = "0.1.0" | ||
| description = "openenv-echo-v1 — <one-line description>." | ||
| requires-python = ">=3.11" | ||
| dependencies = ["verifiers"] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium
🚀 Reply "fix it for me" or copy this AI Prompt for your agent: |
||
|
|
||
| [build-system] | ||
| requires = ["hatchling"] | ||
| build-backend = "hatchling.build" | ||
|
|
||
| [tool.hatch.build.targets.wheel] | ||
| packages = ["openenv_echo_v1"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,205 @@ | ||
| from pathlib import Path | ||
| from unittest.mock import AsyncMock, Mock | ||
|
|
||
| import httpx | ||
| import pytest | ||
| from mcp.server.fastmcp import FastMCP | ||
|
|
||
| import verifiers.v1 as vf | ||
| from verifiers.v1.env import EnvConfig, Environment | ||
| from verifiers.v1.runtimes import ProgramResult | ||
| from verifiers.v1.runtimes.subprocess import SubprocessConfig, SubprocessRuntime | ||
| from verifiers.v1.tasksets.openenv import OpenEnvConfig, OpenEnvTaskset | ||
| from verifiers.v1.tasksets.openenv.taskset import ( | ||
| OPENENV_SOCKET, | ||
| OPENENV_URL, | ||
| ) | ||
|
|
||
| ECHO_EXAMPLE = Path(__file__).parents[2] / "environments" / "openenv_echo_v1" | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_uv_script_ignores_image_system_python() -> None: | ||
| runtime = SubprocessRuntime(SubprocessConfig()) | ||
| runtime.write = AsyncMock() | ||
| runtime.run = AsyncMock( | ||
| side_effect=[ | ||
| ProgramResult(exit_code=0, stdout="", stderr=""), | ||
| ProgramResult( | ||
| exit_code=0, stdout="/tmp/openenv-test/bin/python\n", stderr="" | ||
| ), | ||
| ] | ||
| ) | ||
|
|
||
| await runtime.prepare_uv_script("# openenv image system-python regression\n") | ||
|
|
||
| prepare_command = runtime.run.await_args_list[1].args[0][2] | ||
| assert "; unset UV_SYSTEM_PYTHON; uv sync" in prepare_command | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("harness_id", [None, "rlm", "codex"]) | ||
| def test_openenv_uses_an_external_mcp_harness(harness_id: str | None) -> None: | ||
| harness: dict[str, object] = {"runtime": {"type": "docker"}} | ||
| if harness_id: | ||
| harness["id"] = harness_id | ||
| config = EnvConfig.model_validate( | ||
| { | ||
| "taskset": { | ||
| "id": "openenv", | ||
| "image": "openenv:test", | ||
| "prompt": "Use the available tool.", | ||
| "resources": {"cpu": 2, "memory": 4, "disk": 10}, | ||
| }, | ||
| "harness": harness, | ||
| } | ||
| ) | ||
| env = Environment(config) | ||
| task = env.taskset.load_tasks()[0] | ||
| toolset = env.taskset.tools(task)[0] | ||
|
|
||
| assert env.harness.config.id == (harness_id or "default") | ||
| assert OpenEnvConfig.model_fields["image"].is_required() | ||
| assert OpenEnvConfig.model_fields["prompt"].is_required() | ||
| assert task.name == "openenv" | ||
| assert task.image == "openenv:test" | ||
| assert task.workdir == "/app/env" | ||
| assert task.resources.cpu == 2 | ||
| assert task.resources.memory == 4 | ||
| assert task.resources.disk == 10 | ||
| assert isinstance(toolset, vf.JSONRPCToolset) | ||
| assert toolset.config.colocated is True | ||
| assert toolset.config.endpoint == f"{OPENENV_URL}/mcp" | ||
| assert toolset.config.uds == OPENENV_SOCKET | ||
|
|
||
|
|
||
| def test_echo_image_is_owned_by_the_example(monkeypatch) -> None: | ||
| monkeypatch.syspath_prepend(str(ECHO_EXAMPLE)) | ||
| from openenv_echo_v1.taskset import ( | ||
| ECHO_IMAGE, | ||
| OpenEnvEchoConfig, | ||
| OpenEnvEchoTaskset, | ||
| ) | ||
|
|
||
| taskset = OpenEnvEchoTaskset(OpenEnvEchoConfig(id="openenv-echo-v1")) | ||
| task = taskset.load_tasks()[0] | ||
|
|
||
| assert task.name == "openenv-echo-v1" | ||
| assert task.image == ECHO_IMAGE | ||
| assert task.prompt == ( | ||
| 'Call the echo_message tool with the message "Hello, World!", then return ' | ||
| "the echoed text." | ||
| ) | ||
| assert task.resources.cpu == 2 | ||
| assert task.resources.memory == 4 | ||
| assert task.resources.disk == 10 | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_openenv_setup_only_starts_its_server() -> None: | ||
| taskset = OpenEnvTaskset( | ||
| OpenEnvConfig(id="openenv", image="openenv:test", prompt="Use the tool.") | ||
| ) | ||
| runtime = AsyncMock() | ||
|
|
||
| await taskset.setup(taskset.load_tasks()[0], runtime) | ||
|
|
||
| runtime.run_background.assert_awaited_once_with( | ||
| [ | ||
| "/app/.venv/bin/uvicorn", | ||
| "server.app:app", | ||
| "--uds", | ||
| OPENENV_SOCKET, | ||
| ], | ||
| {"ENABLE_WEB_INTERFACE": "false"}, | ||
| "/tmp/openenv.log", | ||
| ) | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_codex_harness_receives_openenv_mcp_server() -> None: | ||
| from verifiers.v1.harnesses.codex import CodexHarness, CodexHarnessConfig | ||
|
|
||
| runtime = Mock() | ||
| runtime.run_program = AsyncMock( | ||
| return_value=ProgramResult(exit_code=0, stdout="", stderr="") | ||
| ) | ||
| harness = CodexHarness(CodexHarnessConfig(id="codex")) | ||
|
|
||
| await harness.launch( | ||
| vf.RolloutContext( | ||
| model="nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B", | ||
| client=Mock(), | ||
| sampling=vf.SamplingConfig(), | ||
| ), | ||
| vf.Trace(task=vf.Task(idx=0, prompt="Use the Echo tool.")), | ||
| runtime, | ||
| endpoint="http://127.0.0.1:9000/v1", | ||
| secret="secret", | ||
| mcp_urls={"jsonrpc": "http://127.0.0.1:12345/mcp"}, | ||
| ) | ||
|
|
||
| argv, _ = runtime.run_program.await_args.args | ||
| assert CodexHarness.SUPPORTS_MCP is True | ||
| assert harness.config.version == "0.116.0" | ||
| assert 'mcp_servers.jsonrpc.url="http://127.0.0.1:12345/mcp"' in argv | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_jsonrpc_toolset_bridges_tools_to_mcp(monkeypatch) -> None: | ||
| responses = iter( | ||
| [ | ||
| { | ||
| "jsonrpc": "2.0", | ||
| "id": 1, | ||
| "result": { | ||
| "tools": [ | ||
| { | ||
| "name": "echo", | ||
| "description": "Echo text.", | ||
| "inputSchema": { | ||
| "type": "object", | ||
| "properties": {"message": {"type": "string"}}, | ||
| "required": ["message"], | ||
| }, | ||
| } | ||
| ] | ||
| }, | ||
| }, | ||
| {"jsonrpc": "2.0", "id": 1, "result": {"data": "hello"}}, | ||
| ] | ||
| ) | ||
| requests = [] | ||
|
|
||
| class Client: | ||
| def __init__(self, **kwargs): | ||
| pass | ||
|
|
||
| async def __aenter__(self): | ||
| return self | ||
|
|
||
| async def __aexit__(self, *args): | ||
| pass | ||
|
|
||
| async def post(self, url, json): | ||
| requests.append((url, json)) | ||
| return httpx.Response( | ||
| 200, | ||
| json=next(responses), | ||
| request=httpx.Request("POST", url), | ||
| ) | ||
|
|
||
| monkeypatch.setattr("verifiers.v1.mcp.toolset.httpx.AsyncClient", Client) | ||
| toolset = vf.JSONRPCToolset(vf.JSONRPCToolsetConfig(endpoint="http://openenv/mcp")) | ||
| await toolset.setup() | ||
|
|
||
| mcp = FastMCP("test") | ||
| toolset._register(mcp) | ||
| registered = await mcp.list_tools() | ||
| result = await mcp.call_tool("echo", {"message": "hello"}) | ||
|
|
||
| assert registered[0].inputSchema["required"] == ["message"] | ||
| assert "hello" in result[0].text | ||
| assert requests[-1][1]["params"] == { | ||
| "name": "echo", | ||
| "arguments": {"message": "hello"}, | ||
| } |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Echo env README noncompliant
Low Severity
The new
openenv_echo_v1package uses a hand-written README instead of the section structure produced byprime env init/uv run init(intro, Develop steps, Layout, CLI tuning). Project rules disallow freeform environment READMEs for packages underenvironments/.Triggered by project rule: BugBot Instructions
Reviewed by Cursor Bugbot for commit d7020e9. Configure here.