Skip to content

Commit 8e6a019

Browse files
committed
fix(osworld): validate Chrome CDP relay setup
1 parent 8fe1d57 commit 8e6a019

3 files changed

Lines changed: 119 additions & 0 deletions

File tree

‎benchmarks/osworld/README.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,25 @@ Docker SSH setup below. Start the model first, verify the environment second,
4141
then prepare and run Gym. Neither the operator workstation nor a persistent
4242
interactive SSH session is part of runtime communication.
4343

44+
### Chrome CDP port ownership
45+
46+
OSWorld task setup, rather than a deployment script or the Gym adapter, owns
47+
the guest processes that expose Chrome DevTools. Canonical tasks launch Chrome
48+
with `--remote-debugging-port=1337` and then launch
49+
`socat tcp-listen:9222,fork tcp:localhost:1337`. `DesktopEnv.reset()` executes
50+
both commands from `verifier_metadata.osworld_task.config` for each fresh VM.
51+
52+
The Docker image or OpenSandbox Pool must include `socat` and publish guest
53+
port 9222; Gym forwards that published HTTP/WebSocket endpoint to OSWorld. A
54+
user running canonical OSWorld inputs does not need to run either command
55+
manually. Authors of custom inputs must include the relay whenever their task
56+
setup starts Chrome CDP on port 1337. `prepare.py` checks this contract and
57+
fails early with the missing setup command instead of allowing a later 502.
58+
59+
A standalone Sandbox API smoke test that bypasses `DesktopEnv.reset()` must
60+
start both Chrome and the relay before probing port 9222. Screenshot-only
61+
checks against the OSWorld service on port 5000 do not require this relay.
62+
4463
## Requirements
4564

4665
- Linux x86_64 with Docker 20+ and access to the local Docker daemon.

‎benchmarks/osworld/prepare.py‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import hashlib
2222
import json
2323
import os
24+
import re
2425
from collections.abc import Sequence
2526
from pathlib import Path
2627
from typing import Any
@@ -67,6 +68,52 @@
6768
)
6869

6970

71+
def _setup_command_texts(task: dict[str, Any]) -> tuple[str, ...]:
72+
"""Return commands that OSWorld will execute during the task setup."""
73+
74+
config = task.get("config")
75+
if not isinstance(config, list):
76+
return ()
77+
78+
commands: list[str] = []
79+
for setup_item in config:
80+
if not isinstance(setup_item, dict):
81+
continue
82+
parameters = setup_item.get("parameters")
83+
if not isinstance(parameters, dict):
84+
continue
85+
command = parameters.get("command")
86+
if isinstance(command, str):
87+
commands.append(command)
88+
elif isinstance(command, list):
89+
commands.append(" ".join(str(argument) for argument in command))
90+
return tuple(commands)
91+
92+
93+
def _validate_chrome_cdp_relay(task: dict[str, Any], *, line_number: int) -> None:
94+
"""Require OSWorld's guest relay when task setup starts Chrome CDP."""
95+
96+
commands = _setup_command_texts(task)
97+
starts_chrome_cdp = any(
98+
re.search(r"--remote-debugging-port(?:=|\s+)1337(?!\d)", command, flags=re.IGNORECASE)
99+
for command in commands
100+
)
101+
has_cdp_relay = any(
102+
re.search(r"\bsocat\b", command, flags=re.IGNORECASE)
103+
and re.search(r"\blisten:9222(?!\d)", command, flags=re.IGNORECASE)
104+
and re.search(r"\b(?:localhost|127\.0\.0\.1):1337(?!\d)", command, flags=re.IGNORECASE)
105+
for command in commands
106+
)
107+
if starts_chrome_cdp and not has_cdp_relay:
108+
task_id = str(task.get("id") or "<unknown>")
109+
raise ValueError(
110+
f"OSWorld row {line_number} task {task_id!r} starts Chrome CDP on guest port 1337 "
111+
"but verifier_metadata.osworld_task.config does not launch the required guest relay. "
112+
"Add ['socat', 'tcp-listen:9222,fork', 'tcp:localhost:1337']; Gym publishes and "
113+
"forwards port 9222 but does not start this task-owned process."
114+
)
115+
116+
70117
def prepare(input_jsonl: Path = DEFAULT_INPUT) -> Path:
71118
"""Validate and return an OSWorld JSONL suitable for rollout collection."""
72119

@@ -88,6 +135,7 @@ def prepare(input_jsonl: Path = DEFAULT_INPUT) -> Path:
88135
metadata = row.get("verifier_metadata")
89136
if not isinstance(metadata, dict) or not isinstance(metadata.get("osworld_task"), dict):
90137
raise ValueError(f"OSWorld row {line_number} must contain verifier_metadata.osworld_task")
138+
_validate_chrome_cdp_relay(metadata["osworld_task"], line_number=line_number)
91139
row_count += 1
92140

93141
if row_count == 0:

‎benchmarks/osworld/tests/test_prepare.py‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,58 @@ def test_prepare_validates_committed_example() -> None:
3232
assert prepare() == DEFAULT_INPUT.resolve()
3333

3434

35+
def test_prepare_rejects_chrome_cdp_without_guest_relay(tmp_path: Path) -> None:
36+
input_jsonl = tmp_path / "missing-cdp-relay.jsonl"
37+
input_jsonl.write_text(
38+
json.dumps(
39+
{
40+
"verifier_metadata": {
41+
"osworld_task": {
42+
"id": "chrome-without-relay",
43+
"config": [
44+
{
45+
"type": "launch",
46+
"parameters": {"command": "google-chrome --remote-debugging-port 1337"},
47+
}
48+
],
49+
}
50+
}
51+
}
52+
)
53+
+ "\n",
54+
encoding="utf-8",
55+
)
56+
57+
with pytest.raises(ValueError) as exc_info:
58+
prepare(input_jsonl)
59+
60+
message = str(exc_info.value)
61+
assert "OSWorld row 1" in message
62+
assert "chrome-without-relay" in message
63+
assert "tcp-listen:9222" in message
64+
assert "task-owned process" in message
65+
66+
67+
def test_prepare_does_not_require_cdp_relay_without_chrome_cdp(tmp_path: Path) -> None:
68+
input_jsonl = tmp_path / "non-chrome.jsonl"
69+
input_jsonl.write_text(
70+
json.dumps(
71+
{
72+
"verifier_metadata": {
73+
"osworld_task": {
74+
"id": "non-chrome-task",
75+
"config": [{"type": "launch", "parameters": {"command": ["libreoffice"]}}],
76+
}
77+
}
78+
}
79+
)
80+
+ "\n",
81+
encoding="utf-8",
82+
)
83+
84+
assert prepare(input_jsonl) == input_jsonl.resolve()
85+
86+
3587
@pytest.mark.parametrize(
3688
"shard_args",
3789
[[], ["--num-shards", "1", "--shard-index", "0"]],

0 commit comments

Comments
 (0)