Files
hermes-agent/tests/tools/test_computer_use_browser_authorization.py
T
Francesco Bonacci 9a96fdc5b8 fix(computer-use): enforce existing-profile grant, unblock the opt-in
Live-testing the Cua Driver 0.20 convergence on Windows 11 (session 2,
cua-driver 0.20.0) surfaced three defects in the existing-profile browser
path and in install status.

1. The config grant was silently nullified by an approval bypass.

`--yolo` / `-z` map onto a private unrestricted daemon, which answers every
browser_prepare. Because the host delegated the entire existing-profile
decision to the driver, that bypass also nullified
`computer_use.grant_existing_profile: false`: a plain `hermes -z` attached
to the user's real Chrome profile and read live page content over CDP, with
the driver reporting it as "the approved existing Chromium profile". It was
never approved.

An approval bypass is consent to skip prompts, not consent to read an
existing profile's pages, cookies, and storage. CuaTypedBrowserRoute.prepare
now enforces the key itself, regardless of permission mode. bounded stays
exempt - its reviewed capability manifest is the authorization boundary.
The authorization inputs are resolved in the backend from config and the
backend's immutable mode, never from model-supplied kwargs.

2. The grant, once set, still could not be used.

With `grant_existing_profile: true` the runtime is launched
`--grant existing-profile` correctly, but cua_browser_prepare then hit a
runtime approval prompt anyway - re-asking the user to authorize what the
config already authorized, and making the documented opt-in unusable on any
non-interactive run, where the prompt has nobody to answer it and the call
dies on approval timeout. The durable, file-backed grant now stands in for
that prompt. Scope is narrow: only the existing-profile prepare, only when
the grant is present; isolated launches still prompt and any resolution
failure falls closed to prompting.

3. `computer-use status` hid a custom override and spliced its output.

With HERMES_CUA_DRIVER_CMD pointed at cmd.exe, status printed the child's
multi-line banner and prompt inside the one-line version field, never
mentioned the override, and advised `hermes computer-use install` - which
install itself (correctly) refuses to run against an overridden path. It now
names the override and mirrors install's update-or-unset guidance, and
version output is reduced to one bounded line.

Verified on the reported host: `-z` existing-profile attach now refuses and
names the key; `grant: true` no longer prompts (33s vs a 300s approval
timeout); status names the override and prints one line. No change to the
reconciliation path - driver SHA256 unchanged end to end.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 11:34:40 -07:00

481 lines
15 KiB
Python

"""Authorization plumbing for the cua-driver typed browser route.
Covers the authorization modes that let ``existing_profile`` attachment (and
bounded automation generally) work from Hermes:
* ``bounded`` permission mode — a private embedded daemon launched with a
user-reviewed capability manifest (``--capability-manifest`` +
``--approve-capability-manifest``), failing loudly when the manifest is
missing.
* mode resolution — config supplies standard/bounded only; explicit session
YOLO still (and exclusively) selects unrestricted.
"""
from typing import Any, Dict
import pytest
from tools.computer_use import cua_backend as cb
from tools.computer_use.browser_route import CuaTypedBrowserRoute
from tools.computer_use.cua_backend import _EmbeddedCuaDaemon
def _driver_result(payload: Dict[str, Any]) -> Dict[str, Any]:
return {"structuredContent": dict(payload)}
class _PrepareDriver:
def __init__(self) -> None:
self.calls: list[tuple[str, Dict[str, Any]]] = []
def has_tool(self, name: str) -> bool:
return True
def call(self, name: str, args: Dict[str, Any]) -> Dict[str, Any]:
self.calls.append((name, dict(args)))
return _driver_result({"status": "ok"})
def _route(driver: _PrepareDriver) -> CuaTypedBrowserRoute:
return CuaTypedBrowserRoute(
session_id="hermes-a",
call_tool=driver.call,
has_tool=driver.has_tool,
)
# ── existing-profile authorization ownership ───────────────────────────
def test_existing_profile_prepare_delegates_authorization_to_driver():
driver = _PrepareDriver()
result = _route(driver).prepare(
pid=101,
window_id=202,
profile_mode="existing_profile",
grant_existing_profile=True,
)
assert result["status"] == "ok"
assert driver.calls == [
(
"browser_prepare",
{
"pid": 101,
"window_id": 202,
"strategy": {"kind": "existing_profile"},
"session": "hermes-a",
},
)
]
def test_existing_profile_prepare_refused_without_config_grant():
driver = _PrepareDriver()
result = _route(driver).prepare(
pid=101,
window_id=202,
profile_mode="existing_profile",
)
assert result["status"] == "refused"
assert result["code"] == "browser_existing_profile_not_granted"
assert "computer_use.grant_existing_profile" in result["message"]
# Never reached the driver: the host refuses before the transport.
assert driver.calls == []
def test_existing_profile_prepare_refused_in_unrestricted_without_grant():
"""An approval bypass must not stand in for the config grant.
``--yolo`` / ``-z`` give the session a private unrestricted daemon that
answers every prepare, so without this host-side floor the documented
``grant_existing_profile: false`` default silently stopped protecting the
live profile's pages, cookies, and storage.
"""
driver = _PrepareDriver()
result = _route(driver).prepare(
pid=101,
window_id=202,
profile_mode="existing_profile",
grant_existing_profile=False,
permission_mode="unrestricted",
)
assert result["code"] == "browser_existing_profile_not_granted"
assert driver.calls == []
def test_existing_profile_prepare_bounded_mode_exempt_from_grant():
"""bounded's reviewed capability manifest is the authorization boundary."""
driver = _PrepareDriver()
result = _route(driver).prepare(
pid=101,
window_id=202,
profile_mode="existing_profile",
grant_existing_profile=False,
permission_mode="bounded",
)
assert result["status"] == "ok"
assert [name for name, _ in driver.calls] == ["browser_prepare"]
def test_isolated_prepare_unaffected_by_the_grant():
"""The floor is scoped to existing_profile; isolated launches still work."""
driver = _PrepareDriver()
result = _route(driver).prepare(
pid=101,
profile_mode="isolated_new",
allow_launch=True,
grant_existing_profile=False,
)
assert result["status"] == "ok"
assert [name for name, _ in driver.calls] == ["browser_prepare"]
def test_backend_resolves_authorization_and_ignores_model_supplied_values(monkeypatch):
"""pid/window_id come from the model; the grant never does."""
captured: Dict[str, Any] = {}
class _Route:
def prepare(self, **kwargs: Any) -> Dict[str, Any]:
captured.update(kwargs)
return {"status": "ok"}
backend = cb.CuaDriverBackend.__new__(cb.CuaDriverBackend)
backend.permission_mode = "unrestricted"
monkeypatch.setattr(cb, "_cua_grant_existing_profile", lambda: False)
monkeypatch.setattr(
cb.CuaDriverBackend, "_browser_route", lambda self: _Route()
)
backend.typed_browser_prepare(
pid=101,
window_id=202,
profile_mode="existing_profile",
# A model that tries to grant itself access must be ignored.
grant_existing_profile=True,
permission_mode="bounded",
)
assert captured["grant_existing_profile"] is False
assert captured["permission_mode"] == "unrestricted"
# ── config grant stands in for the approval prompt ──────────────────────
def _preauth(**cfg: Any):
from tools.computer_use import tool as cu_tool
return cu_tool._config_preauthorized
def test_config_grant_preauthorizes_existing_profile_prepare(monkeypatch):
"""The durable opt-in is the authorization; re-prompting is redundant.
It also made the documented opt-in unusable on non-interactive runs,
where the prompt has nobody to answer it and the call dies on approval
timeout rather than attaching.
"""
monkeypatch.setattr(
cb, "_computer_use_cfg", lambda: {"grant_existing_profile": True}
)
assert _preauth()(
"cua_browser_prepare", {"profile_mode": "existing_profile"}
) is True
def test_no_preauthorization_without_the_grant(monkeypatch):
monkeypatch.setattr(cb, "_computer_use_cfg", dict)
assert _preauth()(
"cua_browser_prepare", {"profile_mode": "existing_profile"}
) is False
def test_preauthorization_scoped_to_existing_profile(monkeypatch):
"""Isolated launches keep prompting even when the grant is on."""
monkeypatch.setattr(
cb, "_computer_use_cfg", lambda: {"grant_existing_profile": True}
)
assert _preauth()(
"cua_browser_prepare", {"profile_mode": "isolated_new"}
) is False
assert _preauth()("click", {"profile_mode": "existing_profile"}) is False
def test_preauthorization_fails_closed_on_config_error(monkeypatch):
def _boom():
raise RuntimeError("config unreadable")
monkeypatch.setattr(cb, "_computer_use_cfg", _boom)
assert _preauth()(
"cua_browser_prepare", {"profile_mode": "existing_profile"}
) is False
def test_dispatch_does_not_forward_removed_approval_token():
from unittest.mock import Mock
from tools.computer_use.tool import _dispatch
backend = Mock()
backend.typed_browser_prepare.return_value = {"status": "ok"}
_dispatch(
backend,
"cua_browser_prepare",
{
"pid": 101,
"window_id": 202,
"profile_mode": "existing_profile",
},
)
kwargs = backend.typed_browser_prepare.call_args.kwargs
assert "approval_token" not in kwargs
assert kwargs["profile_mode"] == "existing_profile"
def test_schema_does_not_expose_approval_token():
from tools.computer_use.schema import COMPUTER_USE_SCHEMA
assert "approval_token" not in COMPUTER_USE_SCHEMA["parameters"]["properties"]
# ── bounded embedded daemon ─────────────────────────────────────────────
def test_bounded_daemon_requires_a_manifest():
with pytest.raises(ValueError, match="capability_manifest"):
_EmbeddedCuaDaemon("cua-driver", "bounded")
def test_bounded_daemon_requires_manifest_file_to_exist(tmp_path):
with pytest.raises(ValueError, match="not found"):
_EmbeddedCuaDaemon(
"cua-driver", "bounded",
capability_manifest=str(tmp_path / "missing.yaml"),
)
def test_bounded_daemon_env_does_not_bypass_approvals(tmp_path):
manifest = tmp_path / "manifest.yaml"
manifest.write_text("version: 3\n", encoding="utf-8")
daemon = _EmbeddedCuaDaemon(
"cua-driver", "bounded", capability_manifest=str(manifest)
)
env = daemon.child_env()
assert env["CUA_DRIVER_PERMISSION_MODE"] == "bounded"
assert "CUA_DRIVER_DANGEROUSLY_BYPASS_APPROVALS" not in env
def test_unrestricted_daemon_env_keeps_explicit_bypass():
daemon = _EmbeddedCuaDaemon("cua-driver", "unrestricted")
env = daemon.child_env()
assert env["CUA_DRIVER_PERMISSION_MODE"] == "unrestricted"
assert env["CUA_DRIVER_DANGEROUSLY_BYPASS_APPROVALS"] == "1"
def test_bounded_daemon_serves_with_approved_manifest(tmp_path, monkeypatch):
"""The spawn command carries the manifest + launch-time approval flags."""
manifest = tmp_path / "manifest.yaml"
manifest.write_text("version: 3\n", encoding="utf-8")
daemon = _EmbeddedCuaDaemon(
"cua-driver", "bounded", capability_manifest=str(manifest)
)
captured: Dict[str, Any] = {}
class _FakeProc:
stderr = None
def poll(self):
return None
def _fake_popen(command, **kwargs):
captured["command"] = list(command)
return _FakeProc()
def _fake_run(command, **kwargs):
class _Probe:
returncode = 0
return _Probe()
monkeypatch.setattr(cb.subprocess, "Popen", _fake_popen)
monkeypatch.setattr(cb.subprocess, "run", _fake_run)
monkeypatch.setattr(
cb, "_resolve_mcp_invocation", lambda cmd: (cmd, ["mcp"])
)
daemon.start()
command = captured["command"]
assert "--permission-mode" in command
assert command[command.index("--permission-mode") + 1] == "bounded"
assert "--capability-manifest" in command
assert (
command[command.index("--capability-manifest") + 1]
== str(manifest)
)
assert "--approve-capability-manifest" in command
assert "--dangerously-bypass-approvals" not in command
def test_unrestricted_daemon_serve_command_unchanged(monkeypatch):
daemon = _EmbeddedCuaDaemon("cua-driver", "unrestricted")
captured: Dict[str, Any] = {}
class _FakeProc:
stderr = None
def poll(self):
return None
monkeypatch.setattr(
cb.subprocess, "Popen",
lambda command, **kw: captured.update(command=list(command)) or _FakeProc(),
)
def _fake_run(command, **kwargs):
class _Probe:
returncode = 0
return _Probe()
monkeypatch.setattr(cb.subprocess, "run", _fake_run)
monkeypatch.setattr(
cb, "_resolve_mcp_invocation", lambda cmd: (cmd, ["mcp"])
)
daemon.start()
command = captured["command"]
assert "--dangerously-bypass-approvals" in command
assert "--capability-manifest" not in command
# ── standard-mode --grant existing-profile ──────────────────────────────
def test_grant_existing_profile_defaults_off(monkeypatch):
monkeypatch.setattr(cb, "_computer_use_cfg", dict)
assert cb._cua_grant_existing_profile() is False
def test_grant_existing_profile_reads_config(monkeypatch):
monkeypatch.setattr(
cb, "_computer_use_cfg", lambda: {"grant_existing_profile": True}
)
assert cb._cua_grant_existing_profile() is True
# ── permission-mode resolution ──────────────────────────────────────────
def test_configured_mode_defaults_to_standard(monkeypatch):
monkeypatch.setattr(cb, "_computer_use_cfg", dict)
assert cb._cua_configured_permission_mode() == "standard"
def test_configured_mode_honors_bounded(monkeypatch):
monkeypatch.setattr(
cb, "_computer_use_cfg", lambda: {"permission_mode": "Bounded"}
)
assert cb._cua_configured_permission_mode() == "bounded"
@pytest.mark.parametrize("value", ["unrestricted", "yolo", "off", 3, None])
def test_configured_mode_never_yields_unrestricted(monkeypatch, value):
"""A config line must never silently bypass approvals."""
monkeypatch.setattr(
cb, "_computer_use_cfg", lambda: {"permission_mode": value}
)
assert cb._cua_configured_permission_mode() == "standard"
def test_capability_manifest_reads_config(monkeypatch):
monkeypatch.setattr(
cb, "_computer_use_cfg",
lambda: {"capability_manifest": " ~/manifests/cua.yaml "},
)
assert cb._cua_capability_manifest() == "~/manifests/cua.yaml"
monkeypatch.setattr(cb, "_computer_use_cfg", dict)
assert cb._cua_capability_manifest() is None
def test_session_yolo_overrides_configured_bounded(monkeypatch):
import tools.computer_use.tool as cu_tool
monkeypatch.setattr(
cb, "_computer_use_cfg", lambda: {"permission_mode": "bounded"}
)
import tools.approval as approval
monkeypatch.setattr(
approval, "is_approval_bypass_active_for_session", lambda sid: True
)
assert cu_tool._cua_permission_mode("sess-1") == "unrestricted"
def test_no_yolo_uses_configured_bounded(monkeypatch):
import tools.computer_use.tool as cu_tool
monkeypatch.setattr(
cb, "_computer_use_cfg", lambda: {"permission_mode": "bounded"}
)
import tools.approval as approval
monkeypatch.setattr(
approval, "is_approval_bypass_active_for_session", lambda sid: False
)
monkeypatch.setattr(
approval, "get_current_session_key", lambda default="": ""
)
assert cu_tool._cua_permission_mode("sess-1") == "bounded"
def test_no_yolo_no_config_stays_standard(monkeypatch):
import tools.computer_use.tool as cu_tool
monkeypatch.setattr(cb, "_computer_use_cfg", dict)
import tools.approval as approval
monkeypatch.setattr(
approval, "is_approval_bypass_active_for_session", lambda sid: False
)
monkeypatch.setattr(
approval, "get_current_session_key", lambda default="": ""
)
assert cu_tool._cua_permission_mode("sess-1") == "standard"
def test_backend_accepts_bounded_with_manifest(tmp_path, monkeypatch):
manifest = tmp_path / "manifest.yaml"
manifest.write_text("version: 3\n", encoding="utf-8")
monkeypatch.setattr(
cb, "_cua_capability_manifest", lambda: str(manifest)
)
monkeypatch.setattr(cb, "resolve_cua_driver_cmd", lambda override=None: "cua-driver")
backend = cb.CuaDriverBackend(permission_mode="bounded")
assert backend.permission_mode == "bounded"
assert backend._embedded_daemon is not None
assert backend._embedded_daemon.capability_manifest == str(manifest)
def test_backend_bounded_without_manifest_fails_loudly(monkeypatch):
monkeypatch.setattr(cb, "_cua_capability_manifest", lambda: None)
monkeypatch.setattr(cb, "resolve_cua_driver_cmd", lambda override=None: "cua-driver")
with pytest.raises(ValueError, match="capability_manifest"):
cb.CuaDriverBackend(permission_mode="bounded")
def test_backend_rejects_unknown_mode():
with pytest.raises(ValueError, match="unsupported"):
cb.CuaDriverBackend(permission_mode="wide-open")