From ae6820a45a60416b75c06dcaf7fdfe3d96e6aaed Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 12 Apr 2026 01:51:57 -0700 Subject: [PATCH] fix(setup): validate base URL input in hermes model flow (#8264) Reject non-URL values (e.g. shell commands typed by mistake) in the base URL prompt during provider setup. Previously any string was saved as-is to .env, breaking connectivity when the garbage value was used as the API endpoint. Adds http:// / https:// prefix check with a clear error message. The custom-endpoint flow already had this validation (line 1620); this brings the generic API-key provider flow to parity. Triggered by a user support case where 'nano ~/.hermes/.env' was accidentally entered as GLM_BASE_URL during Z.AI setup. --- hermes_cli/main.py | 7 +- .../test_model_provider_persistence.py | 73 +++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 2e580bea84..babcad7191 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -2499,8 +2499,11 @@ def _model_flow_api_key_provider(config, provider_id, current_model=""): print() override = "" if override and base_url_env: - save_env_value(base_url_env, override) - effective_base = override + if not override.startswith(("http://", "https://")): + print(" Invalid URL — must start with http:// or https://. Keeping current value.") + else: + save_env_value(base_url_env, override) + effective_base = override # Model selection — resolution order: # 1. models.dev registry (cached, filtered for agentic/tool-capable models) diff --git a/tests/hermes_cli/test_model_provider_persistence.py b/tests/hermes_cli/test_model_provider_persistence.py index 55f7ac69c7..a06facd300 100644 --- a/tests/hermes_cli/test_model_provider_persistence.py +++ b/tests/hermes_cli/test_model_provider_persistence.py @@ -257,3 +257,76 @@ class TestProviderPersistsAfterModelSave: assert model.get("provider") == "opencode-go" assert model.get("default") == "minimax-m2.5" assert model.get("api_mode") == "anthropic_messages" + + +class TestBaseUrlValidation: + """Reject non-URL values in the base URL prompt (e.g. shell commands).""" + + def test_invalid_base_url_rejected(self, config_home, monkeypatch, capsys): + """Typing a non-URL string should not be saved as the base URL.""" + from hermes_cli.auth import PROVIDER_REGISTRY + + pconfig = PROVIDER_REGISTRY.get("zai") + if not pconfig: + pytest.skip("zai not in PROVIDER_REGISTRY") + + monkeypatch.setenv("GLM_API_KEY", "test-key") + + from hermes_cli.main import _model_flow_api_key_provider + from hermes_cli.config import load_config, get_env_value + + # User types a shell command instead of a URL at the base URL prompt + with patch("hermes_cli.auth._prompt_model_selection", return_value="glm-5"), \ + patch("hermes_cli.auth.deactivate_provider"), \ + patch("builtins.input", return_value="nano ~/.hermes/.env"): + _model_flow_api_key_provider(load_config(), "zai", "old-model") + + # The garbage value should NOT have been saved + saved = get_env_value("GLM_BASE_URL") or "" + assert not saved or saved.startswith(("http://", "https://")), \ + f"Non-URL value was saved as GLM_BASE_URL: {saved}" + captured = capsys.readouterr() + assert "Invalid URL" in captured.out + + def test_valid_base_url_accepted(self, config_home, monkeypatch): + """A proper URL should be saved normally.""" + from hermes_cli.auth import PROVIDER_REGISTRY + + pconfig = PROVIDER_REGISTRY.get("zai") + if not pconfig: + pytest.skip("zai not in PROVIDER_REGISTRY") + + monkeypatch.setenv("GLM_API_KEY", "test-key") + + from hermes_cli.main import _model_flow_api_key_provider + from hermes_cli.config import load_config, get_env_value + + with patch("hermes_cli.auth._prompt_model_selection", return_value="glm-5"), \ + patch("hermes_cli.auth.deactivate_provider"), \ + patch("builtins.input", return_value="https://custom.z.ai/api/paas/v4"): + _model_flow_api_key_provider(load_config(), "zai", "old-model") + + saved = get_env_value("GLM_BASE_URL") or "" + assert saved == "https://custom.z.ai/api/paas/v4" + + def test_empty_base_url_keeps_default(self, config_home, monkeypatch): + """Pressing Enter (empty) should not change the base URL.""" + from hermes_cli.auth import PROVIDER_REGISTRY + + pconfig = PROVIDER_REGISTRY.get("zai") + if not pconfig: + pytest.skip("zai not in PROVIDER_REGISTRY") + + monkeypatch.setenv("GLM_API_KEY", "test-key") + monkeypatch.delenv("GLM_BASE_URL", raising=False) + + from hermes_cli.main import _model_flow_api_key_provider + from hermes_cli.config import load_config, get_env_value + + with patch("hermes_cli.auth._prompt_model_selection", return_value="glm-5"), \ + patch("hermes_cli.auth.deactivate_provider"), \ + patch("builtins.input", return_value=""): + _model_flow_api_key_provider(load_config(), "zai", "old-model") + + saved = get_env_value("GLM_BASE_URL") or "" + assert saved == "", "Empty input should not save a base URL"