From 88e07c42b44c6dcf62a56fbc891bd22ee3fab253 Mon Sep 17 00:00:00 2001 From: JackJin <1037461232@qq.com> Date: Wed, 29 Apr 2026 04:39:51 +0800 Subject: [PATCH] fix(cli): prevent .env sanitizer from splitting GLM_API_KEY by LM_API_KEY suffix The known-key splitter in `_sanitize_env_lines` used substring matching to find concatenated KEY=VALUE pairs. When a registered key was a suffix of another (LM_API_KEY is a suffix of GLM_API_KEY), the shorter key's needle would match inside the longer one, causing the sanitizer to rewrite `GLM_API_KEY=...` as `G\nLM_API_KEY=...` and silently break Z.AI/GLM auth (and similarly `GLM_BASE_URL` -> `G\nLM_BASE_URL`). Drop matches whose needle range is fully contained within a longer overlapping match. Two regression tests cover the suffix-collision case and confirm a real concatenation that happens to start with the longer key still splits where it should. Fixes #17138 --- hermes_cli/config.py | 19 ++++++++++++++----- tests/hermes_cli/test_config.py | 17 +++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/hermes_cli/config.py b/hermes_cli/config.py index ad3cd23b..ca940928 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -3710,18 +3710,27 @@ def _sanitize_env_lines(lines: list) -> list: # Detect concatenated KEY=VALUE pairs on one line. # Search for known KEY= patterns at any position in the line. - split_positions = [] + # We collect full needle ranges so we can drop matches that are + # fully contained within a longer overlapping needle. Without this, + # suffix collisions corrupt the file: e.g. LM_API_KEY= inside + # GLM_API_KEY= would otherwise split the line into "G\nLM_API_KEY=...". + match_ranges: list[tuple[int, int]] = [] for key_name in known_keys: needle = key_name + "=" idx = stripped.find(needle) while idx >= 0: - split_positions.append(idx) + match_ranges.append((idx, idx + len(needle))) idx = stripped.find(needle, idx + len(needle)) + split_positions = sorted({ + s for s, e in match_ranges + if not any( + s2 <= s and e2 >= e and (s2, e2) != (s, e) + for s2, e2 in match_ranges + ) + }) + if len(split_positions) > 1: - split_positions.sort() - # Deduplicate (shouldn't happen, but be safe) - split_positions = sorted(set(split_positions)) for i, pos in enumerate(split_positions): end = split_positions[i + 1] if i + 1 < len(split_positions) else len(stripped) part = stripped[pos:end].strip() diff --git a/tests/hermes_cli/test_config.py b/tests/hermes_cli/test_config.py index 5c719cbc..456439b5 100644 --- a/tests/hermes_cli/test_config.py +++ b/tests/hermes_cli/test_config.py @@ -319,6 +319,23 @@ class TestSanitizeEnvLines: assert result[0].startswith("OPENROUTER_API_KEY=") assert result[1].startswith("OPENAI_BASE_URL=") + def test_glm_suffix_collision_not_split(self): + """GLM_API_KEY / GLM_BASE_URL must not be mangled by LM_API_KEY / LM_BASE_URL suffixes (#17138).""" + lines = [ + "GLM_API_KEY=glm-secret\n", + "GLM_BASE_URL=https://api.z.ai/api/paas/v4\n", + ] + result = _sanitize_env_lines(lines) + assert result == lines, f"GLM_* lines were corrupted by suffix collision: {result}" + + def test_suffix_collision_does_not_break_real_concatenation(self): + """A genuine concatenation that happens to start with a suffix-superset key still splits.""" + lines = ["GLM_API_KEY=glmLM_API_KEY=lm-key\n"] + result = _sanitize_env_lines(lines) + assert len(result) == 2 + assert result[0].startswith("GLM_API_KEY=") + assert result[1].startswith("LM_API_KEY=") + def test_save_env_value_fixes_corruption_on_write(self, tmp_path): """save_env_value sanitizes corrupted lines when writing a new key.""" env_file = tmp_path / ".env"