fix: clear stale ANTHROPIC_TOKEN during migration, remove false *** detection

- Remove *** placeholder detection from _sanitize_env_lines (was based on
  confusing terminal redaction with literal file content)
- Add migrate_config() logic to clear stale ANTHROPIC_TOKEN when better
  credentials exist (ANTHROPIC_API_KEY or Claude Code auto-discovery)
- Old ANTHROPIC_TOKEN values shadow Claude Code credential fallthrough,
  breaking auth for users who updated without re-running setup
- Preserves ANTHROPIC_TOKEN when it's the only auth method available
- 3 new migration tests, updated existing tests
This commit is contained in:
teknium1
2026-03-17 01:26:23 -07:00
parent 634c1f6752
commit b6a51c955e
2 changed files with 97 additions and 33 deletions

View File

@@ -778,13 +778,41 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A
"""
results = {"env_added": [], "config_added": [], "warnings": []}
# ── Always: sanitize .env (split concatenated keys, drop *** placeholders) ──
# ── Always: sanitize .env (split concatenated keys) ──
try:
fixes = sanitize_env_file()
if fixes and not quiet:
print(f" ✓ Repaired .env file ({fixes} corrupted entries fixed)")
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()
@@ -1167,12 +1195,6 @@ def _sanitize_env_lines(lines: list) -> list:
sanitized.append(raw + "\n")
continue
# Drop stale *** placeholder entries
if "=" in stripped:
_k, _, _v = stripped.partition("=")
if _v.strip().strip("'\"") == "***":
continue
# Detect concatenated KEY=VALUE pairs on one line.
# Search for known KEY= patterns at any position in the line.
split_positions = []

View File

@@ -12,6 +12,7 @@ from hermes_cli.config import (
ensure_hermes_home,
load_config,
load_env,
migrate_config,
save_config,
save_env_value,
save_env_value_secure,
@@ -219,25 +220,6 @@ class TestSanitizeEnvLines:
"OPENAI_BASE_URL=https://api.openai.com/v1\n",
]
def test_drops_stale_placeholder(self):
"""KEY=*** entries are removed."""
lines = [
"OPENROUTER_API_KEY=sk-or-real\n",
"ANTHROPIC_TOKEN=***\n",
"FAL_KEY=fal-real\n",
]
result = _sanitize_env_lines(lines)
assert result == [
"OPENROUTER_API_KEY=sk-or-real\n",
"FAL_KEY=fal-real\n",
]
def test_drops_quoted_placeholder(self):
"""KEY='***' and KEY=\"***\" are also removed."""
lines = ['ANTHROPIC_TOKEN="***"\n', "OTHER_KEY='***'\n"]
result = _sanitize_env_lines(lines)
assert result == []
def test_preserves_clean_file(self):
"""A well-formed .env file passes through unchanged (modulo trailing newlines)."""
lines = [
@@ -296,19 +278,18 @@ class TestSanitizeEnvLines:
env_file = tmp_path / ".env"
env_file.write_text(
"ANTHROPIC_API_KEY=sk-antOPENAI_BASE_URL=https://api.openai.com/v1\n"
"STALE_KEY=***\n"
"FAL_KEY=existing\n"
)
with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}):
save_env_value("NEW_KEY", "new-value")
save_env_value("MESSAGING_CWD", "/tmp")
content = env_file.read_text()
lines = content.strip().split("\n")
# Corrupted line should be split, placeholder removed, new key added
# Corrupted line should be split, new key added
assert "ANTHROPIC_API_KEY=sk-ant" in lines
assert "OPENAI_BASE_URL=https://api.openai.com/v1" in lines
assert "NEW_KEY=new-value" in lines
assert "STALE_KEY=***" not in content
assert "MESSAGING_CWD=/tmp" in lines
def test_sanitize_env_file_returns_fix_count(self, tmp_path):
"""sanitize_env_file reports how many entries were fixed."""
@@ -316,7 +297,6 @@ class TestSanitizeEnvLines:
env_file.write_text(
"FAL_KEY=good\n"
"OPENROUTER_API_KEY=valFIRECRAWL_API_KEY=val2\n"
"STALE=***\n"
)
with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}):
fixes = sanitize_env_file()
@@ -324,7 +304,6 @@ class TestSanitizeEnvLines:
# Verify file is now clean
content = env_file.read_text()
assert "STALE=***" not in content
assert "OPENROUTER_API_KEY=val\n" in content
assert "FIRECRAWL_API_KEY=val2\n" in content
@@ -335,3 +314,66 @@ class TestSanitizeEnvLines:
with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}):
fixes = sanitize_env_file()
assert fixes == 0
class TestStaleAnthropicTokenMigration:
"""Test that migrate_config clears stale ANTHROPIC_TOKEN."""
def test_clears_stale_token_when_api_key_exists(self, tmp_path):
"""ANTHROPIC_TOKEN cleared when ANTHROPIC_API_KEY is also set."""
env_file = tmp_path / ".env"
env_file.write_text(
"ANTHROPIC_API_KEY=sk-ant-real-key\n"
"ANTHROPIC_TOKEN=old-stale-token\n"
)
with patch.dict(os.environ, {
"HERMES_HOME": str(tmp_path),
"ANTHROPIC_API_KEY": "sk-ant-real-key",
"ANTHROPIC_TOKEN": "old-stale-token",
}):
migrate_config(interactive=False, quiet=True)
env = load_env()
assert env.get("ANTHROPIC_TOKEN") == ""
assert env.get("ANTHROPIC_API_KEY") == "sk-ant-real-key"
def test_clears_stale_token_when_claude_code_available(self, tmp_path):
"""ANTHROPIC_TOKEN cleared when Claude Code credentials exist."""
env_file = tmp_path / ".env"
env_file.write_text("ANTHROPIC_TOKEN=old-stale-token\n")
fake_creds = {"accessToken": "valid-token", "expiresAt": 0}
with patch.dict(os.environ, {
"HERMES_HOME": str(tmp_path),
"ANTHROPIC_TOKEN": "old-stale-token",
}):
with patch(
"agent.anthropic_adapter.read_claude_code_credentials",
return_value=fake_creds,
), patch(
"agent.anthropic_adapter.is_claude_code_token_valid",
return_value=True,
):
migrate_config(interactive=False, quiet=True)
env = load_env()
assert env.get("ANTHROPIC_TOKEN") == ""
def test_preserves_token_when_no_alternative(self, tmp_path):
"""ANTHROPIC_TOKEN kept when no API key or Claude Code creds exist."""
env_file = tmp_path / ".env"
env_file.write_text("ANTHROPIC_TOKEN=only-auth-method\n")
with patch.dict(os.environ, {
"HERMES_HOME": str(tmp_path),
"ANTHROPIC_TOKEN": "only-auth-method",
}):
os.environ.pop("ANTHROPIC_API_KEY", None)
with patch(
"agent.anthropic_adapter.read_claude_code_credentials",
return_value=None,
):
migrate_config(interactive=False, quiet=True)
env = load_env()
assert env.get("ANTHROPIC_TOKEN") == "only-auth-method"