diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 1cdf7853b..0dde47bf7 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -349,7 +349,7 @@ DEFAULT_CONFIG = { }, # Config schema version - bump this when adding new required fields - "_config_version": 8, + "_config_version": 9, } # ============================================================================= @@ -786,34 +786,6 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A except Exception: pass # best-effort; don't block migration on sanitize failure - # ── Always: clear stale ANTHROPIC_TOKEN when better credentials exist ── - # Old setups left ANTHROPIC_TOKEN with an outdated value that shadows - # Claude Code auto-discovery (CLAUDE_CODE_OAUTH_TOKEN) or a direct - # ANTHROPIC_API_KEY. - try: - old_token = get_env_value("ANTHROPIC_TOKEN") - if old_token: - has_api_key = bool(get_env_value("ANTHROPIC_API_KEY")) - has_claude_code = False - try: - from agent.anthropic_adapter import ( - read_claude_code_credentials, - is_claude_code_token_valid, - ) - cc_creds = read_claude_code_credentials() - has_claude_code = bool( - cc_creds and is_claude_code_token_valid(cc_creds) - ) - except Exception: - pass - if has_api_key or has_claude_code: - save_env_value("ANTHROPIC_TOKEN", "") - if not quiet: - source = "ANTHROPIC_API_KEY" if has_api_key else "Claude Code credentials" - print(f" ✓ Cleared stale ANTHROPIC_TOKEN (using {source} instead)") - except Exception: - pass - # Check config version current_ver, latest_ver = check_config_version() @@ -856,6 +828,32 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A tz_display = config["timezone"] or "(server-local)" print(f" ✓ Added timezone to config.yaml: {tz_display}") + # ── Version 8 → 9: clear stale ANTHROPIC_TOKEN when better creds exist ── + if current_ver < 9: + try: + old_token = get_env_value("ANTHROPIC_TOKEN") + if old_token: + has_api_key = bool(get_env_value("ANTHROPIC_API_KEY")) + has_claude_code = False + try: + from agent.anthropic_adapter import ( + read_claude_code_credentials, + is_claude_code_token_valid, + ) + cc_creds = read_claude_code_credentials() + has_claude_code = bool( + cc_creds and is_claude_code_token_valid(cc_creds) + ) + except Exception: + pass + if has_api_key or has_claude_code: + save_env_value("ANTHROPIC_TOKEN", "") + if not quiet: + source = "ANTHROPIC_API_KEY" if has_api_key else "Claude Code credentials" + print(f" ✓ Cleared stale ANTHROPIC_TOKEN (using {source} instead)") + except Exception: + pass + if current_ver < latest_ver and not quiet: print(f"Config version: {current_ver} → {latest_ver}") diff --git a/tests/hermes_cli/test_config.py b/tests/hermes_cli/test_config.py index 94e7f6a52..4c5a547e0 100644 --- a/tests/hermes_cli/test_config.py +++ b/tests/hermes_cli/test_config.py @@ -319,8 +319,16 @@ class TestSanitizeEnvLines: class TestStaleAnthropicTokenMigration: """Test that migrate_config clears stale ANTHROPIC_TOKEN.""" + def _write_config_version(self, tmp_path, version): + """Write a config.yaml with a specific _config_version.""" + config_path = tmp_path / "config.yaml" + import yaml + config = {"_config_version": version} + config_path.write_text(yaml.safe_dump(config, sort_keys=False)) + def test_clears_stale_token_when_api_key_exists(self, tmp_path): """ANTHROPIC_TOKEN cleared when ANTHROPIC_API_KEY is also set.""" + self._write_config_version(tmp_path, 8) env_file = tmp_path / ".env" env_file.write_text( "ANTHROPIC_API_KEY=sk-ant-real-key\n" @@ -339,6 +347,7 @@ class TestStaleAnthropicTokenMigration: def test_clears_stale_token_when_claude_code_available(self, tmp_path): """ANTHROPIC_TOKEN cleared when Claude Code credentials exist.""" + self._write_config_version(tmp_path, 8) env_file = tmp_path / ".env" env_file.write_text("ANTHROPIC_TOKEN=old-stale-token\n") @@ -361,6 +370,7 @@ class TestStaleAnthropicTokenMigration: def test_preserves_token_when_no_alternative(self, tmp_path): """ANTHROPIC_TOKEN kept when no API key or Claude Code creds exist.""" + self._write_config_version(tmp_path, 8) env_file = tmp_path / ".env" env_file.write_text("ANTHROPIC_TOKEN=only-auth-method\n") @@ -377,3 +387,21 @@ class TestStaleAnthropicTokenMigration: env = load_env() assert env.get("ANTHROPIC_TOKEN") == "only-auth-method" + + def test_skips_on_version_9_or_later(self, tmp_path): + """Migration doesn't fire when already at config version 9+.""" + self._write_config_version(tmp_path, 9) + env_file = tmp_path / ".env" + env_file.write_text( + "ANTHROPIC_API_KEY=sk-ant-real-key\n" + "ANTHROPIC_TOKEN=should-stay\n" + ) + with patch.dict(os.environ, { + "HERMES_HOME": str(tmp_path), + "ANTHROPIC_API_KEY": "sk-ant-real-key", + "ANTHROPIC_TOKEN": "should-stay", + }): + migrate_config(interactive=False, quiet=True) + + env = load_env() + assert env.get("ANTHROPIC_TOKEN") == "should-stay"