Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion agent/credential_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -921,7 +921,7 @@ def _sync_codex_entry_from_auth_store(self, entry: PooledCredential) -> PooledCr
device_code-sourced entries; env/API-key-sourced entries have no
auth.json shadow to sync from.
"""
if self.provider != "openai-codex" or entry.source not in ("device_code", "manual:device_code"):
if self.provider != "openai-codex" or entry.source != "device_code":
return entry
try:
with _auth_store_lock():
Expand Down
116 changes: 116 additions & 0 deletions tests/agent/test_credential_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,122 @@ def test_unmatched_api_key_hint_rotates_without_benching_innocent_key(tmp_path,
assert persisted.get("last_error_code") is None


def test_codex_runtime_sync_does_not_overwrite_independent_manual_entry(tmp_path, monkeypatch):
"""Runtime refresh must not copy singleton tokens into a manual account.

``manual:device_code`` entries are independent accounts added with
``hermes auth add``. The singleton under ``providers`` shadows only the
seeded ``device_code`` entry. Treating every manual entry as that shadow
silently replaces all accounts with the currently active login.
"""
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
monkeypatch.setattr("hermes_cli.auth._import_codex_cli_tokens", lambda: None)
_write_auth_store(
tmp_path,
{
"version": 1,
"providers": {
"openai-codex": {
"tokens": {
"access_token": "singleton-at",
"refresh_token": "singleton-rt",
},
"auth_mode": "chatgpt",
}
},
"credential_pool": {
"openai-codex": [
{
"id": "independent",
"label": "account-b",
"auth_type": "oauth",
"priority": 1,
"source": "manual:device_code",
"access_token": "account-b-at",
"refresh_token": "account-b-rt",
}
]
},
},
)

from agent.credential_pool import load_pool

pool = load_pool("openai-codex")
manual = next(entry for entry in pool.entries() if entry.id == "independent")
synced = pool._sync_codex_entry_from_auth_store(manual)

assert synced is manual
assert synced.access_token == "account-b-at"
assert synced.refresh_token == "account-b-rt"
persisted = json.loads((tmp_path / "hermes" / "auth.json").read_text())
row = next(
entry
for entry in persisted["credential_pool"]["openai-codex"]
if entry["id"] == "independent"
)
assert row["access_token"] == "account-b-at"
assert row["refresh_token"] == "account-b-rt"


def test_codex_runtime_sync_adopts_rotated_refresh_for_seeded_entry(
tmp_path, monkeypatch
):
"""The singleton may recover with only a rotated refresh token."""
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
monkeypatch.setattr("hermes_cli.auth._import_codex_cli_tokens", lambda: None)
_write_auth_store(
tmp_path,
{
"version": 1,
"providers": {
"openai-codex": {
"tokens": {"refresh_token": "rotated-rt"},
"last_refresh": 1234,
}
},
"credential_pool": {
"openai-codex": [
{
"id": "device_code",
"label": "device-code",
"auth_type": "oauth",
"priority": 0,
"source": "device_code",
"access_token": "stale-at",
"refresh_token": "consumed-rt",
"last_status": "exhausted",
"last_error_code": 401,
"last_error_reason": "refresh_token_reused",
"last_error_reset_at": time.time() + 3600,
}
]
},
},
)

from agent.credential_pool import load_pool

pool = load_pool("openai-codex")
seeded = next(entry for entry in pool.entries() if entry.id == "device_code")
synced = pool._sync_codex_entry_from_auth_store(seeded)

assert synced.access_token == "stale-at"
assert synced.refresh_token == "rotated-rt"
assert synced.last_status is None
assert synced.last_error_code is None
assert synced.last_error_reason is None
assert synced.last_error_reset_at is None
persisted = json.loads((tmp_path / "hermes" / "auth.json").read_text())
row = persisted["credential_pool"]["openai-codex"][0]
assert row["access_token"] == "stale-at"
assert row["refresh_token"] == "rotated-rt"
assert row.get("last_status") is None
assert row.get("last_error_code") is None
assert row.get("last_error_reason") is None
assert row.get("last_error_reset_at") is None


def test_token_invalidated_marks_credential_dead(tmp_path, monkeypatch):
"""OpenAI Codex token_invalidated must mark the credential DEAD, not exhausted.

Expand Down
Loading