feat: token-auth removal, security hardening, Stripe-style compat layer (v0.9.2)
Testing / test (push) Successful in 41s
Package Extension / package-extension (push) Successful in 35s
Build & Publish Package / publish (push) Successful in 46s

- Remove token auth entirely; only Ed25519 pubkey auth or --no-auth
- Add 32 MB message-size cap in serve and client (DoS protection)
- Set Unix socket to 0o600 after bind in native_host (multi-user hardening)
- Enforce browser-cli/VERSION user-agent on all TCP connections
- Add PROTOCOL_MIN_CLIENT check (>= 0.9.0) server- and client-side
- Include server_version + min_client_version in challenge frame
- Add browser_cli/version_manager.py: parse_version, get_installed_version
- Add browser_cli/compat.py: Stripe-style versioning layer with adapt_request
  / adapt_response hooks; baseline 0.9.2, no shims needed yet
- Fix BrowserCLI key handling: no Path() wrap for agent specs
- Fix _multi_browser_targets() to forward key to remote_browser_targets()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-02 21:59:46 +02:00
parent b98c4ae116
commit c1a5ef9dd7
17 changed files with 267 additions and 237 deletions
+8 -17
View File
@@ -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 [])