Files
hermes-agent/hermes_cli/cli_browser_gateway.py
Teknium 433e420be4 refactor(browser): drop /browser use, the browser.manage use action and the downgrade notice
browser_exec is the only core browser path, so there is no Browser Use /
built-in tools switch to flip and no silent downgrade to announce.

- classic CLI, Ink TUI and the gateway browser.manage RPC lose the `use`
  action (contract regenerated: BrowserAction, enabled, browser_use);
- /browser status no longer reports a mode; the Chromium-fallback line for
  Lightpanda and the dead "chrome" engine row go with the built-in tools;
- browser_backend_notice leaves the session snapshot, the CLI banner and
  the canonical contract.

TUI locale packs and _keys.tui.json drop the five slashCmd.ops.browser
keys whose only caller was `/browser use`.
2026-10-23 01:41:24 -07:00

75 lines
3.6 KiB
Python

"""Classic CLI ``/browser`` on the shared gateway (#134889 CLI-9): ``browser.manage``, as Ink and
Desktop send it. Split from ``cli_commands_mixin`` (the file is over its size target)."""
from __future__ import annotations
import os
from urllib.parse import urlparse
from hermes_cli.browser_connect import DEFAULT_BROWSER_CDP_URL
from hermes_cli.cli_commands_session_tools import _say_block, _t
def _gateway_is_remote() -> bool:
"""True when this CLI attaches to an explicitly named gateway WebSocket that is not loopback
(``HERMES_TUI_GATEWAY_URL``): Desktop's ``mode === 'remote'``."""
raw = os.environ.get("HERMES_TUI_GATEWAY_URL", "").strip()
if not raw:
return False
host = (urlparse(raw).hostname or "").lower()
return host not in {"127.0.0.1", "localhost", "::1"}
def gateway_browser(cli, connection, word: str, rest: str) -> None:
"""``/browser`` on the shared gateway (#134889 CLI-9): ``browser.manage``, the verb Ink and
Desktop send. The browser tools run in the gateway, so changing this process's
``BROWSER_CDP_URL`` would change nothing the agent uses. ``connect``/``disconnect`` manage a
browser on the gateway HOST: only against a local gateway (Desktop's rule). The gateway scopes
them to this session's profile."""
from hermes_cli.gateway_client import GatewayClientError
if word in {"connect", "disconnect"} and _gateway_is_remote():
return _say_block(_t("browser.remote_gateway"))
params = {"action": word, "session_id": cli.session_id}
if word == "connect":
normalized = _normalize_cdp_url(rest or DEFAULT_BROWSER_CDP_URL)
if normalized is None:
return
params["url"] = normalized[0]
print(f" {_t('browser.checking', url=params['url'])}")
try:
result = connection.call("browser.manage", **params) or {}
except GatewayClientError as exc:
return _say_block(f" ⚠ {exc}")
for message in result.get("messages") or ():
print(f" {message}")
if word == "disconnect":
return _say_block(_t("browser.disconnected"), f" {_t('browser.reverted')}")
if word == "connect":
if not result.get("connected"):
return _say_block(_t("browser.not_connected"))
return _say_block(_t("browser.connected"), f" {_t('browser.endpoint', url=result.get('url') or '')}")
if result.get("connected"):
return _say_block(_t("browser.status_connected"), f" {_t('browser.endpoint', url=result.get('url') or '')}")
_say_block(_t("browser.status_gateway_default"),
f" {_t('browser.status_connect_hint')}", f" {_t('browser.status_disconnect_hint')}")
def _normalize_cdp_url(cdp_url: str):
"""Validate a /browser connect URL: ``(cdp_url, port)`` or None after printing the error.
A ``/devtools/browser/<id>`` path is kept verbatim; anything else is reduced to the origin."""
parsed = urlparse(cdp_url if "://" in cdp_url else f"http://{cdp_url}")
if parsed.scheme not in {"http", "https", "ws", "wss"}:
_say_block(f" {_t('browser.unsupported_scheme', scheme=parsed.scheme or _t('browser.scheme_missing'))}")
return None
try:
port = parsed.port or (443 if parsed.scheme in {"https", "wss"} else 80)
except ValueError:
_say_block(f" {_t('browser.invalid_port', url=cdp_url)}")
return None
if not parsed.hostname:
_say_block(f" {_t('browser.missing_host', url=cdp_url)}")
return None
if parsed.path.startswith("/devtools/browser/"):
return parsed.geturl(), port
return parsed._replace(path="", params="", query="", fragment="").geturl(), port