fix: hermes auth remove now clears env-seeded credentials permanently (#5285)

Removing an env-seeded credential (e.g. from OPENROUTER_API_KEY) via
'hermes auth' previously had no lasting effect -- the entry was deleted
from auth.json but load_pool() re-created it on the next call because
the env var was still set.

Now auth_remove_command detects env-sourced entries (source starts with
'env:') and calls the new remove_env_value() to strip the var from both
.env and os.environ, preventing re-seeding.

Changes:
- hermes_cli/config.py: add remove_env_value() -- atomically removes a
  line from .env and pops from os.environ
- hermes_cli/auth_commands.py: auth_remove_command clears env var when
  removing an env-seeded pool entry
- 8 new tests covering remove_env_value and the full zombie-credential
  lifecycle (remove -> reload -> stays gone)
This commit is contained in:
Teknium 2026-04-05 12:00:53 -07:00 committed by GitHub
parent 0c95e91059
commit 6ee90a7cf6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 235 additions and 0 deletions

View file

@ -521,3 +521,139 @@ def test_auth_list_prefers_explicit_reset_time(monkeypatch, capsys):
out = capsys.readouterr().out
assert "device_code_exhausted" in out
assert "7d 0h left" in out
def test_auth_remove_env_seeded_clears_env_var(tmp_path, monkeypatch):
"""Removing an env-seeded credential should also clear the env var from .env
so the entry doesn't get re-seeded on the next load_pool() call."""
hermes_home = tmp_path / "hermes"
hermes_home.mkdir(parents=True, exist_ok=True)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
# Write a .env with an OpenRouter key
env_path = hermes_home / ".env"
env_path.write_text("OPENROUTER_API_KEY=sk-or-test-key-12345\nOTHER_KEY=keep-me\n")
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-test-key-12345")
# Seed the pool with the env entry
_write_auth_store(
tmp_path,
{
"version": 1,
"credential_pool": {
"openrouter": [
{
"id": "env-1",
"label": "OPENROUTER_API_KEY",
"auth_type": "api_key",
"priority": 0,
"source": "env:OPENROUTER_API_KEY",
"access_token": "sk-or-test-key-12345",
}
]
},
},
)
from hermes_cli.auth_commands import auth_remove_command
class _Args:
provider = "openrouter"
target = "1"
auth_remove_command(_Args())
# Env var should be cleared from os.environ
import os
assert os.environ.get("OPENROUTER_API_KEY") is None
# Env var should be removed from .env file
env_content = env_path.read_text()
assert "OPENROUTER_API_KEY" not in env_content
# Other keys should still be there
assert "OTHER_KEY=keep-me" in env_content
def test_auth_remove_env_seeded_does_not_resurrect(tmp_path, monkeypatch):
"""After removing an env-seeded credential, load_pool should NOT re-create it."""
hermes_home = tmp_path / "hermes"
hermes_home.mkdir(parents=True, exist_ok=True)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
# Write .env with an OpenRouter key
env_path = hermes_home / ".env"
env_path.write_text("OPENROUTER_API_KEY=sk-or-test-key-12345\n")
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-test-key-12345")
_write_auth_store(
tmp_path,
{
"version": 1,
"credential_pool": {
"openrouter": [
{
"id": "env-1",
"label": "OPENROUTER_API_KEY",
"auth_type": "api_key",
"priority": 0,
"source": "env:OPENROUTER_API_KEY",
"access_token": "sk-or-test-key-12345",
}
]
},
},
)
from hermes_cli.auth_commands import auth_remove_command
class _Args:
provider = "openrouter"
target = "1"
auth_remove_command(_Args())
# Now reload the pool — the entry should NOT come back
from agent.credential_pool import load_pool
pool = load_pool("openrouter")
assert not pool.has_credentials()
def test_auth_remove_manual_entry_does_not_touch_env(tmp_path, monkeypatch):
"""Removing a manually-added credential should NOT touch .env."""
hermes_home = tmp_path / "hermes"
hermes_home.mkdir(parents=True, exist_ok=True)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
env_path = hermes_home / ".env"
env_path.write_text("SOME_KEY=some-value\n")
_write_auth_store(
tmp_path,
{
"version": 1,
"credential_pool": {
"openrouter": [
{
"id": "manual-1",
"label": "my-key",
"auth_type": "api_key",
"priority": 0,
"source": "manual",
"access_token": "sk-or-manual-key",
}
]
},
},
)
from hermes_cli.auth_commands import auth_remove_command
class _Args:
provider = "openrouter"
target = "1"
auth_remove_command(_Args())
# .env should be untouched
assert env_path.read_text() == "SOME_KEY=some-value\n"