diff --git a/hermes_cli/auth_commands.py b/hermes_cli/auth_commands.py index 998f72b3e61..8852eb63ef1 100644 --- a/hermes_cli/auth_commands.py +++ b/hermes_cli/auth_commands.py @@ -339,6 +339,7 @@ def auth_add_command(args) -> None: creds = auth_mod._xai_oauth_loopback_login( timeout_seconds=getattr(args, "timeout", None) or 20.0, open_browser=not getattr(args, "no_browser", False), + manual_paste=bool(getattr(args, "manual_paste", False)), ) label = (getattr(args, "label", None) or "").strip() or label_from_token( creds["tokens"]["access_token"], diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 2f5e0933cc3..3168c4818fc 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -2022,7 +2022,7 @@ def select_provider_and_model(args=None): elif selected_provider == "openai-codex": _model_flow_openai_codex(config, current_model) elif selected_provider == "xai-oauth": - _model_flow_xai_oauth(config, current_model) + _model_flow_xai_oauth(config, current_model, args=args) elif selected_provider == "qwen-oauth": _model_flow_qwen_oauth(config, current_model) elif selected_provider == "minimax-oauth": @@ -2903,7 +2903,7 @@ def _model_flow_openai_codex(config, current_model=""): print("No change.") -def _model_flow_xai_oauth(_config, current_model=""): +def _model_flow_xai_oauth(_config, current_model="", *, args=None): """xAI Grok OAuth (SuperGrok Subscription) provider: ensure logged in, then pick model.""" from hermes_cli.auth import ( get_xai_oauth_auth_status, @@ -2934,7 +2934,15 @@ def _model_flow_xai_oauth(_config, current_model=""): print("Starting a fresh xAI OAuth login...") print() try: - mock_args = argparse.Namespace() + # Forward CLI flags from ``hermes model --manual-paste`` + # / ``--no-browser`` / ``--timeout`` into the loopback + # login. Without this, browser-only remotes (#26923) + # can't reach the manual-paste path via ``hermes model``. + mock_args = argparse.Namespace( + manual_paste=bool(getattr(args, "manual_paste", False)), + no_browser=bool(getattr(args, "no_browser", False)), + timeout=getattr(args, "timeout", None), + ) _login_xai_oauth( mock_args, PROVIDER_REGISTRY["xai-oauth"], @@ -2952,7 +2960,11 @@ def _model_flow_xai_oauth(_config, current_model=""): print("Not logged into xAI Grok OAuth (SuperGrok Subscription). Starting login...") print() try: - mock_args = argparse.Namespace() + mock_args = argparse.Namespace( + manual_paste=bool(getattr(args, "manual_paste", False)), + no_browser=bool(getattr(args, "no_browser", False)), + timeout=getattr(args, "timeout", None), + ) _login_xai_oauth(mock_args, PROVIDER_REGISTRY["xai-oauth"]) except SystemExit: print("Login cancelled or failed.") @@ -10041,6 +10053,16 @@ def main(): action="store_true", help="Do not attempt to open the browser automatically during Nous login", ) + model_parser.add_argument( + "--manual-paste", + action="store_true", + help=( + "For loopback OAuth providers (xai-oauth, ...): skip the local " + "callback listener and paste the failed callback URL from your " + "browser instead. Use on browser-only remotes (Cloud Shell, " + "Codespaces, EC2 Instance Connect, ...). See #26923." + ), + ) model_parser.add_argument( "--timeout", type=float, @@ -10503,6 +10525,17 @@ def main(): action="store_true", help="Do not auto-open a browser for OAuth login", ) + auth_add.add_argument( + "--manual-paste", + action="store_true", + help=( + "Skip the loopback callback listener and paste the failed " + "callback URL from your browser instead. Use this on " + "browser-only remotes (GCP Cloud Shell, GitHub Codespaces, " + "EC2 Instance Connect, ...) where 127.0.0.1 on the remote " + "isn't reachable from your laptop. See #26923." + ), + ) auth_add.add_argument( "--timeout", type=float, help="OAuth/network timeout in seconds" )