From cafbc9a734e3f71fc97c0cdedf3a3b59879e5f1f Mon Sep 17 00:00:00 2001 From: xxxigm Date: Sat, 16 May 2026 19:47:22 +0700 Subject: [PATCH] feat(cli): wire --manual-paste into ``hermes auth add`` and ``hermes model`` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Register the new ``--manual-paste`` flag on both entry points and thread it through to the xAI loopback login: * ``hermes auth add xai-oauth --manual-paste`` — pool-add path, forwarded inside ``auth_commands.handle_auth_add``. * ``hermes model --manual-paste`` — model-picker path, forwarded by ``_model_flow_xai_oauth`` into the synthetic ``argparse.Namespace`` it passes to ``_login_xai_oauth``. The picker also now forwards ``--no-browser`` and ``--timeout`` for consistency (previously hardcoded to defaults regardless of CLI flags). Help text on both flags points at #26923 and names the browser-only remote consoles (Cloud Shell, Codespaces, EC2 Instance Connect) so users searching ``hermes --help`` can find the workaround. --- hermes_cli/auth_commands.py | 1 + hermes_cli/main.py | 41 +++++++++++++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 4 deletions(-) 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" )