From dcbc15252c8d1e82869b85fbd3f73098530531cc Mon Sep 17 00:00:00 2001 From: SayHell0W0rld <852938468@qq.com> Date: Wed, 19 Aug 2026 10:51:35 +0800 Subject: [PATCH] fix(profiles): reject non-string profile names instead of coercing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A numeric profile id (DB row id or falsy sentinel) handed to normalize_profile_name() was silently str()'d into a real profile name: `0` → `profiles/0/` bootstrap with no config.yaml (phantom profile created on desktop first launch, #88842). normalize_profile_name now raises ValueError for non-string input; callers holding a numeric id must resolve it to an actual profile name first. A literal "0" typed by a user remains a legal explicit name. --- hermes_cli/profiles.py | 15 +++++++++++++-- tests/hermes_cli/test_profiles.py | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/hermes_cli/profiles.py b/hermes_cli/profiles.py index c21b5319dd..8ee6d18850 100644 --- a/hermes_cli/profiles.py +++ b/hermes_cli/profiles.py @@ -294,10 +294,21 @@ def normalize_profile_name(name: str) -> str: case-insensitively. Dashboards/tools may pass title-cased labels — normalize before validation, assignment, and subprocess spawn. - Named profiles are stored lowercase under ``profiles//``. See #18498. + Named profiles are stored lowercase under ``profiles//``. The special + alias ``default`` is matched case-insensitively (``Default`` → ``default``). + Dashboards and tools may pass title-cased display labels; normalize before + validation, assignment, and subprocess spawn (see issue #18498). + + Raises ``ValueError`` for non-string input: a numeric profile id (e.g. a + DB row id or a falsy sentinel) silently coerced via ``str()`` becomes a + real on-disk profile directory (``profiles/0/``, #88842). Callers that + hold a numeric id must resolve it to an actual profile name first. """ if not isinstance(name, str): - name = str(name) + raise ValueError( + "profile name must be a string, got " + f"{type(name).__name__}: {name!r}" + ) stripped = name.strip() if not stripped: raise ValueError("profile name cannot be empty") diff --git a/tests/hermes_cli/test_profiles.py b/tests/hermes_cli/test_profiles.py index 035dbd8311..95281c20cc 100644 --- a/tests/hermes_cli/test_profiles.py +++ b/tests/hermes_cli/test_profiles.py @@ -75,6 +75,21 @@ class TestNormalizeProfileName: assert normalize_profile_name("Jules") == "jules" assert normalize_profile_name(" Librarian ") == "librarian" + def test_non_string_name_rejected(self): + # A numeric profile id (DB row id / falsy sentinel) must not be + # silently coerced into a real on-disk profile directory (#88842). + with pytest.raises(ValueError): + normalize_profile_name(0) + with pytest.raises(ValueError): + normalize_profile_name(None) + with pytest.raises(ValueError): + normalize_profile_name(42) + + def test_literal_zero_string_is_a_valid_explicit_name(self): + # A literal "0" typed by the user is a legal profile id; only the + # non-string coercion created the phantom profile. + assert normalize_profile_name("0") == "0" + class TestValidateProfileName: """Tests for validate_profile_name()."""