Skip to content

Add OpenHands CLI agent - #116

Merged
Julia-Lex merged 1 commit into
mainfrom
codex/add-openhands-agent
Jul 31, 2026
Merged

Add OpenHands CLI agent#116
Julia-Lex merged 1 commit into
mainfrom
codex/add-openhands-agent

Conversation

@Julia-Lex

Copy link
Copy Markdown
Contributor

Summary

Adds an OpenHands CLI agent adapter for browser benchmark runs. The adapter launches openhands --headless --json, injects Playwright MCP through a task-local OpenHands home, maps OpenHands JSONL events into benchmark actions/results, and registers the agent in config, docs, and Docker optional CLI installs.

Gemini CLI is intentionally not included in this PR.

Contribution type

  • Agent adapter
  • Browser backend
  • Benchmark task or data
  • Leaderboard/result submission
  • Evaluation or judge strategy
  • Documentation/example
  • Bug fix

Reproduction or validation

uv run pytest tests/browseruse_bench/test_openhands_agent.py tests/browseruse_bench/test_config_loader.py tests/browseruse_bench/test_cli_list.py
# 84 passed

uv run ruff check browseruse_bench/agents/openhands.py tests/browseruse_bench/test_openhands_agent.py
# All checks passed

git diff --check
# passed

uv run bubench run --agent openhands --data LexBench-Browser --mode single --browser-id local --timeout 600
# Success: 1 | Failed: 0

env ANTHROPIC_API_KEY= uv run pytest tests/
# 840 passed, 2 skipped, 3 warnings

Note: running uv run pytest tests/ with the local disabled Claude key present still reaches the Claude Code external smoke and fails with API Error: 403 API Key 所属分组已停用. That is unrelated to this OpenHands adapter; the clean validation above explicitly blanks ANTHROPIC_API_KEY so that external smoke is skipped.

Result artifacts

OpenHands real smoke output:

/Users/zhu/.codex/worktrees/73b0/browseruse-agent-bench/experiments/LexBench-Browser/All/openhands/gpt-5.5/20260731_143248

The smoke result had env_status=success, agent_done=done, 10 steps, and 4 screenshots.

Self-review (mandatory)

See Mandatory Self-Review.
PRs with unticked boxes are returned without review. / 以下勾选项未完成的 PR 会被直接打回。

  • I read every line of my diff and every change is intentional
    / 我逐行读完了自己的 diff,每处改动都是有意为之
  • The diff passes the hard rules in CONTRIBUTING.md (logger not print, specific exceptions,
    no hardcoded config, imports at top) / diff 符合 CONTRIBUTING.md 的硬性红线
  • I ran a local /code-review and fixed or justified every finding
    / 本地跑过 /code-review,findings 已全部修复或说明理由
  • uv run pytest tests/ passes and behavior changes have targeted tests
    / 测试通过,行为变更配了针对性测试
  • Agent-touching change: real smoke run evidence is included above, or this PR does not
    touch any agent runtime path / 涉及 agent 运行路径的改动已附真实 smoke run 证据,或本
    PR 不涉及 agent 运行路径
  • No secrets, cookies, or unredacted logs in the diff / diff 中无密钥、cookie、未脱敏日志

Notes for reviewers

OpenHands requires Python 3.12+ when installed as a uv tool. The adapter currently supports local/self-launched browsers and CDP-capable managed browser backends via Playwright MCP; non-CDP backends fail fast with an explicit error.

Per user instruction, self-review was performed manually instead of using Claude review automation.

@Julia-Lex
Julia-Lex merged commit 6a0b310 into main Jul 31, 2026
1 of 3 checks passed
@Julia-Lex
Julia-Lex deleted the codex/add-openhands-agent branch July 31, 2026 06:46

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2c18cd240a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +327 to +329
env_status, agent_done = self._map_exit_status(
returncode, execution_error, has_result=bool(answer)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Treat nonzero OpenHands exits as failed runs

When OpenHands emits an agent message and then exits nonzero without a JSON error event—for example, after an MCP failure reported only on stderr—answer is truthy, so _map_exit_status(..., has_result=True) returns success/done; the subsequent checks neither inspect the return code nor read stderr. This records a crashed task as completed and admits its partial answer into evaluation. Preserve the partial answer if useful, but mark nonzero exits as failed unless the run timed out.

Useful? React with 👍 / 👎.

Comment on lines +220 to +221
env = {**os.environ, "HOME": str(home_dir), "OH_PERSISTENCE_DIR": str(home_dir / ".openhands")}
env.setdefault("UV_CACHE_DIR", str(Path.home() / ".cache" / "uv"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve npm's shared cache before replacing HOME

When NPM_CONFIG_CACHE is unset, as in the documented non-Docker installation, replacing HOME makes npm resolve its cache to the task-local .openhands-home/.npm. Because every MCP launch uses npx @playwright/mcp@latest, each benchmark task starts with an empty npx cache, repeatedly downloads Playwright MCP, inflates end-to-end latency, and makes otherwise cached runs network-dependent. Preserve the operator's npm cache explicitly, as is already done for UV_CACHE_DIR.

Useful? React with 👍 / 👎.

Comment on lines +42 to +44
from browseruse_bench.agents import openhands # noqa: F401
except ImportError as exc:
logger.warning("Skipping optional agent module browseruse_bench.agents.openhands: %s", exc)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Do not hide OpenHands import defects as an optional skip

The OpenHands module has no optional Python SDK import, so catching every ImportError here also swallows project or interface errors raised by its imports. In such an environment the provider is silently left unregistered and users receive a misleading unknown-agent failure rather than the underlying defect. Import it directly, or move genuinely optional loading into a lazy factory that only tolerates the target package being absent.

AGENTS.md reference: AGENTS.md:L12-L13

Useful? React with 👍 / 👎.

Comment on lines +224 to +227
if agent_config.get("api_key"):
env["LLM_API_KEY"] = str(agent_config["api_key"])
if agent_config.get("base_url"):
env["LLM_BASE_URL"] = str(agent_config["base_url"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Clear inherited OpenHands model endpoint variables

When the selected model configuration omits api_key or base_url—including when an unset $OPENAI_BASE_URL expands to an empty string—the corresponding inherited LLM_API_KEY or LLM_BASE_URL remains in this copied environment because these branches only overwrite truthy values. Since the command passes --override-with-envs, a stale operator-level OpenHands variable can silently authenticate against or route the benchmark to a different provider than the selected config. Remove both variables from the inherited environment before conditionally adding the resolved values.

Useful? React with 👍 / 👎.

home_dir = self._write_workspace_config(agent_config, task_workspace, cdp_url)
cmd = self._build_command(f"{rules}\n\n{prompt}", agent_config)

env = {**os.environ, "HOME": str(home_dir), "OH_PERSISTENCE_DIR": str(home_dir / ".openhands")}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Redirect the OpenHands user profile on Windows

On Windows, setting HOME alone does not redirect ~: Python's Windows home resolution prefers USERPROFILE and then HOMEDRIVE/HOMEPATH. Consequently OpenHands can read the operator's ~/.openhands rather than the task-local directory where this adapter wrote mcp.json, leaving the Playwright MCP server unavailable or exposing mutable user configuration. Set the Windows profile variables as well, or pass an explicit OpenHands config location.

Useful? React with 👍 / 👎.

Comment thread Dockerfile
&& rm -rf /var/lib/apt/lists/* \
&& if [ "$USE_CN_MIRROR" = "true" ]; then npm config set registry https://registry.npmmirror.com -g; fi \
&& npm install -g @anthropic-ai/claude-code @openai/codex openclaw \
&& UV_TOOL_DIR=/opt/uv-tools UV_TOOL_BIN_DIR=/usr/local/bin uv tool install openhands --python 3.12 \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Relocate the uv-managed Python used by OpenHands

In this python:3.11-slim stage, --python 3.12 makes uv download a managed interpreter; uv python dir shows that the default installation directory is /root/.local/share/uv/python. Setting only UV_TOOL_DIR relocates the tool environment, not that interpreter, so its Python executable references a path beneath /root, which mode 0700 prevents the documented uid-1000 runtime user from traversing. The OpenHands entry point therefore cannot start in the CLI-enabled image. Set UV_PYTHON_INSTALL_DIR to a shared location (and grant traversal), or install Python 3.12 somewhere accessible to the runtime user.

Useful? React with 👍 / 👎.

"mcpServers": {
"playwright": {
"command": agent_config.get("playwright_mcp_command", "npx"),
"args": build_playwright_mcp_args(agent_config, cdp_url),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Remove credential-bearing CDP URLs from task artifacts

For managed backends whose CDP URL embeds credentials, this persists the secret verbatim in every task workspace. For example, browseruse_bench/browsers/providers/steel.py lines 52–54 explicitly append the configured apiKey to the CDP URL, and this mcp.json is never removed or scrubbed after the subprocess exits. Anyone receiving or inspecting the experiment directory can recover that provider key. Delete or redact the MCP configuration in a finally cleanup path once OpenHands has stopped.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant