- 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>
A previous bug (fixed in fcd2e8b) caused str(AgentKey(...)) to be saved
as the key spec instead of the plain string "agent". This made
_load_private_key() return None, sending messages unsigned.
- _is_valid_key_spec() guards save_remote_key() against persisting
serialized objects or other non-spec values
- key_for_remote() rejects already-persisted corrupt specs so fallback
key loading still works
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
browser-cli.auth.keys and browser-cli.auth.trust are handled by serve.py
directly and never need a _route profile, so they no longer trigger
_auto_route_remote (which would open a second connection just to discover
available browser profiles).
Also fixes _auto_route_remote receiving an already-loaded AgentKey object
instead of the key spec string — the nested send_command call couldn't
re-load it for signing, causing auth failures.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- remote_browser_targets(), _auto_route_remote(), active_browser_targets()
now accept and forward the key parameter so pubkey auth works during
the initial browser-cli.targets discovery call
- _multi_browser_targets() in tabs/groups/windows/session commands now
reads key from ctx.obj and passes it through
- send_command() auto-saves the key spec (e.g. "agent") to remotes.json
on first explicit use; subsequent calls to the same remote reuse it
without requiring --key every time
- Added save_remote_key() / key_for_remote() helpers (mirrors token helpers)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Security:
- serve.py: server now sends nonce challenge before accepting any command;
clients sign nonce + SHA256(canonical_payload) with Ed25519 key
- New --authorized-keys FILE option for serve; token auth still works as fallback
- Connection limit: BoundedSemaphore(64) in serve.py
- Secure file creation with os.open(..., 0o600) for token/key files
- New auth.py module: keygen, file key load/save, SSH agent protocol (pure Python),
sign/verify helpers compatible with both file keys and agent-held keys (YubiKey,
TPM, gpg-agent)
Features:
- YubiKey support via SSH agent protocol — no new runtime deps, just $SSH_AUTH_SOCK
- New `browser-cli auth` command group: keygen, trust, show, keys
- Global --key PATH flag (or BROWSER_CLI_KEY env) selects signing key;
pass "agent" or "agent:<selector>" to use SSH agent key
- BrowserCLI Python API gains key= parameter
Bug fixes (11 issues across two review passes):
- client.py: check response is not None before json.loads
- native_host.py: _read_exact_stream loop handles EINTR short reads; fix Windows
Listener leak on accept error
- __init__.py: open_wait / tabs_watch_url raise RuntimeError instead of silent None
- extension/tabs.ts: dedupe skips tabs without URL; tabsSort uses pendingUrl fallback
- extension/session.ts: removeListener before addListener prevents duplicate handlers
Breaking: TCP serve protocol now sends a challenge frame first (v0.9.0)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Exposes a local browser over a TCP socket so remote machines can
control it using the same CLI and Python API. Token auth (auto-generated
via secrets.token_urlsafe) is on by default; --no-auth disables it.
Profile routing via _route message field lets clients target specific
browser instances on the remote host. BROWSER_CLI_PROFILE is forwarded
automatically so --browser flag works transparently over remote.
- browser-cli serve [--host] [--port] [--token] [--no-auth]
- browser-cli --remote HOST:PORT --token TOKEN <command>
- BrowserCLI(remote="host:port", token="...").tabs_list()