"""Host Claude auth helpers. Reads the host's Claude Code auth state and returns only the session key needed by egress. Does not expose refresh tokens or raw auth payloads. """ from __future__ import annotations import json import os from datetime import datetime, timezone from pathlib import Path from ...log import die def claude_auth_path(host_env: dict[str, str] | None = None) -> Path: env = os.environ if host_env is None else host_env home = env.get("HOME") if home: return Path(home) / ".claude.json" return Path.home() / ".claude.json" def claude_host_access_token( host_env: dict[str, str] | None = None, *, now: datetime | None = None, ) -> str: path = claude_auth_path(host_env) if not path.is_file(): die( f"claude host credentials: auth file missing at {path}. " "Run `claude login` on the host or disable " "agent_provider.forward_host_credentials." ) try: raw = json.loads(path.read_text()) except (OSError, json.JSONDecodeError) as e: die(f"claude host credentials: could not read valid JSON at {path}: {e}") if not isinstance(raw, dict): die(f"claude host credentials: {path} must contain a JSON object") oauth = raw.get("oauthAccount") if not isinstance(oauth, dict): die( f"claude host credentials: {path} is missing oauthAccount. " "Run `claude login` on the host or disable " "agent_provider.forward_host_credentials." ) session_key = oauth.get("sessionKey") if not isinstance(session_key, str) or not session_key: die( f"claude host credentials: {path} oauthAccount.sessionKey is missing " "or empty. Run `claude login` on the host and restart the bottle." ) expires_at = oauth.get("expiresAt") if isinstance(expires_at, (int, float)): check_now = now or datetime.now(timezone.utc) exp_dt = datetime.fromtimestamp(float(expires_at), timezone.utc) if exp_dt <= check_now: die( "claude host credentials: host Claude session token is expired. " "Run `claude login` on the host and restart the bottle." ) return session_key __all__ = [ "claude_auth_path", "claude_host_access_token", ]