fix(auth): auto-heal single-use OAuth grants already forked across profiles (#100339)
The clone-strip and root-write-through in the previous commit stop NEW forks but leave installs that forked before upgrading in the broken state: each profile keeps its own copy of the root grant, whichever profile rotated last holds the only live refresh token, and root plus every sibling still hit invalid_grant on their next refresh. The PR body asked those users to re-auth at root and hand-edit profiles/*/auth.json; this makes it automatic. `heal_forked_single_use_oauth_grants(provider)` (hermes_cli/auth.py) runs at the top of a profile's `load_pool()` for SINGLE_USE_REFRESH_POOL_PROVIDERS. Under the profile lock then the root lock it matches each profile OAuth row to its root counterpart by lineage — same pool id (preserved by both fork paths), same JWT account identity, same token material, else same provider + same client (Anthropic pkce grants carry no claims) — keeps the copy with the freshest rotation (`expires_at_ms` / `last_refresh` / JWT exp), writes it into ROOT when root's is older, and strips the profile copy (pool rows, the `providers.<id>` device-code block for Codex/xAI, and a profile-local `.anthropic_oauth.json`) so the profile borrows root from then on. Root's singleton and its hermes_pkce row are kept in step so root's own re-seed cannot resurrect the spent pair. Guarantees: idempotent (mtime-keyed clean mark skips the locked scan on the per-call hot path); one INFO line per healed profile; API-key rows untouched; a row with no root counterpart (root lost its grant, or an independent account whose claims differ) is never deleted; only the two auth.json files the root fallback already reads are touched — no environ/secret-scope reads. `hermes auth list` / `hermes auth status <provider>` print the heal note. Live repro (real imports, temp root + forge/atlas each holding a pre-fix verbatim copy, forge already rotated RT0->RT1 into its own file, fake single-use token endpoint): before — atlas None, forge AT2 (only in forge), root None; server log 4x REUSE of spent RT0. After — forge's load heals to root and rotates there, atlas and root select AT2, profiles/*/auth.json hold no anthropic rows, server log exactly one ROTATE and zero REUSE.
This commit is contained in:
@@ -3774,6 +3774,12 @@ def _seed_custom_pool(pool_key: str, entries: List[PooledCredential]) -> Tuple[b
|
||||
|
||||
def load_pool(provider: str) -> CredentialPool:
|
||||
provider = (provider or "").strip().lower()
|
||||
if provider in SINGLE_USE_REFRESH_POOL_PROVIDERS:
|
||||
# One-time heal for installs that forked this grant across profiles
|
||||
# BEFORE the clone-strip / root-write-through existed: consolidate the
|
||||
# profile's copy into root so the read below borrows root's grant
|
||||
# (#100339). No-op in classic mode or once the profile is clean.
|
||||
auth_mod.heal_forked_single_use_oauth_grants(provider)
|
||||
raw_entries = read_credential_pool(provider)
|
||||
disk_ids = {
|
||||
entry.get("id")
|
||||
|
||||
Reference in New Issue
Block a user