From 26a57467a8bcf7f5ea162484c14c5698ef5d3f60 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Thu, 4 Jun 2026 02:33:33 +0530 Subject: [PATCH] fix(cli): harden `hermes portal` SystemExit handling + finish model-pick doc sweep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-review of #38465 surfaced three real items: 1. SystemExit escape (defense): `_login_nous` raises SystemExit(130)/(1) on cancel/failure. The logged-out login path inside `_model_flow_nous` catches it, but the expired-session re-login path (main.py) only catches Exception, so a Ctrl-C during re-auth could propagate past `_run_portal_one_shot` and kill the CLI. Add SystemExit to the portal handler so all cancel/abort cases end with the graceful 'Setup cancelled / retry later' message. 2. Doc sweep: the model-pick step was only added to the bare-`hermes portal` prose. Propagate it to the surfaces describing `hermes setup --portal` behavior that still omitted model selection: - `--portal` argparse help (main.py) - nous-portal.md intro + the numbered 'what it does' step list (EN + zh-Hans) - run-hermes-with-nous-portal.md 'default model after setup --portal' line, which was now contradictory (there's a picker, not a forced default) (EN + zh) 3. Test coverage: add parametrized regression test asserting the portal handler swallows KeyboardInterrupt / EOFError / SystemExit (returns None, no escape). Note on 'Skip (keep current)': delegating to _model_flow_nous means picking Skip preserves the prior provider instead of force-switching to nous — this is intentional and matches quick setup exactly; docs now say 'sets Nous as your provider (when you pick a model)' rather than unconditionally. --- hermes_cli/main.py | 6 +++--- hermes_cli/setup.py | 7 ++++++- tests/hermes_cli/test_portal_cli.py | 20 +++++++++++++++++++ .../guides/run-hermes-with-nous-portal.md | 2 +- website/docs/integrations/nous-portal.md | 9 +++++---- .../guides/run-hermes-with-nous-portal.md | 2 +- .../current/integrations/nous-portal.md | 9 +++++---- 7 files changed, 41 insertions(+), 14 deletions(-) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index ae83d7546..57dbaaf7c 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -12681,9 +12681,9 @@ def main(): setup_parser.add_argument( "--portal", action="store_true", - help="One-shot Nous Portal setup: log in via OAuth, set Nous as the " - "inference provider, and opt into the Tool Gateway. Skips the " - "rest of the wizard.", + help="One-shot Nous Portal setup: log in via OAuth, pick a Nous " + "model, set Nous as the inference provider, and opt into the Tool " + "Gateway. Skips the rest of the wizard.", ) setup_parser.set_defaults(func=cmd_setup) diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index 43e2009c3..8930d543a 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -2770,7 +2770,12 @@ def _run_portal_one_shot(config: dict) -> None: from hermes_cli.main import _model_flow_nous _model_flow_nous(config) - except (KeyboardInterrupt, EOFError): + except (KeyboardInterrupt, EOFError, SystemExit): + # _login_nous raises SystemExit(130)/(1) on cancel/failure; the + # logged-out path inside _model_flow_nous catches it, but the + # expired-session re-login path only catches Exception, so a + # SystemExit there would otherwise escape and kill the whole CLI. + # Treat all of these as a graceful cancel/abort for the portal flow. print() print_info(" Setup cancelled.") print_info(" You can retry later with `hermes portal`.") diff --git a/tests/hermes_cli/test_portal_cli.py b/tests/hermes_cli/test_portal_cli.py index a4c50f062..927661ef5 100644 --- a/tests/hermes_cli/test_portal_cli.py +++ b/tests/hermes_cli/test_portal_cli.py @@ -135,3 +135,23 @@ def test_one_shot_delegates_to_model_flow_nous(monkeypatch): "`hermes portal` must route through _model_flow_nous so the model " "picker runs every time (matching quick setup)." ) + + +@pytest.mark.parametrize("exc", [KeyboardInterrupt, EOFError, SystemExit]) +def test_one_shot_swallows_cancel_and_systemexit(monkeypatch, exc): + """A cancel/abort from the delegated Nous flow must NOT escape and kill the + CLI. `_login_nous` raises SystemExit(130)/(1) on cancel/failure, and the + expired-session re-login path inside `_model_flow_nous` only catches + Exception — so SystemExit could otherwise propagate out. The portal handler + must treat KeyboardInterrupt/EOFError/SystemExit as a graceful cancel. + """ + import hermes_cli.setup as setup_mod + + def boom(config): + raise exc + + monkeypatch.setattr("hermes_cli.main._model_flow_nous", boom) + monkeypatch.setattr("hermes_cli.config.load_config", lambda: {}) + + # Must return normally (None), not propagate the exception. + assert setup_mod._run_portal_one_shot({}) is None diff --git a/website/docs/guides/run-hermes-with-nous-portal.md b/website/docs/guides/run-hermes-with-nous-portal.md index 77518ff8b..6850193a1 100644 --- a/website/docs/guides/run-hermes-with-nous-portal.md +++ b/website/docs/guides/run-hermes-with-nous-portal.md @@ -95,7 +95,7 @@ You should see Hermes call `web_search` (Firecrawl-backed, through the gateway) ## 5. Pick the model you actually want -The default after `hermes setup --portal` is a sensible general-purpose model, but the whole point of the subscription is access to the full catalog. Switch with `/model` mid-session: +`hermes setup --portal` lets you pick a model during setup, but the whole point of the subscription is access to the full catalog — switch any time with `/model` mid-session: ```bash /model anthropic/claude-sonnet-4.6 # best general-purpose agentic diff --git a/website/docs/integrations/nous-portal.md b/website/docs/integrations/nous-portal.md index c6a8d2733..cd1ceff06 100644 --- a/website/docs/integrations/nous-portal.md +++ b/website/docs/integrations/nous-portal.md @@ -14,7 +14,7 @@ If you only have time to set up one thing, set up this. The fastest path: hermes setup --portal ``` -That single command runs the Portal OAuth, sets Nous as your inference provider in `config.yaml`, and turns on the Tool Gateway. You're ready to `hermes chat` immediately after. +That single command runs the Portal OAuth, lets you pick a Nous model, sets Nous as your inference provider in `config.yaml`, and turns on the Tool Gateway. You're ready to `hermes chat` immediately after. Don't have a subscription yet? [portal.nousresearch.com/manage-subscription](https://portal.nousresearch.com/manage-subscription) — sign up, then come back and run the command above. @@ -99,9 +99,10 @@ This runs the full setup in one shot: 1. Opens your browser to portal.nousresearch.com for OAuth login 2. Stores the refresh token at `~/.hermes/auth.json` -3. Sets Nous as your inference provider in `~/.hermes/config.yaml` -4. Turns on the Tool Gateway (web, image, TTS, browser routing) -5. Returns you to your terminal ready to `hermes chat` +3. Lets you pick a Nous model from the curated list (or skip to keep your current one) +4. Sets Nous as your inference provider in `~/.hermes/config.yaml` (when you pick a model) +5. Turns on the Tool Gateway (web, image, TTS, browser routing) +6. Returns you to your terminal ready to `hermes chat` If you don't have a subscription yet, sign up at [portal.nousresearch.com/manage-subscription](https://portal.nousresearch.com/manage-subscription) first. diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/guides/run-hermes-with-nous-portal.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/guides/run-hermes-with-nous-portal.md index fefe97df9..41dc86b4b 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/guides/run-hermes-with-nous-portal.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/guides/run-hermes-with-nous-portal.md @@ -95,7 +95,7 @@ Hey, search the web for "Hermes Agent release notes" and summarize the top 3 hit ## 5. 选择你实际需要的模型 -`hermes setup --portal` 后的默认模型是一个合理的通用模型,但订阅的意义在于可以访问完整的模型目录。在会话中使用 `/model` 切换: +`hermes setup --portal` 会在设置过程中让你选择模型,但订阅的意义在于可以访问完整的模型目录——随时可在会话中使用 `/model` 切换: ```bash /model anthropic/claude-sonnet-4.6 # 最佳通用 agentic 模型 diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/integrations/nous-portal.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/integrations/nous-portal.md index 21465c5e0..8e66915a0 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/integrations/nous-portal.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/integrations/nous-portal.md @@ -14,7 +14,7 @@ description: "一个订阅,300+ 前沿模型,Tool Gateway,以及 Nous Chat hermes setup --portal ``` -这条命令会完成 Portal OAuth 认证,在 `config.yaml` 中将 Nous 设为推理提供商,并开启 Tool Gateway。完成后即可立即运行 `hermes chat`。 +这条命令会完成 Portal OAuth 认证,让你选择一个 Nous 模型,在 `config.yaml` 中将 Nous 设为推理提供商,并开启 Tool Gateway。完成后即可立即运行 `hermes chat`。 还没有订阅?前往 [portal.nousresearch.com/manage-subscription](https://portal.nousresearch.com/manage-subscription) 注册,然后回来运行上面的命令。 @@ -95,9 +95,10 @@ hermes setup --portal 1. 打开浏览器跳转至 portal.nousresearch.com 进行 OAuth 登录 2. 将 refresh token 存储至 `~/.hermes/auth.json` -3. 在 `~/.hermes/config.yaml` 中将 Nous 设为推理提供商 -4. 开启 Tool Gateway(网页、图像、TTS、浏览器路由) -5. 返回终端,即可运行 `hermes chat` +3. 让你从精选列表中选择一个 Nous 模型(也可跳过以保留当前模型) +4. 在 `~/.hermes/config.yaml` 中将 Nous 设为推理提供商(当你选择模型时) +5. 开启 Tool Gateway(网页、图像、TTS、浏览器路由) +6. 返回终端,即可运行 `hermes chat` 如果还没有订阅,请先在 [portal.nousresearch.com/manage-subscription](https://portal.nousresearch.com/manage-subscription) 注册。