fix: normalise Nous device-code pool source to avoid duplicates

Review feedback on the original commit: the helper wrote a pool entry
with source `manual:device_code` while `_seed_from_singletons()` upserts
with `device_code` (no `manual:` prefix), so the pool grew a duplicate
row on every `load_pool()` after login.

Normalise: the helper now writes `providers.nous` and delegates the pool
write entirely to `_seed_from_singletons()` via a follow-up
`load_pool()` call. The canonical source is `device_code`; the helper
never materialises a parallel `manual:device_code` entry.

- `persist_nous_credentials()` loses its `label` and `source` kwargs —
  both are now derived by the seed path from the singleton state.
- CLI and web dashboard call sites simplified accordingly.
- New test `test_persist_nous_credentials_idempotent_no_duplicate_pool_entries`
  asserts that two consecutive persists leave exactly one pool row and
  no stray `manual:` entries.
- Existing `test_auth_add_nous_oauth_persists_pool_entry` updated to
  assert the canonical source and single-entry invariant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Antoine Khater
2026-04-18 01:08:09 +00:00
committed by Teknium
parent c096a6935f
commit c7fece1f9d
5 changed files with 98 additions and 80 deletions

View File

@@ -2159,52 +2159,45 @@ def refresh_nous_oauth_from_state(
)
def persist_nous_credentials(
creds: Dict[str, Any],
*,
label: str,
source: str,
):
"""Persist minted Nous OAuth credentials to both auth-store sections.
NOUS_DEVICE_CODE_SOURCE = "device_code"
def persist_nous_credentials(creds: Dict[str, Any]):
"""Persist minted Nous OAuth credentials as the singleton provider state
and ensure the credential pool is in sync.
Nous credentials are read at runtime from two independent locations:
- ``credential_pool.nous``: used by the runtime ``pool.select()`` path that
services outbound inference requests.
- ``providers.nous``: used by ``resolve_nous_runtime_credentials()`` — the
singleton-state reader invoked during 401 recovery and dashboard status
checks.
- ``providers.nous``: singleton state read by
``resolve_nous_runtime_credentials()`` during 401 recovery and by
``_seed_from_singletons()`` during pool load.
- ``credential_pool.nous``: used by the runtime ``pool.select()`` path.
Historically ``hermes auth add nous`` wrote only to the pool while the web
dashboard device-code flow wrote to both, so CLI-provisioned profiles
failed silently when the recovery path was later consulted. This helper
is the single source of truth for CLI/web device-code persistence: both
stores are always written together.
Historically ``hermes auth add nous`` wrote a ``manual:device_code`` pool
entry only, skipping ``providers.nous``. When the 24h agent_key TTL
expired, the recovery path read the empty singleton state and raised
``AuthError`` silently (``logger.debug`` at INFO level).
Returns the added :class:`PooledCredential` entry.
This helper writes ``providers.nous`` then calls ``load_pool("nous")`` so
``_seed_from_singletons`` materialises the canonical ``device_code`` pool
entry from the singleton. Re-running login upserts the same entry in
place; the pool never accumulates duplicate device_code rows.
Returns the upserted :class:`PooledCredential` entry (or ``None`` if
seeding somehow produced no match — shouldn't happen).
"""
from agent.credential_pool import (
PooledCredential,
load_pool,
AUTH_TYPE_OAUTH,
)
pool = load_pool("nous")
entry = PooledCredential.from_dict("nous", {
**creds,
"label": label,
"auth_type": AUTH_TYPE_OAUTH,
"source": source,
"base_url": creds.get("inference_base_url"),
})
pool.add_entry(entry)
from agent.credential_pool import load_pool
with _auth_store_lock():
auth_store = _load_auth_store()
_save_provider_state(auth_store, "nous", creds)
_save_auth_store(auth_store)
return entry
pool = load_pool("nous")
return next(
(e for e in pool.entries() if e.source == NOUS_DEVICE_CODE_SOURCE),
None,
)
def resolve_nous_runtime_credentials(