fix: propagate key through remote discovery; auto-persist key per remote

- 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>
This commit is contained in:
2026-05-02 19:50:51 +02:00
parent 4b2abbbfc5
commit 8593916e5a
6 changed files with 95 additions and 20 deletions
+37 -10
View File
@@ -86,15 +86,37 @@ def token_for_remote(endpoint: str | None) -> str | None:
return str(token) if token else None
def save_remote_key(endpoint: str, key_spec: str) -> None:
"""Persist the key spec (e.g. 'agent' or a file path) for a remote endpoint."""
if not endpoint or not key_spec:
return
remotes = _load_remotes()
current = remotes.get(endpoint, {})
current["key"] = key_spec
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 key_for_remote(endpoint: str | None) -> str | None:
if not endpoint:
return None
cfg = _load_remotes().get(endpoint) or {}
key = cfg.get("key")
return str(key) if key else None
def _remote_display_name(endpoint: str, profile_name: str, display_name: str) -> str:
host, sep, port = endpoint.rpartition(":")
remote_name = host if sep and port == "8765" else endpoint
return f"{remote_name}:{display_name or profile_name}"
def remote_browser_targets(endpoint: str, token: str | None = None) -> list[BrowserTarget]:
def remote_browser_targets(endpoint: str, token: str | None = None, key=None) -> list[BrowserTarget]:
"""Return browser targets advertised by a single remote endpoint."""
remote_targets = send_command("browser-cli.targets", remote=endpoint, token=token)
remote_targets = send_command("browser-cli.targets", remote=endpoint, token=token, key=key)
targets: list[BrowserTarget] = []
for item in remote_targets or []:
profile = str(item.get("profile") or "default")
@@ -111,12 +133,12 @@ def remote_browser_targets(endpoint: str, token: str | None = None) -> list[Brow
return targets
def _remote_browser_targets() -> list[BrowserTarget]:
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
try:
targets.extend(remote_browser_targets(endpoint, token))
targets.extend(remote_browser_targets(endpoint, token, key=key))
except (BrowserNotConnected, RuntimeError):
continue
return targets
@@ -144,7 +166,7 @@ def remote_target_for_alias(alias: str | None) -> BrowserTarget | None:
return None
def active_browser_targets(*, include_remotes: bool = True) -> list[BrowserTarget]:
def active_browser_targets(*, include_remotes: bool = True, key=None) -> list[BrowserTarget]:
targets: list[BrowserTarget] = []
if REGISTRY_PATH.exists():
reg = load_registry(REGISTRY_PATH)
@@ -153,7 +175,7 @@ def active_browser_targets(*, include_remotes: bool = True) -> list[BrowserTarge
for profile, sock_path in _active_endpoints(reg).items()
)
if include_remotes:
targets.extend(_remote_browser_targets())
targets.extend(_remote_browser_targets(key=key))
return targets
@@ -251,8 +273,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) -> str | None:
targets = remote_browser_targets(endpoint, token)
def _auto_route_remote(endpoint: str, token: str | None, key=None) -> str | None:
targets = remote_browser_targets(endpoint, token, key=key)
if len(targets) == 1:
return targets[0].profile
if len(targets) > 1:
@@ -283,13 +305,18 @@ def send_command(command: str, args: dict | None = None, profile: str | None = N
"args": args or {},
}
if remote_endpoint:
private_key = _load_private_key(key)
# 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
if not route_profile and command != "browser-cli.targets":
route_profile = _auto_route_remote(remote_endpoint, resolved_token)
route_profile = _auto_route_remote(remote_endpoint, resolved_token, key=private_key)
if route_profile:
msg["_route"] = route_profile
else: