mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
fix: reject empty credential pool leases (#63620)
This commit is contained in:
parent
ba7e5b052a
commit
5d524d0427
2 changed files with 78 additions and 5 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue