fix(auth): only a Codex token refresh writes through to root
Following the grant's source on every save made a fresh device-code login (or `hermes auth import`) under a profile that had been borrowing root's Codex grant overwrite root's account instead of creating the profile's own. Redirecting a save into another file is the exception, so it is opt-in: the refresh path passes write_through=True; login, import and recovery keep saving locally. The two save branches collapse into one (store, path, set_active) triple. Test: root discovery on Windows comes from LOCALAPPDATA — set it so the fixture's root is the resolved root on every host.
This commit is contained in:
+14
-14
@@ -142,7 +142,9 @@ def _sync_codex_pool_entries(
|
||||
_clear_pool_entry_status(entry)
|
||||
|
||||
|
||||
def _save_codex_tokens(tokens: Dict[str, str], last_refresh: str = None, label: str = None) -> None:
|
||||
def _save_codex_tokens(
|
||||
tokens: Dict[str, str], last_refresh: str = None, label: str = None, *, write_through: bool = False,
|
||||
) -> None:
|
||||
"""Save Codex OAuth tokens to the auth store the grant was resolved FROM.
|
||||
|
||||
Codex refresh tokens are single-use with rotation-family reuse detection. A profile without its
|
||||
@@ -151,10 +153,13 @@ def _save_codex_tokens(tokens: Dict[str, str], last_refresh: str = None, label:
|
||||
consumed refresh token, the next process replays it and OpenAI revokes the whole family
|
||||
(#87503). Root-only write-back: a profile copy would shadow root and disable the write-through
|
||||
on the next refresh (#74339). Mirrors the xAI source-aware save.
|
||||
|
||||
Only a token REFRESH passes ``write_through=True``: a fresh login or import under a profile
|
||||
is the profile's own grant and must not overwrite the root account it was borrowing.
|
||||
"""
|
||||
from hermes_cli.auth import (
|
||||
_auth_file_path, _load_auth_store, _provider_state_transaction, _same_path,
|
||||
_save_auth_store, _save_provider_state, _store_provider_state, _utc_now_z)
|
||||
_save_auth_store, _store_provider_state, _utc_now_z)
|
||||
if last_refresh is None:
|
||||
last_refresh = _utc_now_z()
|
||||
with _provider_state_transaction("openai-codex") as (auth_store, state, source_path):
|
||||
@@ -166,20 +171,15 @@ def _save_codex_tokens(tokens: Dict[str, str], last_refresh: str = None, label:
|
||||
state.update(tokens=tokens, last_refresh=last_refresh, auth_mode="chatgpt")
|
||||
if label and str(label).strip():
|
||||
state["label"] = str(label).strip()
|
||||
if source_path is not None and not _same_path(source_path, _auth_file_path()):
|
||||
target_store, target_path, set_active = auth_store, None, True
|
||||
if write_through and source_path is not None and not _same_path(source_path, _auth_file_path()):
|
||||
# Root-borrowed grant: the transaction already holds root's lock, so write the rotated
|
||||
# chain into ROOT's store (never set_active — a refresh is not a provider choice).
|
||||
root_store = _load_auth_store(source_path)
|
||||
_store_provider_state(root_store, "openai-codex", state, set_active=False)
|
||||
_sync_codex_pool_entries(
|
||||
root_store, tokens, last_refresh,
|
||||
previous_singleton_tokens=previous_singleton_tokens)
|
||||
_save_auth_store(root_store, target_path=source_path)
|
||||
return
|
||||
_save_provider_state(auth_store, "openai-codex", state)
|
||||
target_store, target_path, set_active = _load_auth_store(source_path), source_path, False
|
||||
_store_provider_state(target_store, "openai-codex", state, set_active=set_active)
|
||||
_sync_codex_pool_entries(
|
||||
auth_store, tokens, last_refresh, previous_singleton_tokens=previous_singleton_tokens)
|
||||
_save_auth_store(auth_store)
|
||||
target_store, tokens, last_refresh, previous_singleton_tokens=previous_singleton_tokens)
|
||||
_save_auth_store(target_store, target_path=target_path)
|
||||
|
||||
|
||||
def _recover_codex_tokens_from_cli(reason: str) -> Optional[Dict[str, str]]:
|
||||
@@ -396,7 +396,7 @@ def _refresh_codex_auth_tokens(tokens: Dict[str, str], timeout_seconds: float) -
|
||||
updated_tokens = {
|
||||
**tokens, "access_token": refreshed["access_token"],
|
||||
"refresh_token": refreshed["refresh_token"]}
|
||||
_save_codex_tokens(updated_tokens)
|
||||
_save_codex_tokens(updated_tokens, write_through=True)
|
||||
return updated_tokens
|
||||
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ def _read(path: Path) -> dict:
|
||||
def profile_env(tmp_path, monkeypatch):
|
||||
"""Global root at tmp/.hermes, active profile at tmp/.hermes/profiles/work (real on-disk layout)."""
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
monkeypatch.setenv("LOCALAPPDATA", str(tmp_path)) # Windows resolves the native root from here
|
||||
root = tmp_path / ".hermes"
|
||||
profile = root / "profiles" / "work"
|
||||
profile.mkdir(parents=True)
|
||||
@@ -49,7 +50,7 @@ def test_profile_refresh_of_root_grant_writes_through_to_root(profile_env):
|
||||
_write(profile_path, {"version": 1, "providers": {}})
|
||||
|
||||
rotated = _pair("new")
|
||||
auth._save_codex_tokens(rotated, last_refresh="2026-08-16T00:00:00Z")
|
||||
auth._save_codex_tokens(rotated, last_refresh="2026-08-16T00:00:00Z", write_through=True)
|
||||
|
||||
root = _read(root_path)
|
||||
assert root["providers"]["openai-codex"]["tokens"] == rotated
|
||||
@@ -68,7 +69,15 @@ def test_profile_owned_grant_stays_local(profile_env):
|
||||
_write(root_path, {"version": 1, "providers": {}})
|
||||
|
||||
rotated = _pair("next")
|
||||
auth._save_codex_tokens(rotated, last_refresh="2026-08-16T00:00:00Z")
|
||||
auth._save_codex_tokens(rotated, last_refresh="2026-08-16T00:00:00Z", write_through=True)
|
||||
|
||||
assert _read(profile_path)["providers"]["openai-codex"]["tokens"] == rotated
|
||||
assert "openai-codex" not in _read(root_path).get("providers", {})
|
||||
|
||||
# A fresh login under a profile that was borrowing root's grant is the profile's own account,
|
||||
# never a rewrite of root's.
|
||||
_write(profile_path, {"version": 1, "providers": {}})
|
||||
_write(root_path, {"version": 1, "providers": {"openai-codex": {"auth_mode": "chatgpt", "tokens": _pair("root")}}})
|
||||
auth._save_codex_tokens(_pair("login"))
|
||||
assert _read(profile_path)["providers"]["openai-codex"]["tokens"] == _pair("login")
|
||||
assert _read(root_path)["providers"]["openai-codex"]["tokens"] == _pair("root")
|
||||
|
||||
Reference in New Issue
Block a user