diff --git a/browser_cli/__init__.py b/browser_cli/__init__.py index 06c0d96..832d8fb 100644 --- a/browser_cli/__init__.py +++ b/browser_cli/__init__.py @@ -18,7 +18,6 @@ Usage: """ from collections.abc import Callable, Iterable from dataclasses import dataclass -from pathlib import Path from browser_cli.client import BrowserNotConnected, active_browser_targets, remote_browser_targets, send_command from browser_cli.models import Group, Tab @@ -34,7 +33,7 @@ class BrowserCounts: class BrowserCLI: - def __init__(self, browser: str | None = None, remote: str | None = None, token: str | None = None, key: str | None = None): + def __init__(self, browser: str | None = None, remote: str | None = None, key: str | None = None): """ Args: browser: Profile alias to target. Required when multiple browser @@ -43,24 +42,22 @@ class BrowserCLI: Format: ``"host:port"`` (e.g. ``"192.168.1.10:8765"``). Can be combined with ``browser`` to route to a specific remote profile. - token: Auth token for the remote serve instance (legacy token auth). - key: Path to Ed25519 private key PEM for pubkey auth. When set, - overrides token auth. Defaults to ``~/.config/browser-cli/client.key.pem`` - if that file exists. + key: Path to Ed25519 private key PEM for pubkey auth, or ``"agent"`` + to use a key from the SSH agent (YubiKey, gpg-agent, etc.). + Defaults to ``~/.config/browser-cli/client.key.pem`` if that file exists. """ self._browser = browser self._remote = remote - self._token = token - self._key = Path(key) if key else None + self._key = key if key else None def _cmd(self, command: str, args: dict | None = None): - return send_command(command, args, profile=self._browser, remote=self._remote, token=self._token, key=self._key) + return send_command(command, args, profile=self._browser, remote=self._remote, key=self._key) def _multi_browser_targets(self): if self._browser is not None: return [] if self._remote: - targets = remote_browser_targets(self._remote, self._token) + targets = remote_browser_targets(self._remote, key=self._key) else: targets = active_browser_targets() if len(targets) <= 1 and not any(target.remote for target in targets): @@ -73,7 +70,7 @@ class BrowserCLI: for target in targets: try: if target.remote: - data = send_command(command, args, profile=target.profile, remote=target.remote, token=target.token, key=self._key) + data = send_command(command, args, profile=target.profile, remote=target.remote, key=self._key) else: data = send_command(command, args, profile=target.profile) except (BrowserNotConnected, RuntimeError): @@ -98,7 +95,6 @@ class BrowserCLI: browser_profile: str | None = None, browser_name: str | None = None, browser_remote: str | None = None, - browser_token: str | None = None, ) -> Tab: tab = Tab( id=data["id"], @@ -113,7 +109,6 @@ class BrowserCLI: tab._browser = self if browser_profile is None else BrowserCLI( browser=browser_profile, remote=browser_remote, - token=browser_token, ) return tab @@ -124,7 +119,6 @@ class BrowserCLI: browser_profile: str | None = None, browser_name: str | None = None, browser_remote: str | None = None, - browser_token: str | None = None, ) -> Group: group = Group( id=data["id"], @@ -137,7 +131,6 @@ class BrowserCLI: group._browser = self if browser_profile is None else BrowserCLI( browser=browser_profile, remote=browser_remote, - token=browser_token, ) return group @@ -237,7 +230,6 @@ class BrowserCLI: browser_profile=target.profile, browser_name=target.display_name, browser_remote=target.remote, - browser_token=target.token, ) for target, tabs in multi_results for tab in (tabs or []) @@ -392,7 +384,6 @@ class BrowserCLI: browser_profile=target.profile, browser_name=target.display_name, browser_remote=target.remote, - browser_token=target.token, ) for target, groups in multi_results for group in (groups or []) diff --git a/browser_cli/cli.py b/browser_cli/cli.py index 99b779a..17b524a 100755 --- a/browser_cli/cli.py +++ b/browser_cli/cli.py @@ -30,7 +30,6 @@ from browser_cli.client import ( REGISTRY_PATH, active_browser_targets, display_browser_name, - save_remote_token, remote_target_for_alias, remote_browser_targets, ) @@ -191,16 +190,12 @@ def _print_version(ctx, param, value): "--remote", default=None, metavar="HOST:PORT", help="Connect to a remote browser exposed via 'browser-cli serve'.", ) -@click.option( - "--token", default=None, metavar="TOKEN", - help="Auth token for the remote browser-cli serve instance.", -) @click.option( "--key", default=None, metavar="PATH", help="Ed25519 private key PEM for pubkey auth with a remote serve instance.", ) @click.pass_context -def main(ctx, browser, remote, token, key): +def main(ctx, browser, remote, key): """Control your running browser from the terminal via a Chrome extension.""" ctx.ensure_object(dict) ctx.obj["browser"] = browser @@ -209,13 +204,10 @@ def main(ctx, browser, remote, token, key): os.environ["BROWSER_CLI_PROFILE"] = browser ctx.call_on_close(lambda: os.environ.pop("BROWSER_CLI_PROFILE", None)) ctx.obj["remote"] = remote - ctx.obj["token"] = token ctx.obj["key"] = key if remote: os.environ["BROWSER_CLI_REMOTE"] = remote ctx.call_on_close(lambda: os.environ.pop("BROWSER_CLI_REMOTE", None)) - if token: - save_remote_token(remote, token) if key: os.environ["BROWSER_CLI_KEY"] = key ctx.call_on_close(lambda: os.environ.pop("BROWSER_CLI_KEY", None)) @@ -399,7 +391,6 @@ def clients_group(ctx): browser_alias = (ctx.obj or {}).get("browser") remote = (ctx.obj or {}).get("remote") or os.environ.get("BROWSER_CLI_REMOTE") - token = (ctx.obj or {}).get("token") or os.environ.get("BROWSER_CLI_TOKEN") key = (ctx.obj or {}).get("key") if not remote and browser_alias: @@ -407,15 +398,14 @@ def clients_group(ctx): # then show ALL clients from that remote (not just the one resolved profile). resolved = remote_target_for_alias(browser_alias) if resolved: - resolved_token = token or resolved.token try: - targets = remote_browser_targets(resolved.remote, resolved_token) + targets = remote_browser_targets(resolved.remote) except (BrowserNotConnected, RuntimeError) as e: console.print(f"[red]Error:[/red] {e}") sys.exit(1) for target in targets: try: - result = send_command("clients.list", profile=target.profile, remote=resolved.remote, token=resolved_token, key=key) + result = send_command("clients.list", profile=target.profile, remote=resolved.remote, key=key) for c in (result or []): c["profile"] = target.display_name all_clients.append(c) @@ -423,7 +413,7 @@ def clients_group(ctx): continue elif remote: try: - result = send_command("clients.list", profile=browser_alias, remote=remote, token=token, key=key) + result = send_command("clients.list", profile=browser_alias, remote=remote, key=key) for c in (result or []): c["profile"] = c.get("profile") or browser_alias or "remote" all_clients.append(c) @@ -455,7 +445,7 @@ def clients_group(ctx): if target.remote is None: continue try: - result = send_command("clients.list", profile=target.profile, remote=target.remote, token=target.token) + result = send_command("clients.list", profile=target.profile, remote=target.remote) for c in (result or []): c["profile"] = target.display_name all_clients.append(c) diff --git a/browser_cli/client.py b/browser_cli/client.py index 634c259..6014cbf 100644 --- a/browser_cli/client.py +++ b/browser_cli/client.py @@ -21,6 +21,12 @@ from typing import Any from browser_cli.platform import endpoint_for_alias, is_windows, registry_path from browser_cli.registry import load_registry +try: + from importlib.metadata import version as _pkg_version + _USER_AGENT = f"browser-cli/{_pkg_version('browser-cli')}" +except Exception: + _USER_AGENT = "browser-cli/0" + REGISTRY_PATH = registry_path() REMOTE_REGISTRY_PATH = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "browser-cli" / "remotes.json" _DEFAULT_KEY_PATH = Path(os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))) / "browser-cli" / "client.key.pem" @@ -36,7 +42,6 @@ class BrowserTarget: display_name: str socket_path: str remote: str | None = None - token: str | None = None def _active_endpoints(reg: dict) -> dict: @@ -64,27 +69,6 @@ def _load_remotes() -> dict[str, dict[str, str]]: return {str(endpoint): cfg for endpoint, cfg in data.items() if isinstance(cfg, dict)} -def save_remote_token(endpoint: str, token: str | None) -> None: - """Persist the auth token for a remote endpoint used by this client.""" - if not endpoint or not token: - return - remotes = _load_remotes() - current = remotes.get(endpoint, {}) - current["token"] = token - remotes[endpoint] = current - REMOTE_REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True) - fd = os.open(str(REMOTE_REGISTRY_PATH), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) - with os.fdopen(fd, "w", encoding="utf-8") as f: - f.write(json.dumps(remotes, indent=2, sort_keys=True)) - - -def token_for_remote(endpoint: str | None) -> str | None: - if not endpoint: - return None - cfg = _load_remotes().get(endpoint) or {} - token = cfg.get("token") - return str(token) if token else None - def _is_valid_key_spec(s: str) -> bool: """Return True if s looks like a usable key spec: 'agent', 'agent:', or a file path.""" @@ -127,9 +111,9 @@ def _remote_display_name(endpoint: str, profile_name: str, display_name: str) -> return f"{remote_name}:{display_name or profile_name}" -def remote_browser_targets(endpoint: str, token: str | None = None, key=None) -> list[BrowserTarget]: +def remote_browser_targets(endpoint: str, key=None) -> list[BrowserTarget]: """Return browser targets advertised by a single remote endpoint.""" - remote_targets = send_command("browser-cli.targets", remote=endpoint, token=token, key=key) + remote_targets = send_command("browser-cli.targets", remote=endpoint, key=key) targets: list[BrowserTarget] = [] for item in remote_targets or []: profile = str(item.get("profile") or "default") @@ -140,7 +124,6 @@ def remote_browser_targets(endpoint: str, token: str | None = None, key=None) -> display_name=_remote_display_name(endpoint, profile, display), socket_path="", remote=endpoint, - token=token, ) ) return targets @@ -148,10 +131,9 @@ def remote_browser_targets(endpoint: str, token: str | None = None, key=None) -> def _remote_browser_targets(key=None) -> list[BrowserTarget]: targets: list[BrowserTarget] = [] - for endpoint, cfg in _load_remotes().items(): - token = str(cfg.get("token") or "") or None + for endpoint in _load_remotes(): try: - targets.extend(remote_browser_targets(endpoint, token, key=key)) + targets.extend(remote_browser_targets(endpoint, key=key)) except (BrowserNotConnected, RuntimeError): continue return targets @@ -273,6 +255,19 @@ def _send_remote(endpoint: str, msg: dict, private_key=None) -> bytes | None: except (json.JSONDecodeError, AttributeError): nonce_hex = None + min_ver = challenge.get("min_client_version") if isinstance(challenge, dict) else None + if min_ver: + from browser_cli.version_manager import parse_version + try: + client_ver = _USER_AGENT.split("/", 1)[1] + if parse_version(client_ver) < parse_version(min_ver): + raise BrowserNotConnected( + f"Client version {client_ver} is too old for this server " + f"(requires >= {min_ver}). Run: pip install --upgrade browser-cli" + ) + except (IndexError, ValueError): + pass + if nonce_hex and private_key is not None: from browser_cli.auth import sign, public_key_hex nonce = bytes.fromhex(nonce_hex) @@ -286,8 +281,8 @@ def _send_remote(endpoint: str, msg: dict, private_key=None) -> bytes | None: return _recv_all(sock) -def _auto_route_remote(endpoint: str, token: str | None, key=None) -> str | None: - targets = remote_browser_targets(endpoint, token, key=key) +def _auto_route_remote(endpoint: str, key=None) -> str | None: + targets = remote_browser_targets(endpoint, key=key) if len(targets) == 1: return targets[0].profile if len(targets) > 1: @@ -300,7 +295,7 @@ def _auto_route_remote(endpoint: str, token: str | None, key=None) -> str | None return None -def send_command(command: str, args: dict | None = None, profile: str | None = None, remote: str | None = None, token: str | None = None, key: "Path | None" = None) -> Any: +def send_command(command: str, args: dict | None = None, profile: str | None = None, remote: str | None = None, key: "Path | None" = None) -> Any: """Send a command to the browser and return the response data.""" requested_profile = profile or os.environ.get("BROWSER_CLI_PROFILE") remote_endpoint = remote or os.environ.get("BROWSER_CLI_REMOTE") @@ -311,26 +306,23 @@ def send_command(command: str, args: dict | None = None, profile: str | None = N remote_endpoint = remote_alias_target.remote requested_profile = remote_alias_target.profile - resolved_token = token or os.environ.get("BROWSER_CLI_TOKEN") or (remote_alias_target.token if remote_alias_target else None) or token_for_remote(remote_endpoint) msg = { "id": str(uuid.uuid4()), "command": command, "args": args or {}, } if remote_endpoint: + msg["user_agent"] = _USER_AGENT # key priority: explicit flag > saved per-remote config > BROWSER_CLI_KEY env > default file key_spec = key if key is not None else key_for_remote(remote_endpoint) private_key = _load_private_key(key_spec) # persist explicit key spec so future calls don't need --key if key is not None: save_remote_key(remote_endpoint, str(key)) - # use token auth only when no Ed25519 key is available - if private_key is None and resolved_token: - msg["token"] = resolved_token route_profile = requested_profile _no_route_commands = {"browser-cli.targets", "browser-cli.auth.keys", "browser-cli.auth.trust"} if not route_profile and command not in _no_route_commands: - route_profile = _auto_route_remote(remote_endpoint, resolved_token, key=key_spec) + route_profile = _auto_route_remote(remote_endpoint, key=key_spec) if route_profile: msg["_route"] = route_profile else: @@ -376,9 +368,14 @@ def send_command(command: str, args: dict | None = None, profile: str | None = N return result.get("data") +_MAX_MSG_BYTES = 32 * 1024 * 1024 + + def _recv_all(sock: socket.socket) -> bytes: raw_len = _recv_exact(sock, 4) msg_len = struct.unpack(" _MAX_MSG_BYTES: + raise ConnectionError(f"Response too large ({msg_len} bytes)") return _recv_exact(sock, msg_len) diff --git a/browser_cli/commands/groups.py b/browser_cli/commands/groups.py index 465628f..c0b1b42 100644 --- a/browser_cli/commands/groups.py +++ b/browser_cli/commands/groups.py @@ -17,10 +17,10 @@ def _handle(command, args=None, profile=None): raise SystemExit(1) -def _handle_multi(command, args=None, profile=None, remote=None, token=None): +def _handle_multi(command, args=None, profile=None, remote=None): try: if remote: - return send_command(command, args or {}, profile=profile, remote=remote, token=token) + return send_command(command, args or {}, profile=profile, remote=remote) return send_command(command, args or {}, profile=profile) except (BrowserNotConnected, RuntimeError): return None @@ -33,7 +33,7 @@ def _multi_browser_targets(): remote = root.obj.get("remote") key = root.obj.get("key") if remote: - targets = remote_browser_targets(remote, root.obj.get("token"), key=key) + targets = remote_browser_targets(remote, key=key) else: targets = active_browser_targets(key=key) if len(targets) <= 1 and not any(target.remote for target in targets): @@ -78,7 +78,7 @@ def group_list(): if targets: groups = [] for target in targets: - result = _handle_multi("group.list", profile=target.profile, remote=target.remote, token=target.token) + result = _handle_multi("group.list", profile=target.profile, remote=target.remote) if result is None: continue groups.extend({**group, "browser": target.display_name} for group in result) @@ -111,7 +111,7 @@ def group_count(): total = 0 rows = 0 for target in targets: - count = _handle_multi("group.count", profile=target.profile, remote=target.remote, token=target.token) + count = _handle_multi("group.count", profile=target.profile, remote=target.remote) if count is None: continue count = int(count or 0) diff --git a/browser_cli/commands/serve.py b/browser_cli/commands/serve.py index 4a55e94..d471c33 100644 --- a/browser_cli/commands/serve.py +++ b/browser_cli/commands/serve.py @@ -1,7 +1,12 @@ -import hmac, threading, secrets, socket, struct, click, json, sys +import re, threading, secrets, socket, struct, click, json, sys, os from pathlib import Path +from browser_cli.version_manager import PROTOCOL_MIN_CLIENT, parse_version, get_installed_version +from browser_cli.compat import adapt_request, adapt_response + +_UA_PATTERN = re.compile(r"^browser-cli/\d") _CONN_LIMIT = threading.BoundedSemaphore(64) +_MAX_MSG_BYTES = 32 * 1024 * 1024 from rich.console import Console from datetime import datetime @@ -25,17 +30,10 @@ def _log(addr:tuple, command:str, profile:str|None, status:str, error:str|None=N else: console.print(f"[dim]{ts}[/dim] {addr_str} {profile_str}[cyan]{command}[/cyan] [green]{status}[/green]") -def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, server_token:str|None, auth_keys:list[str]|None, auth_keys_path:"Path|None", nonce:str) -> None: +def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, auth_keys:list[str]|None, auth_keys_path:"Path|None", nonce:str) -> None: from browser_cli.client import _resolve_socket, BrowserNotConnected from browser_cli.platform import is_windows - try: - header = _recv_exact(client_sock, 4) - msg_len = struct.unpack(" None: err = json.dumps({"id": msg_id, "success": False, "error": msg}).encode() try: @@ -43,6 +41,16 @@ def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, serv except OSError: pass + try: + header = _recv_exact(client_sock, 4) + msg_len = struct.unpack(" _MAX_MSG_BYTES: + _send_error(None, f"message too large ({msg_len} bytes)") + return + payload = _recv_exact(client_sock, msg_len) + except (ConnectionError, OSError): + return + try: msg = json.loads(payload) except (json.JSONDecodeError, ValueError): @@ -53,6 +61,22 @@ def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, serv msg_id = msg.get("id") command = msg.get("command", "?") + # ── user-agent + version check ──────────────────────────────────────────── + ua = msg.get("user_agent") or "" + if not _UA_PATTERN.match(ua): + _send_error(msg_id, "forbidden: client required") + _log(addr, command, None, "DENIED", f"bad user-agent: {ua!r}") + return + client_ver = "0" + try: + client_ver = ua.split("/", 1)[1] + if parse_version(client_ver) < parse_version(PROTOCOL_MIN_CLIENT): + _send_error(msg_id, f"client version {client_ver} is too old; please upgrade to >= {PROTOCOL_MIN_CLIENT}") + _log(addr, command, None, "DENIED", f"client {client_ver} < min {PROTOCOL_MIN_CLIENT}") + return + except (IndexError, ValueError): + pass + # ── auth ────────────────────────────────────────────────────────────────── if auth_keys is not None: pub = msg.get("pubkey") or "" @@ -70,11 +94,6 @@ def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, serv _send_error(msg_id, "unauthorized: invalid signature") _log(addr, command, None, "DENIED", "bad signature") return - elif server_token is not None: - if not hmac.compare_digest(msg.get("token") or "", server_token): - _send_error(msg_id, "unauthorized: invalid or missing token") - _log(addr, command, None, "DENIED", "bad token") - return if command == "browser-cli.targets": from browser_cli.client import active_browser_targets @@ -120,13 +139,12 @@ def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, serv resolved_profile = msg.get("_route") or profile - strip = {"token", "_route", "pubkey", "sig"} - if strip & msg.keys(): - clean_payload = json.dumps({k: v for k, v in msg.items() if k not in strip}).encode() - clean_header = struct.pack(" None: +def _handle_client(client_sock:socket.socket, addr:tuple, profile:str|None, auth_keys_path:"Path|None") -> None: if not _CONN_LIMIT.acquire(blocking=False): client_sock.close() return @@ -174,24 +194,29 @@ def _handle_client(client_sock:socket.socket, addr:tuple, profile:str|None, serv else: auth_keys = None nonce = secrets.token_hex(32) - challenge = json.dumps({"type": "challenge", "nonce": nonce}).encode() + challenge = json.dumps({ + "type": "challenge", + "nonce": nonce, + "server_version": get_installed_version(), + "min_client_version": PROTOCOL_MIN_CLIENT, + }).encode() try: client_sock.sendall(struct.pack(" dict`` + Upgrade an incoming client message from a client older than X.Y.Z to the + current format before forwarding it to the native host. +- ``response_fn(resp: bytes, command: str) -> bytes`` + Downgrade a native-host response to the format a client older than X.Y.Z + expects before sending it back. + +Either function may be ``None`` when only one direction needs adapting. + +Entries must stay in ascending version order. ``adapt_request`` walks forward +(oldest change first); ``adapt_response`` walks backward (newest change first) +so the transformations compose correctly. + +Current baseline: 0.9.1 — no shims needed yet. +""" + +from __future__ import annotations +from typing import Callable +from browser_cli.version_manager import parse_version + +_COMPAT: list[tuple[str, Callable[[dict], dict] | None, Callable[[bytes, str], bytes] | None]] = [ + # ("1.0.0", _req_1_0_0, _resp_1_0_0), +] + + +def adapt_request(msg: dict, client_version: str) -> dict: + """Upgrade a client message to the current server format.""" + cv = parse_version(client_version) + for version, req_fn, _ in _COMPAT: + if cv < parse_version(version) and req_fn is not None: + msg = req_fn(msg) + return msg + + +def adapt_response(resp: bytes, command: str, client_version: str) -> bytes: + """Downgrade a server response to the format the client expects.""" + cv = parse_version(client_version) + for version, _, resp_fn in reversed(_COMPAT): + if cv < parse_version(version) and resp_fn is not None: + resp = resp_fn(resp, command) + return resp diff --git a/browser_cli/native_host.py b/browser_cli/native_host.py index fe1eacd..c10d486 100644 --- a/browser_cli/native_host.py +++ b/browser_cli/native_host.py @@ -156,6 +156,7 @@ def socket_server(sock_path: str, bound_sock: "socket.socket | None" = None): path.unlink() sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.bind(sock_path) + os.chmod(sock_path, 0o600) sock.listen(16) while True: @@ -319,6 +320,7 @@ def main(): path.unlink() bound_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) bound_sock.bind(sock_path) + os.chmod(sock_path, 0o600) bound_sock.listen(16) else: bound_sock = None diff --git a/browser_cli/version_manager.py b/browser_cli/version_manager.py new file mode 100644 index 0000000..e8876ce --- /dev/null +++ b/browser_cli/version_manager.py @@ -0,0 +1,17 @@ +from importlib.metadata import version as _pkg_version + +PROTOCOL_MIN_CLIENT = "0.9.0" + + +def parse_version(v: str) -> tuple[int, ...]: + try: + return tuple(int(x) for x in v.lstrip("v").split(".")) + except ValueError: + return (0,) + + +def get_installed_version() -> str: + try: + return _pkg_version("browser-cli") + except Exception: + return "0.0.0" diff --git a/extension/manifest.json b/extension/manifest.json index be6f62e..4b30aa8 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "browser-cli", - "version": "0.9.1", + "version": "0.9.2", "description": "Control your browser from the terminal via browser-cli", "permissions": [ "tabs", diff --git a/pyproject.toml b/pyproject.toml index c835ca3..34f6d9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browser-cli" -version = "0.9.1" +version = "0.9.2" description = "Control your real running browser from the terminal via a browser extension" requires-python = ">=3.10" dependencies = [ diff --git a/tests/test_api.py b/tests/test_api.py index 1a11eee..3d3315f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -65,10 +65,9 @@ class TestBrowserCLIInit: assert b._browser == "chrome" def test_remote_options_stored(self): - b = BrowserCLI(browser="work", remote="host:8765", token="secret", key=None) + b = BrowserCLI(browser="work", remote="host:8765", key=None) assert b._browser == "work" assert b._remote == "host:8765" - assert b._token == "secret" # ── Internal factories ──────────────────────────────────────────────────────── @@ -129,7 +128,7 @@ class TestNavigation: mock_send.assert_called_once_with( "navigate.open", {"url": "https://example.com", "background": False, "window": None, "group": None}, - profile=None, remote=None, token=None, key=None, + profile=None, remote=None, key=None, ) def test_open_background(self, b, mock_send): @@ -143,38 +142,38 @@ class TestNavigation: def test_reload(self, b, mock_send): b.reload(tab_id=5) - mock_send.assert_called_once_with("navigate.reload", {"tabId": 5}, profile=None, remote=None, token=None, key=None) + mock_send.assert_called_once_with("navigate.reload", {"tabId": 5}, profile=None, remote=None, key=None) def test_hard_reload(self, b, mock_send): b.hard_reload(tab_id=7) - mock_send.assert_called_once_with("navigate.hard_reload", {"tabId": 7}, profile=None, remote=None, token=None, key=None) + mock_send.assert_called_once_with("navigate.hard_reload", {"tabId": 7}, profile=None, remote=None, key=None) def test_back(self, b, mock_send): b.back(tab_id=3) - mock_send.assert_called_once_with("navigate.back", {"tabId": 3}, profile=None, remote=None, token=None, key=None) + mock_send.assert_called_once_with("navigate.back", {"tabId": 3}, profile=None, remote=None, key=None) def test_forward(self, b, mock_send): b.forward(tab_id=3) - mock_send.assert_called_once_with("navigate.forward", {"tabId": 3}, profile=None, remote=None, token=None, key=None) + mock_send.assert_called_once_with("navigate.forward", {"tabId": 3}, profile=None, remote=None, key=None) def test_focus_url(self, b, mock_send): b.focus_url("github.com") - mock_send.assert_called_once_with("navigate.focus", {"pattern": "github.com"}, profile=None, remote=None, token=None, key=None) + mock_send.assert_called_once_with("navigate.focus", {"pattern": "github.com"}, profile=None, remote=None, key=None) def test_navigate_tab(self, b, mock_send): b.navigate_tab(5, "https://example.com") mock_send.assert_called_once_with( - "navigate.to", {"tabId": 5, "url": "https://example.com"}, profile=None, remote=None, token=None, key=None + "navigate.to", {"tabId": 5, "url": "https://example.com"}, profile=None, remote=None, key=None ) def test_profile_forwarded(self, b_profile, mock_send): b_profile.reload() - mock_send.assert_called_once_with("navigate.reload", {"tabId": None}, profile="brave", remote=None, token=None, key=None) + mock_send.assert_called_once_with("navigate.reload", {"tabId": None}, profile="brave", remote=None, key=None) def test_remote_forwarded(self, mock_send): - b = BrowserCLI(browser="work", remote="host:8765", token="secret", key=None) + b = BrowserCLI(browser="work", remote="host:8765", key=None) b.reload() - mock_send.assert_called_once_with("navigate.reload", {"tabId": None}, profile="work", remote="host:8765", token="secret", key=None) + mock_send.assert_called_once_with("navigate.reload", {"tabId": None}, profile="work", remote="host:8765", key=None) # ── Search ──────────────────────────────────────────────────────────────────── @@ -207,12 +206,12 @@ class TestExtract: result = b.extract_markdown() assert result == "# Title" - mock_send.assert_called_once_with("extract.markdown", {"selector": None}, profile=None, remote=None, token=None, key=None) + mock_send.assert_called_once_with("extract.markdown", {"selector": None}, profile=None, remote=None, key=None) def test_extract_markdown_selector(self, b, mock_send): b.extract_markdown("article") - mock_send.assert_called_once_with("extract.markdown", {"selector": "article"}, profile=None, remote=None, token=None, key=None) + mock_send.assert_called_once_with("extract.markdown", {"selector": "article"}, profile=None, remote=None, key=None) # ── Tabs ────────────────────────────────────────────────────────────────────── @@ -247,7 +246,7 @@ class TestTabs: mock_send.assert_called_once_with( "tabs.close", {"tabId": 10, "inactive": False, "duplicates": False}, - profile=None, remote=None, token=None, key=None, + profile=None, remote=None, key=None, ) def test_tabs_move(self, b, mock_send): @@ -255,19 +254,19 @@ class TestTabs: mock_send.assert_called_once_with( "tabs.move", {"tabId": 10, "forward": True, "backward": False, "groupId": None, "windowId": None, "index": None}, - profile=None, remote=None, token=None, key=None, + profile=None, remote=None, key=None, ) def test_tabs_active(self, b, mock_send): b.tabs_active(10) - mock_send.assert_called_once_with("tabs.active", {"tabId": 10}, profile=None, remote=None, token=None, key=None) + mock_send.assert_called_once_with("tabs.active", {"tabId": 10}, profile=None, remote=None, key=None) def test_window_active_tab(self, b, mock_send): mock_send.return_value = TAB_DATA tab = b.window_active_tab(1) assert isinstance(tab, Tab) assert tab.id == 10 - mock_send.assert_called_once_with("tabs.active_in_window", {"windowId": 1}, profile=None, remote=None, token=None, key=None) + mock_send.assert_called_once_with("tabs.active_in_window", {"windowId": 1}, profile=None, remote=None, key=None) def test_window_active_tab_missing_raises(self, b, mock_send): mock_send.return_value = None @@ -319,17 +318,17 @@ class TestTabs: assert mock_send.call_args_list == [ call("tabs.list", {}, profile="default"), call("tabs.list", {}, profile="work"), - call("tabs.close", {"tabId": 11}, profile="work", remote=None, token=None, key=None), + call("tabs.close", {"tabId": 11}, profile="work", remote=None, key=None), ] def test_tabs_list_remote_uses_only_requested_remote_and_binds_actions(self, mock_send): - b = BrowserCLI(remote="host:8765", token="secret", key=None) + b = BrowserCLI(remote="host:8765", key=None) with patch( "browser_cli.active_browser_targets", side_effect=AssertionError("local targets should not be used for explicit remote"), ), patch( "browser_cli.remote_browser_targets", - return_value=[BrowserTarget("work", "host:work", "", remote="host:8765", token="secret")], + return_value=[BrowserTarget("work", "host:work", "", remote="host:8765")], ): mock_send.side_effect = [[TAB_DATA], None] tabs = b.tabs_list() @@ -337,8 +336,8 @@ class TestTabs: assert [tab.browser for tab in tabs] == ["host:work"] assert mock_send.call_args_list == [ - call("tabs.list", {}, profile="work", remote="host:8765", token="secret", key=None), - call("tabs.close", {"tabId": 10}, profile="work", remote="host:8765", token="secret", key=None), + call("tabs.list", {}, profile="work", remote="host:8765", key=None), + call("tabs.close", {"tabId": 10}, profile="work", remote="host:8765", key=None), ] def test_tabs_count_multi_browser_returns_browser_counts(self, b, mock_send): @@ -381,7 +380,7 @@ class TestTabs: def test_tabs_sort(self, b, mock_send): b.tabs_sort(by="title") - mock_send.assert_called_once_with("tabs.sort", {"by": "title"}, profile=None, remote=None, token=None, key=None) + mock_send.assert_called_once_with("tabs.sort", {"by": "title"}, profile=None, remote=None, key=None) def test_tabs_merge_windows(self, b, mock_send): mock_send.return_value = {"moved": 4} @@ -414,7 +413,7 @@ class TestGroups: mock_send.return_value = [TAB_DATA] tabs = b.group_tabs(42) assert isinstance(tabs[0], Tab) - mock_send.assert_called_once_with("group.tabs", {"groupId": 42}, profile=None, remote=None, token=None, key=None) + mock_send.assert_called_once_with("group.tabs", {"groupId": 42}, profile=None, remote=None, key=None) def test_group_count(self, b, mock_send): mock_send.return_value = 7 @@ -442,17 +441,17 @@ class TestGroups: assert mock_send.call_args_list == [ call("group.list", {}, profile="default"), call("group.list", {}, profile="work"), - call("group.close", {"groupId": 99}, profile="work", remote=None, token=None, key=None), + call("group.close", {"groupId": 99}, profile="work", remote=None, key=None), ] def test_group_list_remote_uses_only_requested_remote_and_binds_actions(self, mock_send): - b = BrowserCLI(remote="host:8765", token="secret", key=None) + b = BrowserCLI(remote="host:8765", key=None) with patch( "browser_cli.active_browser_targets", side_effect=AssertionError("local targets should not be used for explicit remote"), ), patch( "browser_cli.remote_browser_targets", - return_value=[BrowserTarget("work", "host:work", "", remote="host:8765", token="secret")], + return_value=[BrowserTarget("work", "host:work", "", remote="host:8765")], ): mock_send.side_effect = [[GROUP_DATA], None] groups = b.group_list() @@ -460,8 +459,8 @@ class TestGroups: assert [group.browser for group in groups] == ["host:work"] assert mock_send.call_args_list == [ - call("group.list", {}, profile="work", remote="host:8765", token="secret", key=None), - call("group.close", {"groupId": 42}, profile="work", remote="host:8765", token="secret", key=None), + call("group.list", {}, profile="work", remote="host:8765", key=None), + call("group.close", {"groupId": 42}, profile="work", remote="host:8765", key=None), ] def test_group_count_multi_browser_returns_browser_counts(self, b, mock_send): @@ -484,7 +483,7 @@ class TestGroups: def test_group_close(self, b, mock_send): b.group_close(42) - mock_send.assert_called_once_with("group.close", {"groupId": 42}, profile=None, remote=None, token=None, key=None) + mock_send.assert_called_once_with("group.close", {"groupId": 42}, profile=None, remote=None, key=None) def test_group_create_dict_response(self, b, mock_send): mock_send.return_value = GROUP_DATA @@ -504,7 +503,7 @@ class TestGroups: tab_id = b.group_add_tab(42, "https://example.com") assert tab_id == 55 mock_send.assert_called_once_with( - "group.add_tab", {"group": "42", "url": "https://example.com"}, profile=None, remote=None, token=None, key=None + "group.add_tab", {"group": "42", "url": "https://example.com"}, profile=None, remote=None, key=None ) def test_group_add_tab_non_dict_response(self, b, mock_send): @@ -514,7 +513,7 @@ class TestGroups: def test_group_move_forward(self, b, mock_send): b.group_move(42, forward=True) mock_send.assert_called_once_with( - "group.move", {"group": "42", "forward": True, "backward": False}, profile=None, remote=None, token=None, key=None + "group.move", {"group": "42", "forward": True, "backward": False}, profile=None, remote=None, key=None ) @@ -544,7 +543,7 @@ class TestWindows: result = b.windows_open() assert result == {"id": 5} - mock_send.assert_called_once_with("windows.open", {"url": None}, profile=None, remote=None, token=None, key=None) + mock_send.assert_called_once_with("windows.open", {"url": None}, profile=None, remote=None, key=None) def test_windows_open_with_url(self, b, mock_send): mock_send.return_value = {"id": 9} @@ -552,7 +551,7 @@ class TestWindows: result = b.windows_open("https://example.com") assert result == {"id": 9} - mock_send.assert_called_once_with("windows.open", {"url": "https://example.com"}, profile=None, remote=None, token=None, key=None) + mock_send.assert_called_once_with("windows.open", {"url": "https://example.com"}, profile=None, remote=None, key=None) class TestSession: @@ -562,7 +561,7 @@ class TestSession: result = b.session_list() assert result == [{"name": "saved", "tabs": 3, "savedAt": 1712707200000}] - mock_send.assert_called_once_with("session.list", {}, profile=None, remote=None, token=None, key=None) + mock_send.assert_called_once_with("session.list", {}, profile=None, remote=None, key=None) def test_session_list_multi_browser_adds_browser(self, b, mock_send): with patch( @@ -597,26 +596,26 @@ class TestTabModel: def test_close(self, tab, mock_send): tab.close() - mock_send.assert_called_once_with("tabs.close", {"tabId": 10}, profile=None, remote=None, token=None, key=None) + mock_send.assert_called_once_with("tabs.close", {"tabId": 10}, profile=None, remote=None, key=None) def test_activate(self, tab, mock_send): tab.activate() - mock_send.assert_called_once_with("tabs.active", {"tabId": 10}, profile=None, remote=None, token=None, key=None) + mock_send.assert_called_once_with("tabs.active", {"tabId": 10}, profile=None, remote=None, key=None) def test_reload(self, tab, mock_send): tab.reload() - mock_send.assert_called_once_with("navigate.reload", {"tabId": 10}, profile=None, remote=None, token=None, key=None) + mock_send.assert_called_once_with("navigate.reload", {"tabId": 10}, profile=None, remote=None, key=None) def test_hard_reload(self, tab, mock_send): tab.hard_reload() - mock_send.assert_called_once_with("navigate.hard_reload", {"tabId": 10}, profile=None, remote=None, token=None, key=None) + mock_send.assert_called_once_with("navigate.hard_reload", {"tabId": 10}, profile=None, remote=None, key=None) def test_move_forward(self, tab, mock_send): tab.move(forward=True) mock_send.assert_called_once_with( "tabs.move", {"tabId": 10, "forward": True, "backward": False, "groupId": None, "windowId": None, "index": None}, - profile=None, remote=None, token=None, key=None, + profile=None, remote=None, key=None, ) def test_move_to_group(self, tab, mock_send): @@ -626,12 +625,12 @@ class TestTabModel: def test_html(self, tab, mock_send): mock_send.return_value = "" assert tab.html() == "" - mock_send.assert_called_once_with("tabs.html", {"tabId": 10}, profile=None, remote=None, token=None, key=None) + mock_send.assert_called_once_with("tabs.html", {"tabId": 10}, profile=None, remote=None, key=None) def test_open(self, tab, mock_send): tab.open("https://new.example.com") mock_send.assert_called_once_with( - "navigate.to", {"tabId": 10, "url": "https://new.example.com"}, profile=None, remote=None, token=None, key=None + "navigate.to", {"tabId": 10, "url": "https://new.example.com"}, profile=None, remote=None, key=None ) def test_open_background_changes_same_tab(self, tab, mock_send): @@ -639,7 +638,7 @@ class TestTabModel: mock_send.assert_called_once_with( "navigate.to", {"tabId": 10, "url": "https://new.example.com"}, - profile=None, remote=None, token=None, key=None, + profile=None, remote=None, key=None, ) def test_unbound_raises(self): @@ -657,18 +656,18 @@ class TestGroupModel: def test_close(self, group, mock_send): group.close() - mock_send.assert_called_once_with("group.close", {"groupId": 42}, profile=None, remote=None, token=None, key=None) + mock_send.assert_called_once_with("group.close", {"groupId": 42}, profile=None, remote=None, key=None) def test_tabs(self, group, mock_send): mock_send.return_value = [TAB_DATA] tabs = group.tabs() assert isinstance(tabs[0], Tab) - mock_send.assert_called_once_with("group.tabs", {"groupId": 42}, profile=None, remote=None, token=None, key=None) + mock_send.assert_called_once_with("group.tabs", {"groupId": 42}, profile=None, remote=None, key=None) def test_move_forward(self, group, mock_send): group.move(forward=True) mock_send.assert_called_once_with( - "group.move", {"group": "42", "forward": True, "backward": False}, profile=None, remote=None, token=None, key=None + "group.move", {"group": "42", "forward": True, "backward": False}, profile=None, remote=None, key=None ) def test_move_backward(self, group, mock_send): diff --git a/tests/test_cli.py b/tests/test_cli.py index dedc42d..ef4cb9c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -168,19 +168,16 @@ def test_clients_reads_registry_with_trailing_garbage(tmp_path): assert "0.8.2" in result.output def test_clients_remote_uses_remote_endpoint_without_local_registry(): - def fake_send_command(command, args=None, profile=None, remote=None, token=None, key=None): + def fake_send_command(command, args=None, profile=None, remote=None, key=None): assert command == "clients.list" assert profile is None assert remote == "127.0.0.1:8765" - assert token == "test" return [{"name": "Chrome", "version": "1", "extensionVersion": "2.3.4"}] with patch.dict(os.environ, {}, clear=True), patch( "browser_cli.cli.REGISTRY_PATH", Path("/nonexistent/browser-cli-registry.json") - ), patch("browser_cli.cli.send_command", side_effect=fake_send_command) as send_command, patch( - "browser_cli.cli.save_remote_token" - ): - result = CliRunner().invoke(main, ["--remote", "127.0.0.1:8765", "--token", "test", "clients"]) + ), patch("browser_cli.cli.send_command", side_effect=fake_send_command) as send_command: + result = CliRunner().invoke(main, ["--remote", "127.0.0.1:8765", "clients"]) assert result.exit_code == 0 send_command.assert_called_once() @@ -194,7 +191,7 @@ def test_clients_remote_respects_global_browser_route(): result = CliRunner().invoke(main, ["--remote", "127.0.0.1:8765", "--browser", "work", "clients"]) assert result.exit_code == 1 - send_command.assert_called_once_with("clients.list", profile="work", remote="127.0.0.1:8765", token=None, key=None) + send_command.assert_called_once_with("clients.list", profile="work", remote="127.0.0.1:8765", key=None) def test_clients_browser_alias_resolves_to_remote(): @@ -207,15 +204,13 @@ def test_clients_browser_alias_resolves_to_remote(): display_name="192.168.188.104:automatisation", socket_path="", remote="192.168.188.104:8765", - token="tok", ) all_remote_targets = [resolved_target] - def fake_send_command(command, args=None, profile=None, remote=None, token=None, key=None): + def fake_send_command(command, args=None, profile=None, remote=None, key=None): assert command == "clients.list" assert profile == "automatisation" assert remote == "192.168.188.104:8765" - assert token == "tok" return [{"name": "Chrome", "version": "147.0.0.0", "extensionVersion": "0.8.5"}] with patch.dict(os.environ, {}, clear=True), patch( @@ -287,17 +282,17 @@ def test_tabs_list_with_remote_uses_only_remote_targets(): side_effect=AssertionError("local targets should not be used for explicit remote"), ), patch( "browser_cli.commands.tabs.remote_browser_targets", - return_value=[BrowserTarget("work", "remote-host:work", "", remote="remote-host:8765", token="secret")], + return_value=[BrowserTarget("work", "remote-host:work", "", remote="remote-host:8765")], ), patch( "browser_cli.commands.tabs.send_command", return_value=[{"id": 1, "windowId": 1, "active": True, "title": "Remote", "url": "https://example.com"}], - ) as send_command, patch("browser_cli.cli.save_remote_token"): - result = CliRunner().invoke(main, ["--remote", "remote-host:8765", "--token", "secret", "tabs", "list"]) + ) as send_command: + result = CliRunner().invoke(main, ["--remote", "remote-host:8765", "tabs", "list"]) assert result.exit_code == 0 assert "remote-host:work" in result.output assert "Remote" in result.output - send_command.assert_called_once_with("tabs.list", {}, profile="work", remote="remote-host:8765", token="secret") + send_command.assert_called_once_with("tabs.list", {}, profile="work", remote="remote-host:8765") def test_tabs_list_with_explicit_browser_does_not_show_browser_column(): @@ -634,14 +629,6 @@ def test_convert_html_to_markdown_indents_multiline_list_items(): " Local LLMs / API Modelle / Spezialmodelle" ) in markdown -def test_remote_token_is_saved_when_passed_on_cli(): - endpoint = "browser-host.example:8765" - with patch("browser_cli.cli.save_remote_token") as save_remote_token: - result = CliRunner().invoke(main, ["--remote", endpoint, "--token", "secret", "completion", "bash", "--script"]) - - assert result.exit_code == 0 - save_remote_token.assert_called_once_with(endpoint, "secret") - def test_tabs_list_multi_browser_queries_remote_target(): endpoint = "browser-host.example:8765" @@ -650,7 +637,6 @@ def test_tabs_list_multi_browser_queries_remote_target(): "browser-host.example:work", "", remote=endpoint, - token="secret", ) with patch("browser_cli.commands.tabs.active_browser_targets", return_value=[remote_target, BrowserTarget("local", "local", "/tmp/local.sock")]), patch( @@ -660,5 +646,5 @@ def test_tabs_list_multi_browser_queries_remote_target(): result = CliRunner().invoke(main, ["tabs", "list"]) assert result.exit_code == 0 - send_command.assert_any_call("tabs.list", {}, profile="work", remote=endpoint, token="secret") + send_command.assert_any_call("tabs.list", {}, profile="work", remote=endpoint) assert "browser-host.example:work" in result.output diff --git a/tests/test_client.py b/tests/test_client.py index 4b7852a..1e13873 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -10,10 +10,8 @@ from browser_cli.client import ( active_browser_targets, display_browser_name, key_for_remote, - save_remote_token, send_command, remote_target_for_alias, - token_for_remote, ) from browser_cli.platform import endpoint_for_alias @@ -94,29 +92,15 @@ def test_active_browser_targets_keeps_windows_registry_entries(monkeypatch, tmp_ assert targets[0].socket_path == r"\\.\pipe\browser-cli-work" -def test_save_remote_token_persists_per_endpoint(monkeypatch, tmp_path): - remotes_path = tmp_path / "remotes.json" - monkeypatch.setattr("browser_cli.client.REMOTE_REGISTRY_PATH", remotes_path) - - endpoint = "browser-host.example:8765" - - save_remote_token(endpoint, "secret-token") - - assert token_for_remote(endpoint) == "secret-token" - assert json.loads(remotes_path.read_text(encoding="utf-8")) == { - endpoint: {"token": "secret-token"} - } - def test_send_command_auto_routes_single_remote_target(monkeypatch): monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False) monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False) - monkeypatch.delenv("BROWSER_CLI_TOKEN", raising=False) sent = {} monkeypatch.setattr( "browser_cli.client.remote_browser_targets", - lambda endpoint, token=None, key=None: [BrowserTarget("work", "host:work", "", remote=endpoint, token=token)], + lambda endpoint, key=None: [BrowserTarget("work", "host:work", "", remote=endpoint)], ) def fake_send_remote(endpoint, msg, private_key=None): @@ -125,20 +109,19 @@ def test_send_command_auto_routes_single_remote_target(monkeypatch): monkeypatch.setattr("browser_cli.client._send_remote", fake_send_remote) - assert send_command("tabs.list", remote="host:8765", token="secret", key=None) == "ok" + assert send_command("tabs.list", remote="host:8765", key=None) == "ok" assert sent["_route"] == "work" - assert sent["token"] == "secret" + assert "token" not in sent def test_send_command_resolves_browser_alias_to_remote_target(monkeypatch): monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False) - monkeypatch.delenv("BROWSER_CLI_TOKEN", raising=False) monkeypatch.setenv("BROWSER_CLI_PROFILE", "host:work") sent = {} monkeypatch.setattr( "browser_cli.client._remote_browser_targets", - lambda: [BrowserTarget("work", "host:work", "", remote="host:8765", token="secret")], + lambda: [BrowserTarget("work", "host:work", "", remote="host:8765")], ) def fake_send_remote(endpoint, msg, private_key=None): @@ -151,13 +134,13 @@ def test_send_command_resolves_browser_alias_to_remote_target(monkeypatch): assert send_command("tabs.list") == [] assert sent["endpoint"] == "host:8765" assert sent["_route"] == "work" - assert sent["token"] == "secret" + assert "token" not in sent def test_remote_target_for_alias_accepts_full_endpoint_profile(monkeypatch): monkeypatch.setattr( "browser_cli.client._remote_browser_targets", - lambda: [BrowserTarget("work", "host:work", "", remote="host:8765", token="secret")], + lambda: [BrowserTarget("work", "host:work", "", remote="host:8765")], ) target = remote_target_for_alias("host:8765:work") @@ -172,7 +155,7 @@ def test_remote_target_for_alias_accepts_host_when_only_one_remote_target(monkey remote_endpoint = f"{remote_host}:8765" monkeypatch.setattr( "browser_cli.client._remote_browser_targets", - lambda: [BrowserTarget("work", f"{remote_host}:work", "", remote=remote_endpoint, token="secret")], + lambda: [BrowserTarget("work", f"{remote_host}:work", "", remote=remote_endpoint)], ) target = remote_target_for_alias(remote_host) @@ -186,13 +169,12 @@ def test_send_command_resolves_host_alias_to_single_remote_target(monkeypatch): remote_host = "browser-host.example" remote_endpoint = f"{remote_host}:8765" monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False) - monkeypatch.delenv("BROWSER_CLI_TOKEN", raising=False) monkeypatch.setenv("BROWSER_CLI_PROFILE", remote_host) sent = {} monkeypatch.setattr( "browser_cli.client._remote_browser_targets", - lambda: [BrowserTarget("work", f"{remote_host}:work", "", remote=remote_endpoint, token="secret")], + lambda: [BrowserTarget("work", f"{remote_host}:work", "", remote=remote_endpoint)], ) def fake_send_remote(endpoint, msg, private_key=None): @@ -205,15 +187,15 @@ def test_send_command_resolves_host_alias_to_single_remote_target(monkeypatch): assert send_command("tabs.list") == [] assert sent["endpoint"] == remote_endpoint assert sent["_route"] == "work" - assert sent["token"] == "secret" + assert "token" not in sent def test_remote_target_for_alias_keeps_host_alias_ambiguous_for_multiple_targets(monkeypatch): monkeypatch.setattr( "browser_cli.client._remote_browser_targets", lambda: [ - BrowserTarget("main", "host:main", "", remote="host:8765", token="secret"), - BrowserTarget("work", "host:work", "", remote="host:8765", token="secret"), + BrowserTarget("main", "host:main", "", remote="host:8765"), + BrowserTarget("work", "host:work", "", remote="host:8765"), ], ) @@ -224,27 +206,26 @@ def test_send_command_requires_browser_for_multiple_remote_targets(monkeypatch): monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False) monkeypatch.setattr( "browser_cli.client.remote_browser_targets", - lambda endpoint, token=None, key=None: [ - BrowserTarget("main", "host:main", "", remote=endpoint, token=token), - BrowserTarget("furry", "host:furry", "", remote=endpoint, token=token), + lambda endpoint, key=None: [ + BrowserTarget("main", "host:main", "", remote=endpoint), + BrowserTarget("furry", "host:furry", "", remote=endpoint), ], ) with pytest.raises(BrowserNotConnected, match="Multiple remote browser instances are active: main, furry"): - send_command("tabs.list", remote="host:8765", token="secret") + send_command("tabs.list", remote="host:8765") def test_active_browser_targets_includes_remote_targets(monkeypatch, tmp_path): remotes_path = tmp_path / "remotes.json" endpoint = "browser-host.example:8765" - remotes_path.write_text(json.dumps({endpoint: {"token": "secret-token"}}), encoding="utf-8") + remotes_path.write_text(json.dumps({endpoint: {}}), encoding="utf-8") monkeypatch.setattr("browser_cli.client.REGISTRY_PATH", tmp_path / "missing-registry.json") monkeypatch.setattr("browser_cli.client.REMOTE_REGISTRY_PATH", remotes_path) - def fake_send_command(command, args=None, profile=None, remote=None, token=None, key=None): + def fake_send_command(command, args=None, profile=None, remote=None, key=None): assert command == "browser-cli.targets" assert remote == endpoint - assert token == "secret-token" return [{"profile": "work", "displayName": "work"}] monkeypatch.setattr("browser_cli.client.send_command", fake_send_command) @@ -255,7 +236,6 @@ def test_active_browser_targets_includes_remote_targets(monkeypatch, tmp_path): assert targets[0].profile == "work" assert targets[0].display_name == "browser-host.example:work" assert targets[0].remote == endpoint - assert targets[0].token == "secret-token" def test_send_command_auto_saves_and_reuses_key_for_remote(monkeypatch, tmp_path): @@ -268,7 +248,6 @@ def test_send_command_auto_saves_and_reuses_key_for_remote(monkeypatch, tmp_path monkeypatch.setattr("browser_cli.client.REGISTRY_PATH", tmp_path / "missing-registry.json") monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False) monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False) - monkeypatch.delenv("BROWSER_CLI_TOKEN", raising=False) monkeypatch.delenv("BROWSER_CLI_KEY", raising=False) from pathlib import Path as _Path @@ -281,7 +260,7 @@ def test_send_command_auto_saves_and_reuses_key_for_remote(monkeypatch, tmp_path monkeypatch.setattr("browser_cli.client._load_private_key", fake_load_private_key) monkeypatch.setattr( "browser_cli.client.remote_browser_targets", - lambda endpoint, token=None, key=None: [BrowserTarget("default", "host:default", "", remote=endpoint)], + lambda endpoint, key=None: [BrowserTarget("default", "host:default", "", remote=endpoint)], ) def fake_send_remote(endpoint, msg, private_key=None): diff --git a/uv.lock b/uv.lock index 7959699..a8d6fde 100644 --- a/uv.lock +++ b/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.10" [[package]] name = "browser-cli" -version = "0.9.1" +version = "0.9.2" source = { editable = "." } dependencies = [ { name = "click" },