fix: reject empty credential pool leases (#63620)

This commit is contained in:
Teknium 2026-07-12 23:07:02 -07:00 committed by GitHub
parent ba7e5b052a
commit 5d524d0427
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 78 additions and 5 deletions

View file

@ -1416,6 +1416,11 @@ class CredentialPool:
entries_to_prune: List[str] = []
available: List[PooledCredential] = []
for entry in self._entries:
# Borrowed credentials persist as metadata-only references and are
# hydrated from their live source on load. A stale duplicate row
# can remain unhydrated; never lease or select it as an empty key.
if entry.auth_type == AUTH_TYPE_API_KEY and not entry.runtime_api_key:
continue
# For anthropic claude_code entries, sync from the credentials file
# before any status/refresh checks. This picks up tokens refreshed
# by other processes (Claude Code CLI, other Hermes profiles).
@ -1732,11 +1737,15 @@ class CredentialPool:
def _upsert_entry(entries: List[PooledCredential], provider: str, source: str, payload: Dict[str, Any]) -> bool:
existing_idx = None
matching_indices = []
for idx, entry in enumerate(entries):
if entry.source == source:
existing_idx = idx
break
matching_indices.append(idx)
existing_idx = matching_indices[0] if matching_indices else None
duplicate_indices = set(matching_indices[1:])
if duplicate_indices:
entries[:] = [entry for idx, entry in enumerate(entries) if idx not in duplicate_indices]
if existing_idx is None:
payload.setdefault("id", uuid.uuid4().hex[:6])
@ -1768,8 +1777,8 @@ def _upsert_entry(entries: List[PooledCredential], provider: str, source: str, p
# Runtime-only borrowed secret updates should refresh the in-memory
# entry without forcing auth.json churn when the disk-safe payload is
# unchanged (for example env keys with the same fingerprint).
return existing.to_dict() != updated.to_dict()
return False
return bool(duplicate_indices) or existing.to_dict() != updated.to_dict()
return bool(duplicate_indices)
def _normalize_pool_priorities(provider: str, entries: List[PooledCredential]) -> bool:

View file

@ -831,6 +831,70 @@ def test_load_pool_does_not_persist_env_seeded_secret_value(tmp_path, monkeypatc
assert persisted["secret_fingerprint"].startswith("sha256:")
def test_load_pool_collapses_duplicate_env_rows_to_active_key(tmp_path, monkeypatch):
"""One env source is one credential, even if auth.json contains stale duplicates."""
key = "sk-or-active-main-key"
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
monkeypatch.setenv("OPENROUTER_API_KEY", key)
_write_auth_store(
tmp_path,
{
"version": 1,
"credential_pool": {
"openrouter": [
{
"id": "current-row",
"label": "OPENROUTER_API_KEY",
"auth_type": "api_key",
"priority": 0,
"source": "env:OPENROUTER_API_KEY",
},
{
"id": "stale-duplicate",
"label": "OPENROUTER_API_KEY",
"auth_type": "api_key",
"priority": 1,
"source": "env:OPENROUTER_API_KEY",
},
]
},
},
)
from agent.credential_pool import load_pool
pool = load_pool("openrouter")
assert [(entry.id, entry.runtime_api_key) for entry in pool.entries()] == [
("current-row", key)
]
persisted = json.loads((tmp_path / "hermes" / "auth.json").read_text())
assert [entry["id"] for entry in persisted["credential_pool"]["openrouter"]] == [
"current-row"
]
def test_credential_pool_never_selects_empty_borrowed_entry():
from agent.credential_pool import CredentialPool, PooledCredential
pool = CredentialPool(
"openrouter",
[
PooledCredential(
provider="openrouter",
id="metadata-only",
label="OPENROUTER_API_KEY",
auth_type="api_key",
priority=0,
source="env:OPENROUTER_API_KEY",
access_token="",
)
],
)
assert pool.select() is None
assert pool.acquire_lease() is None
def test_load_pool_persists_bitwarden_origin_metadata_without_secret(tmp_path, monkeypatch):
"""Bitwarden-injected env vars retain source metadata but not raw values."""