From ae40fca95523b2daf7d8c3245dd27ea28059a5cb Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 4 May 2026 04:44:00 -0700 Subject: [PATCH] fix(profiles): keep validate_profile_name strict; callers normalize first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to @changchun989's cherry-pick: reverts the validate-via- normalize change so validate_profile_name remains a strict regex check on the input AS-GIVEN. Callers that accept mixed-case user input (dashboard UI, CLI args, import flows) call normalize_profile_name() first, then validate the result. This keeps validate honest about what the on-disk directory name must look like — e.g. ' jules ' (trailing whitespace) is now rejected instead of silently trimmed and accepted. - validate_profile_name: strict lowercase/regex check again, 'UPPER' back in the invalid-names parametrize - 8 call sites in profiles.py (create_profile, delete_profile, set_active_profile, export_profile, import_profile, rename_profile, resolve_profile_env, plus the clone_from branch): swap the normalize-then-validate order - scripts/release.py: add changchun989@proton.me -> changchun989 to AUTHOR_MAP so CI doesn't block on the unmapped contributor email All kanban + profile tests pass (268 across test_profiles.py + test_kanban_db.py + test_kanban_core_functionality.py, plus 73 in test_kanban_tools.py + test_kanban_dashboard_plugin.py). Closes #18498. --- hermes_cli/profiles.py | 33 +++++++++++++++++++------------ scripts/release.py | 1 + tests/hermes_cli/test_profiles.py | 8 +++++--- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/hermes_cli/profiles.py b/hermes_cli/profiles.py index a6fb276189..10cd36b88c 100644 --- a/hermes_cli/profiles.py +++ b/hermes_cli/profiles.py @@ -198,13 +198,19 @@ def normalize_profile_name(name: str) -> str: def validate_profile_name(name: str) -> None: - """Raise ``ValueError`` if *name* is not a valid profile identifier.""" - canon = normalize_profile_name(name) - if canon == "default": + """Raise ``ValueError`` if *name* is not a valid profile identifier. + + Validates the input as-given — strict lowercase match. Callers that accept + mixed-case or title-cased input from users (dashboard UI, CLI args) should + call :func:`normalize_profile_name` first. This separation keeps validate + honest about what the on-disk directory name must look like, while + ingress-point normalization handles UX flexibility (see #18498). + """ + if name == "default": return # special alias for ~/.hermes - if not _PROFILE_ID_RE.match(canon): + if not _PROFILE_ID_RE.match(name): raise ValueError( - f"Invalid profile name {canon!r}. Must match " + f"Invalid profile name {name!r}. Must match " f"[a-z0-9][a-z0-9_-]{{0,63}}" ) @@ -444,8 +450,8 @@ def create_profile( Path The newly created profile directory. """ - validate_profile_name(name) canon = normalize_profile_name(name) + validate_profile_name(canon) if canon == "default": raise ValueError( @@ -464,6 +470,7 @@ def create_profile( from hermes_constants import get_hermes_home source_dir = get_hermes_home() else: + clone_from = normalize_profile_name(clone_from) validate_profile_name(clone_from) source_dir = get_profile_dir(clone_from) if not source_dir.is_dir(): @@ -564,8 +571,8 @@ def delete_profile(name: str, yes: bool = False) -> Path: Returns the path that was removed. """ - validate_profile_name(name) canon = normalize_profile_name(name) + validate_profile_name(canon) if canon == "default": raise ValueError( @@ -755,8 +762,8 @@ def set_active_profile(name: str) -> None: Writes to ``~/.hermes/active_profile``. Use ``"default"`` to clear. """ - validate_profile_name(name) canon = normalize_profile_name(name) + validate_profile_name(canon) if canon != "default" and not profile_exists(canon): raise FileNotFoundError( f"Profile '{canon}' does not exist. " @@ -837,8 +844,8 @@ def export_profile(name: str, output_path: str) -> Path: """ import tempfile - validate_profile_name(name) canon = normalize_profile_name(name) + validate_profile_name(canon) profile_dir = get_profile_dir(canon) if not profile_dir.is_dir(): raise FileNotFoundError(f"Profile '{canon}' does not exist.") @@ -979,8 +986,8 @@ def import_profile(archive_path: str, name: Optional[str] = None) -> Path: # Archives exported from the default profile have "default/" as top-level # dir. Importing as "default" would target ~/.hermes itself — disallow # that and guide the user toward a named profile. - validate_profile_name(inferred_name) canon = normalize_profile_name(inferred_name) + validate_profile_name(canon) if canon == "default": raise ValueError( "Cannot import as 'default' — that is the built-in root profile (~/.hermes). " @@ -1076,10 +1083,10 @@ def rename_profile(old_name: str, new_name: str) -> Path: Returns the new profile directory. """ - validate_profile_name(old_name) - validate_profile_name(new_name) old_canon = normalize_profile_name(old_name) new_canon = normalize_profile_name(new_name) + validate_profile_name(old_canon) + validate_profile_name(new_canon) if old_canon == "default": raise ValueError("Cannot rename the default profile.") @@ -1221,8 +1228,8 @@ def resolve_profile_env(profile_name: str) -> str: Called early in the CLI entry point, before any hermes modules are imported, to set the HERMES_HOME environment variable. """ - validate_profile_name(profile_name) canon = normalize_profile_name(profile_name) + validate_profile_name(canon) profile_dir = get_profile_dir(canon) if canon != "default" and not profile_dir.is_dir(): diff --git a/scripts/release.py b/scripts/release.py index 4794f5bbfd..cfafa36e2a 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -97,6 +97,7 @@ AUTHOR_MAP = { "252818347@qq.com": "hejuntt1014", "uzmpsk.dilekakbas@gmail.com": "dlkakbs", "beliefanx@gmail.com": "BeliefanX", + "changchun989@proton.me": "changchun989", "jefferson@heimdallstrategy.com": "Mind-Dragon", "44753291+Nanako0129@users.noreply.github.com": "Nanako0129", "steve.westerhouse@origami-analytics.com": "westers", diff --git a/tests/hermes_cli/test_profiles.py b/tests/hermes_cli/test_profiles.py index 9dd783c2ef..7ddb8fd20a 100644 --- a/tests/hermes_cli/test_profiles.py +++ b/tests/hermes_cli/test_profiles.py @@ -85,10 +85,12 @@ class TestValidateProfileName: # Should not raise validate_profile_name(name) - def test_uppercase_accepted_via_normalization(self): - validate_profile_name("Jules") + def test_uppercase_rejected(self): + # validate_profile_name is strict — callers normalize first, then validate. + with pytest.raises(ValueError): + validate_profile_name("Jules") - @pytest.mark.parametrize("name", ["has space", ".hidden", "-leading"]) + @pytest.mark.parametrize("name", ["UPPER", "has space", ".hidden", "-leading"]) def test_invalid_names_rejected(self, name): with pytest.raises(ValueError): validate_profile_name(name)