refactor: split compat into package, harden serve proxy (v0.9.3)
Testing / test (push) Failing after 10m21s

- compat.py → compat/ package: auth.py (auth-field normalizers),
  commands.py (command-format shims), __init__.py (re-exports)
- Add _auth_0_9_3 transformer: normalizes pubkey to lowercase before auth
  so clients < 0.9.3 sending uppercase hex are accepted
- adapt_auth() now called before auth check in serve.py; command extracted
  after adapt_auth so future transformers can rename commands safely
- serve.py: deduplicate _recv_exact (import from client), unify
  resp/resp_payload across Windows/Unix branches, require lowercase hex
  pubkey (re.fullmatch), reorganize imports, drop unused os import
- client.py: move payload/framed construction inside branches (remote path
  no longer serializes JSON it never uses); fix _is_valid_key_spec
  operator precedence; import MAX_MSG_BYTES from version_manager
- auth.py: narrow except clause (ValueError instead of bare Exception)
- Bump version 0.9.2 → 0.9.3

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-03 10:12:55 +02:00
parent c1a5ef9dd7
commit 0d5c49c19a
26 changed files with 630 additions and 352 deletions
+5 -7
View File
@@ -19,6 +19,7 @@ from pathlib import Path
from typing import Any
from browser_cli.platform import endpoint_for_alias, is_windows, registry_path
from browser_cli.version_manager import MAX_MSG_BYTES as _MAX_MSG_BYTES
from browser_cli.registry import load_registry
try:
@@ -72,7 +73,7 @@ def _load_remotes() -> dict[str, dict[str, str]]:
def _is_valid_key_spec(s: str) -> bool:
"""Return True if s looks like a usable key spec: 'agent', 'agent:<sel>', or a file path."""
return s == "agent" or s.startswith("agent:") or (not s.startswith("<") and "/" in s or Path(s).suffix in {".pem", ".key"})
return s == "agent" or s.startswith("agent:") or (not s.startswith("<") and ("/" in s or Path(s).suffix in {".pem", ".key"}))
def save_remote_key(endpoint: str, key_spec: str) -> None:
@@ -327,22 +328,22 @@ def send_command(command: str, args: dict | None = None, profile: str | None = N
msg["_route"] = route_profile
else:
private_key = None
payload = json.dumps(msg).encode("utf-8")
framed = struct.pack("<I", len(payload)) + payload
try:
if remote_endpoint:
response = _send_remote(remote_endpoint, msg, private_key)
elif is_windows():
payload = json.dumps(msg).encode("utf-8")
sock_path = _resolve_socket(profile)
with PipeClient(sock_path, family="AF_PIPE") as conn:
conn.send_bytes(payload)
response = conn.recv_bytes()
else:
payload = json.dumps(msg).encode("utf-8")
sock_path = _resolve_socket(profile)
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
sock.connect(sock_path)
sock.sendall(framed)
sock.sendall(struct.pack("<I", len(payload)) + payload)
response = _recv_all(sock)
except (FileNotFoundError, ConnectionRefusedError, OSError):
if remote_endpoint:
@@ -368,9 +369,6 @@ 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("<I", raw_len)[0]