diff --git a/tests/tools/test_bot_desktop_browser_fence.py b/tests/tools/test_bot_desktop_browser_fence.py index d8875e11af..1d8ba3ae5d 100644 --- a/tests/tools/test_bot_desktop_browser_fence.py +++ b/tests/tools/test_bot_desktop_browser_fence.py @@ -109,3 +109,23 @@ def test_browser_console_supervisor_fast_path_is_fenced_while_human_controls_sha assert evaluated == [] and commands == [], "human holds the lease, yet the page was evaluated" assert "WHAT-THE-HUMAN-TYPED" not in raw assert result.get("code") == "human_has_control" + + +def test_vault_page_operations_respect_the_human_lease(monkeypatch): + """The vault tools reach the page over the supervisor socket, outside `_run_browser_command`: while a + human holds the screen they must be refused like every other page access, and never focus, inspect or + write to the form the human is typing into.""" + from tools import browser_tool as browser + from tools import browser_vault_tool as vault + + browser._active_sessions["default"] = {"session_name": "review", "cdp_url": None, "features": {"local": True}} + touched = [] + for name in ("browser_vault_fill", "browser_vault_enter_code", "browser_vault_save_login"): + monkeypatch.setattr(vault, name, lambda *a, **k: touched.append(name) or json.dumps({"success": True})) + lease.acquire("human") + for handler in (vault._handle_vault_fill, vault._handle_vault_enter_code, vault._handle_vault_save_login): + res = json.loads(handler({"handle": "vault_x"}, task_id="default")) + assert res["code"] == "human_has_control", handler.__name__ + assert touched == [], "no vault page access while the human holds the screen" + lease.release("human") + assert json.loads(vault._handle_vault_fill({"handle": "vault_x"}, task_id="default"))["success"] is True diff --git a/tools/browser_vault_tool.py b/tools/browser_vault_tool.py index 63d0cae598..6378ac6449 100644 --- a/tools/browser_vault_tool.py +++ b/tools/browser_vault_tool.py @@ -660,12 +660,26 @@ BROWSER_VAULT_ENTER_CODE_SCHEMA = { } +def _fenced_page_op(task_id: Optional[str], fn) -> str: + """Vault operations focus, inspect and fill the page over the supervisor socket, bypassing + ``_run_browser_command``; they must honour the Bot Desktop lease like every other page access, + or a human typing a credential on the taken-over screen could be read or written to.""" + from tools.browser_tool import _active_sessions, _last_session_key + from tools.browser_tool_session import run_fenced + + session = _active_sessions.get(_last_session_key(task_id or "default")) or {} + res = run_fenced(session, lambda: {"raw": fn()}) + return res["raw"] if "raw" in res else json.dumps(res) + + def _handle_vault_enter_code(args: Dict[str, Any], **kwargs) -> str: - return browser_vault_enter_code(handle=str(args.get("handle") or ""), task_id=kwargs.get("task_id")) + tid = kwargs.get("task_id") + return _fenced_page_op(tid, lambda: browser_vault_enter_code(handle=str(args.get("handle") or ""), task_id=tid)) def _handle_vault_save_login(args: Dict[str, Any], **kwargs) -> str: - return browser_vault_save_login(label=str(args.get("label") or ""), task_id=kwargs.get("task_id")) + tid = kwargs.get("task_id") + return _fenced_page_op(tid, lambda: browser_vault_save_login(label=str(args.get("label") or ""), task_id=tid)) def _handle_vault_list(args: Dict[str, Any], **kwargs) -> str: @@ -677,9 +691,8 @@ def _handle_vault_unlock(args: Dict[str, Any], **kwargs) -> str: def _handle_vault_fill(args: Dict[str, Any], **kwargs) -> str: - return browser_vault_fill( - handle=str(args.get("handle") or ""), task_id=kwargs.get("task_id") - ) + tid = kwargs.get("task_id") + return _fenced_page_op(tid, lambda: browser_vault_fill(handle=str(args.get("handle") or ""), task_id=tid)) from tools.registry import no_cache_check_fn, registry # noqa: E402