diff --git a/hermes_cli/config.py b/hermes_cli/config.py index ad3cd23b53..ca94092879 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 5c719cbc21..456439b574 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"