Skip to content

fix(auth): _auth_lock_path follows symlinks so shared auth.json serializes across processes - #62945

Open
vkrmch wants to merge 1 commit into
NousResearch:mainfrom
vkrmch:fix/auth-lock-follows-symlink
Open

fix(auth): _auth_lock_path follows symlinks so shared auth.json serializes across processes#62945
vkrmch wants to merge 1 commit into
NousResearch:mainfrom
vkrmch:fix/auth-lock-follows-symlink

Conversation

@vkrmch

@vkrmch vkrmch commented Jul 12, 2026

Copy link
Copy Markdown

Summary

Motivation

I hit this on a 3-agent host sharing one Codex OAuth via ~/.hermes/auth.json/var/lib/hermes-oauth/auth.json symlinks. After the first refresh cycle, the shared auth.json went from 5 providers (gemini, openai-codex, openrouter, xai, copilot) to 2 (gemini, openrouter). openai-codex, xai, copilot all wiped from credential_pool. active_provider cleared. providers map emptied. Required a full OAuth re-auth to recover.

Traced to _auth_lock_path() — the fcntl.flock was on ~/agent-x/.hermes/auth.lock, not on the target of the symlink, so cross-process serialization silently didn't work.

Related

Fix

hermes_cli/auth.py::_auth_lock_path now resolves the auth.json symlink and places the lock next to the REAL file:

def _auth_lock_path() -> Path:
    auth = _auth_file_path()
    try:
        target = Path(os.path.realpath(str(auth)))
    except OSError:
        target = auth
    return target.with_suffix(".lock")
  • Symlink setup: all processes lock the SAME <target-dir>/auth.lock — cross-process load-modify-save is properly serialized.
  • Non-symlink setup: realpath returns the original path; lock stays at ~/.hermes/auth.lock — identical to today.
  • Broken symlink (dangling): realpath returns the (non-existent) target path; the flock is created next to it, and a future creator of the shared file picks up the same lock.

Tests

Added tests/hermes_cli/test_auth_lock_path_symlink.py with 4 cases:

  • Two profiles sharing an auth.json via symlink resolve to the SAME lock (the regression this fixes).
  • Non-symlinked auth.json: lock sits next to it (behavior unchanged).
  • No auth.json yet: lock path derives from _auth_file_path() unchanged.
  • Dangling symlink: lock is placed next to the (non-existent) target.

All 4 pass locally.

Test plan

  • New regression tests pass
  • Verified on my 3-agent host: after applying, _auth_lock_path() returns the shared target /var/lib/hermes-oauth/auth.lock for all agents; the shared lock file gets created group-writable on first acquisition; no observed provider-purge in subsequent refresh cycles.
  • CI green

🤖 Generated with Claude Code

https://claude.ai/code/session_01CmDNdF6D8TVXRohg17tqNU

…lizes

`_auth_lock_path()` returned the caller's LOCAL `~/.hermes/auth.lock`.
That's fine solo, but silently breaks the shared-auth patterns requested
in NousResearch#19436 / NousResearch#29530 / NousResearch#38176 — profiled workers, delegated agents, or
multi-agent hosts symlinking `~/.hermes/auth.json` to one canonical file.

In that setup each process holds a DIFFERENT flock while writing to the
SAME real file, so cross-process load-modify-save races can persist a
partial store that purges providers a peer just added. Repro on a
3-agent shared-Codex host: `credential_pool` went 5 providers → 2 in one
refresh cycle; codex + xai + copilot all wiped, `active_provider`
cleared. Full re-auth required.

Fix resolves the symlink target so all sharers lock the same file. Non-
symlink setups are unchanged (realpath returns the original path).

Related: NousResearch#8040 (same class — process-local threading.Lock in
credential_pool.py — this PR fixes only the auth.py surface).
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused regression fix. The premise remains present on current main: hermes_cli/auth.py:983-984 derives the lock from the caller-local auth path, and _auth_store_lock() passes it to the cross-process lock at hermes_cli/auth.py:1072-1078. With separate symlinks to one auth store, those are different locks.

The change resolves the relevant target without altering normal-path behavior. It also fits the existing write behavior: hermes_cli/auth.py:1153 uses atomic_replace, whose symlink-preserving implementation resolves the real target before replacing it (utils.py:91-136). That means the proposed lock coordinates the same file the existing persistence code updates.

gh pr diff 62945 | git apply --check --verbose succeeds against current main.

Automated hermes-sweeper review.

@alt-glitch alt-glitch added type/bug Something isn't working comp/cli CLI entry point, hermes_cli/, setup wizard area/auth Authentication, OAuth, credential pools P2 Medium — degraded but workaround exists labels Jul 12, 2026
@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants