Auth credential removal undermined by automatic re-seeding
NousResearch/hermes-agent · cluster 9528 · dashboard
Data as of
Nothing open on GitHub. Fix merged in #12360. Check it covers your case.
The short answer
- Known?
- Yes. 9 related items grouped here, 4 issues already closed.
- Fix event
- A fix was merged to main in PR #12360. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PR #67429. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These issues and fixes center on credential removal operations being silently reversed by automatic re-import from sources like ~/.codex/auth.json or singleton auth stores. Work spans CLI auth commands, dashboard pool deletes, and the openai-codex provider.
Open issues0
Open PRs0
Fixes on record
-
#12360 fix(codex): Hermes owns its own Codex auth; stop touching ~/.codex/auth.json
files:
agent/credential_pool.py,hermes_cli/auth.py,tests/hermes_cli/test_auth_codex_provider.pyShow the diff
-
agent/credential_pool.py@@ -22,8 +22,6 @@ _auth_store_lock, _codex_access_token_is_expiring, _decode_jwt_claims, - _import_codex_cli_tokens, - _write_codex_cli_tokens, _load_auth_store, _load_provider_state, _resolve_kimi_base_url, @@ -457,39 +455,6 @@ def _sync_anthropic_entry_from_credentials_file(self, entry: PooledCredential) - logger.debug("Failed to sync from credentials file: %s", exc) return entry - def _sync_codex_entry_from_cli(self, entry: PooledCredential) -> PooledCredential: - """Sync an openai-codex pool entry from ~/.codex/auth.json if tokens differ. - - OpenAI OAuth refresh tokens are single-use and rotate on every refresh. - When the Codex CLI (or another Hermes profile) refreshes its token, - the pool entry's refresh_token becomes stale. This method detects that - by comparing against ~/.codex/auth.json and syncing the fresh pair. - """ - if self.provider != "openai-codex": - return entry - try: - cli_tokens = [redacted:secret-assignment] - if not cli_tokens: - return entry - cli_refresh = cli_tokens.get("refresh_token", "") - cli_access = cli_tokens.get("access_token", "") - if cli_refresh and cli_refresh != entry.refresh_token: - logger.debug("Pool entry %s: syncing tokens from ~/.codex/auth.json (refresh token changed)", entry.id) - updated = replace( - entry, - access_token=[redacted:secret-assignment] - refresh_token=[redacted:secret-assignment] - last_status=None, - last_status_at=None, - last_error_code=None, - ) - self._replace_entry(entry, updated) - self._persist() - return updated - except Exception as exc: - logger.debug("Failed to sync from ~/.codex/auth.json: %s", exc) - return entry - def _sync_device_code_entry_to_auth_store(self, entry: PooledCredential) -> None: """Write refreshed pool entry tokens back to auth.json providers. @@ -585,13 +550,6 @@ def _refresh_entry(self, entry: PooledCredential, *, force: bool) -> Optional[Po except Exception as wexc: logger.debug("Failed to write refreshed token to credentials file: %s", wexc) elif self.provider == "openai-codex": - # Proactively sync from ~/.codex/auth.json before refresh. - # The Codex CLI (or another Hermes profile) may have already - # consumed our refresh_token. Syncing first avoids a - # "refresh_token_reused" error when the CLI has a newer pair. - synced = self._sync_codex_entry_from_cli(entry) - if synced is not entry: - entry = synced refreshed = auth_mod.refresh_codex_oauth_pure( entry.access_token, entry.refresh_token, @@ -677,45 +635,6 @@ def _refresh_entry(self, entry: PooledCredential, *, force: bool) -> Optional[Po # Credentials file had a valid (non-expired) token — use it directly logger.debug("Credentials file has valid token, using without refresh") return synced - # For openai-codex: the refresh_token may have been consumed by - # the Codex CLI between our proactive sync and the refresh call. - # Re-sync and retry once. - if self.provider == "openai-codex": - synced = self._sync_codex_entry_from_cli(entry) - if synced.refresh_token != entry.refresh_token: - logger.debug("Retrying Codex refresh with synced token from ~/.codex/auth.json") - try: - refreshed = auth_mod.refresh_codex_oauth_pure( - synced.access_token, - synced.refresh_token, - ) - updated = replace( - synced, - access_token=[redacted:secret-assignment]"access_token"], - refresh_token=[redacted:secret-assignment]"refresh_token"], - last_refresh=refreshed.get("last_refresh"), - last_status=STATUS_OK, - last_status_at=None, - last_error_code=None, - ) - self._replace_entry(synced, updated) - self._persist() - self._sync_device_code_entry_to_auth_store(updated) - try: - _write_codex_cli_tokens( - updated.access_token, - updated.refresh_token, - last_refresh=updated.last_refresh, - ) - except Exception as wexc: - logger.debug("Failed to write refreshed Codex tokens to CLI file (retry): %s", wexc) - return updated - except Exception as retry_exc: - logger.debug("Codex retry refresh also failed: %s", retry_exc) - elif not self._entry_needs_refresh(synced): - logger.debug("Codex CLI has valid token, using without refresh") - self._sync_device_code_entry_to_auth_store(synced) - return synced self._mark_exhausted(entry, None) return None @@ -734,17 +653,6 @@ def _refresh_entry(self, entry: PooledCredential, *, force: bool) -> Optional[Po # _seed_from_singletons() on the next load_pool() sees fresh state # instead of re-seeding stale/consumed tokens. self._sync_device_code_entry_to_auth_store(updated) - # Write refreshed tokens back to ~/.codex/auth.json so Codex CLI - # and VS Code don't hit "refresh_token_reused" on their next refresh. - if self.provider == "openai-codex": - try: - _write_codex_cli_tokens( - updated.access_token, - updated.refresh_token, - last_refresh=updated.last_refresh, - ) - except Exception as wexc: - logger.debug("Failed to write refreshed Codex tokens to CLI file: %s", wexc) return updated def _entry_needs_refresh(self, entry: PooledCredential) -> bool: @@ -790,16 +698,6 @@ def _available_entries(self, *, clear_expired: bool = False, refresh: bool = Fal if synced is not entry: entry = synced cleared_any = True - # For openai-codex entries, sync from ~/.codex/auth.json before - # any status/refresh checks. This picks up tokens refreshed by - # the Codex CLI or another Hermes profile. - if (self.provider == "openai-codex" - and entry.last_status == STATUS_EXHAUSTED - and entry.refresh_token): - synced = self._sync_codex_entry_from_cli(entry) - if synced is not entry: - entry = synced - cleared_any = True if entry.last_status == STATUS_EXHAUSTED: exhausted_until = _exhausted_until(entry) if exhausted_until is not None and now < exhausted_until: @@ -1218,8 +1116,8 @@ def _seed_from_singletons(provider: str, entries: List[PooledCredential]) -> Tup elif provider == "openai-codex": # Respect user suppression — `hermes auth remove openai-codex` marks # the device_code source as suppressed so it won't be re-seeded from - # either the Hermes auth store or ~/.codex/auth.json. Without this - # gate the removal is instantly undone on the next load_pool() call. + # the Hermes auth store. Without this gate the removal is instantly + # undone on the next load_pool() call. codex_suppressed = False try: from hermes_cli.auth import is_source_suppressed @@ -1231,23 +1129,12 @@ def _seed_from_singletons(provider: str, entries: List[PooledCredential]) -> Tup state = _load_provider_state(auth_store, "openai-codex") tokens = [redacted:secret-assignment]"tokens") if isinstance(state, dict) else None - # Fallback: import from Codex CLI (~/.codex/auth.json) if Hermes auth - # store has no tokens. This mirrors resolve_codex_runtime_credentials() - # so that load_pool() and list_authenticated_providers() detect tokens - # that only exist in the Codex CLI shared file. - if not (isinstance(tokens, dict) and tokens.get("access_token")): - try: - from hermes_cli.auth import _import_codex_cli_tokens, _save_codex_tokens - cli_tokens = [redacted:secret-assignment] - if cli_tokens: - logger.info("Importing Codex CLI tokens into Hermes auth store.") - _save_codex_tokens(cli_tokens) - # Re-read state after import - auth_store = _load_auth_store() - state = _load_provider_state(auth_store, "openai-codex") - tokens = [redacted:secret-assignment]"tokens") if isinstance(state, dict) else None - except Exception as exc: - logger.debug("Codex CLI token import failed: %s", exc) + # Hermes owns its own Codex auth state — we do NOT auto-import from + # ~/.codex/auth.json at pool-load time. OAuth refresh tokens are + # single-use, so sharing them with Codex CLI / VS Code causes + # refresh_token_reused race failures. Users who want to adopt + # existing Codex CLI credentials get a one-time, explicit prompt + # via `hermes auth openai-codex`. if isinstance(tokens, dict) and tokens.get("access_token"): active_sources.add("device_code") changed |= _upsert_entry( -
hermes_cli/auth.py@@ -1434,49 +1434,6 @@ def _read_codex_tokens(*, _lock: bool = True) -> Dict[str, Any]: } -def _write_codex_cli_tokens( - access_token: str, - refresh_token: str, - *, - last_refresh: Optional[str] = None, -) -> None: - """Write refreshed tokens back to ~/.codex/auth.json. - - OpenAI OAuth refresh tokens are single-use and rotate on every refresh. - When Hermes refreshes a token it consumes the old refresh_token; if we - don't write the new pair back, the Codex CLI (or VS Code extension) will - fail with ``refresh_token_reused`` on its next refresh attempt. - - This mirrors the Anthropic write-back to ~/.claude/.credentials.json - via ``_write_claude_code_credentials()``. - """ - codex_home = os.getenv("CODEX_HOME", "").strip() - if not codex_home: - codex_home = str(Path.home() / ".codex") - auth_path = Path(codex_home).expanduser() / "auth.json" - try: - existing: Dict[str, Any] = {} - if auth_path.is_file(): - existing = json.loads(auth_path.read_text(encoding="utf-8")) - if not isinstance(existing, dict): - existing = {} - - tokens_dict = [redacted:secret-assignment]"tokens") - if not isinstance(tokens_dict, dict): - tokens_dict = {} - tokens_dict["access_token"] = access_token - tokens_dict["refresh_token"] = refresh_token - existing["tokens"] = tokens_dict - if last_refresh is not None: - existing["last_refresh"] = last_refresh - - auth_path.parent.mkdir(parents=True, exist_ok=True) - auth_path.write_text(json.dumps(existing, indent=2), encoding="utf-8") - auth_path.chmod(0o600) - except (OSError, IOError) as exc: - logger.debug("Failed to write refreshed tokens to %s: %s", auth_path, exc) - - def _save_codex_tokens(tokens: [redacted:secret-assignment] str], last_refresh: str = None) -> None: """Save Codex OAuth tokens to Hermes auth store (~/.hermes/auth.json).""" if last_refresh is None: @@ -1604,12 +1561,6 @@ def _refresh_codex_auth_tokens( updated_tokens["refresh_token"] = refreshed["refresh_token"] _save_codex_tokens(updated_tokens) - # Write back to ~/.codex/auth.json so Codex CLI / VS Code stay in sync. - _write_codex_cli_tokens( - refreshed["access_token"], - refreshed["refresh_token"], - last_refresh=refreshed.get("last_refresh"), - ) return updated_tokens @@ -1654,25 +1605,7 @@ def resolve_codex_runtime_credentials( refresh_skew_seconds: int = CODEX_ACCESS_TOKEN_REFRESH_SKEW_SECONDS, ) -> Dict[str, Any]: """Resolve runtime credentials from Hermes's own Codex token store.""" - try: - data = _read_codex_tokens() - except AuthError as orig_err: - # Only attempt migration when there are NO tokens stored at all - # (code == "codex_auth_missing"), not when tokens exist but are invalid. - if orig_err.code != "codex_auth_missing": - raise - - # Migration: user had Codex as active provider with old storage (~/.codex/). - cli_tokens = [redacted:secret-assignment] - if cli_tokens: - logger.info("Migrating Codex credentials from ~/.codex/ to Hermes auth store") - print("⚠️ Migrating Codex credentials to Hermes's own auth store.") - print(" This avoids conflicts with Codex CLI and VS Code.") - print(" Run `hermes auth` to create a fully independent session.\n") - _save_codex_tokens(cli_tokens) - data = _read_codex_tokens() - else: - raise + data = _read_codex_tokens() tokens = [redacted:secret-assignment]"tokens"]) access_token = [redacted:secret-assignment]"access_token", "") or "").strip() refresh_timeout_seconds = float(os.getenv("HERMES_CODEX_REFRESH_TIMEOUT_SECONDS", "20")) -
tests/hermes_cli/test_auth_codex_provider.py@@ -14,7 +14,6 @@ PROVIDER_REGISTRY, _read_codex_tokens, _save_codex_tokens, - _write_codex_cli_tokens, _import_codex_cli_tokens, get_codex_auth_status, get_provider_auth_state, @@ -182,98 +181,6 @@ def test_codex_tokens_not_written_to_shared_file(tmp_path, monkeypatch): assert data["tokens"]["access_token"] == "hermes-at" -def test_write_codex_cli_tokens_creates_file(tmp_path, monkeypatch): - """_write_codex_cli_tokens creates ~/.codex/auth.json with refreshed tokens.""" - codex_home = tmp_path / "codex-cli" - monkeypatch.setenv("CODEX_HOME", str(codex_home)) - - _write_codex_cli_tokens("new-access", "new-refresh", last_refresh="2026-04-12T00:00:00Z") - - auth_path = codex_home / "auth.json" - assert auth_path.exists() - data = json.loads(auth_path.read_text()) - assert data["tokens"]["access_token"] == "new-access" - assert data["tokens"]["refresh_token"] == "new-refresh" - assert data["last_refresh"] == "2026-04-12T00:00:00Z" - # Verify file permissions are restricted - assert (auth_path.stat().st_mode & 0o777) == 0o600 - - -def test_write_codex_cli_tokens_preserves_existing(tmp_path, monkeypatch): - """_write_codex_cli_tokens preserves extra fields in existing auth.json.""" - codex_home = tmp_path / "codex-cli" - codex_home.mkdir(parents=True, exist_ok=True) - monkeypatch.setenv("CODEX_HOME", str(codex_home)) - - existing = { - "tokens": { - "access_token": "old-access", - "refresh_token": "old-refresh", - "extra_field": "preserved", - }, - "last_refresh": "2026-01-01T00:00:00Z", - "custom_key": "keep_me", - } - (codex_home / "auth.json").write_text(json.dumps(existing)) - - _write_codex_cli_tokens("updated-access", "updated-refresh") - - data = json.loads((codex_home / "auth.json").read_text()) - assert data["tokens"]["access_token"] == "updated-access" - assert data["tokens"]["refresh_token"] == "updated-refresh" - assert data["tokens"]["extra_field"] == "preserved" - assert data["custom_key"] == "keep_me" - # last_refresh not updated since we didn't pass it - assert data["last_refresh"] == "2026-01-01T00:00:00Z" - - -def test_write_codex_cli_tokens_handles_missing_dir(tmp_path, monkeypatch): - """_write_codex_cli_tokens creates parent directories if missing.""" - codex_home = tmp_path / "does" / "not" / "exist" - monkeypatch.setenv("CODEX_HOME", str(codex_home)) - - _write_codex_cli_tokens("at", "rt") - - assert (codex_home / "auth.json").exists() - data = json.loads((codex_home / "auth.json").read_text()) - assert data["tokens"]["access_token"] == "at" - - -def test_refresh_codex_auth_tokens_writes_back_to_cli(tmp_path, monkeypatch): - """After refreshing, _refresh_codex_auth_tokens writes back to ~/.codex/auth.json.""" - from hermes_cli.auth import _refresh_codex_auth_tokens - - hermes_home = tmp_path / "hermes" - codex_home = tmp_path / "codex-cli" - hermes_home.mkdir(parents=True, exist_ok=True) - codex_home.mkdir(parents=True, exist_ok=True) - (hermes_home / "auth.json").write_text(json.dumps({"version": 1, "providers": {}})) - monkeypatch.setenv("HERMES_HOME", str(hermes_home)) - monkeypatch.setenv("CODEX_HOME", str(codex_home)) - - # Write initial CLI tokens - (codex_home / "auth.json").write_text(json.dumps({ - "tokens": {"access_token": "old-at", "refresh_token": "old-rt"}, - })) - - # Mock the pure refresh to return new tokens - monkeypatch.setattr("hermes_cli.auth.refresh_codex_oauth_pure", lambda *a, **kw: { - "access_token": "refreshed-at", - "refresh_token": "refreshed-rt", - "last_refresh": "2026-04-12T01:00:00Z", - }) - - _refresh_codex_auth_tokens( - {"access_token": "old-at", "refresh_token": "old-rt"}, - timeout_seconds=10, - ) - - # Verify CLI file was updated - cli_data = json.loads((codex_home / "auth.json").read_text()) - assert cli_data["tokens"]["access_token"] == "refreshed-at" - assert cli_data["tokens"]["refresh_token"] == "refreshed-rt" - - def test_resolve_returns_hermes_auth_store_source(tmp_path, monkeypatch): hermes_home = tmp_path / "hermes" _setup_hermes_auth(hermes_home)
-
-
#67429 fix(credentials): pool entry delete via API sticks — suppress source re-seeding (#55217)
files:
hermes_cli/web_server.py,tests/hermes_cli/test_dashboard_admin_endpoints.pyShow the diff
-
hermes_cli/web_server.py@@ -12498,6 +12498,7 @@ async def add_credential_pool_entry(body: CredentialPoolAdd): load_pool, PooledCredential, AUTH_TYPE_API_KEY, + CUSTOM_POOL_PREFIX, SOURCE_MANUAL, ) @@ -12519,6 +12520,23 @@ async def add_credential_pool_entry(body: CredentialPoolAdd): access_token=[redacted:secret-assignment] ) pool.add_entry(entry) + # Re-adding a credential is an explicit re-engagement signal: lift + # every suppression for this provider so a source deleted earlier + # (via DELETE below or `hermes auth remove`) can seed again. + # Mirrors the `hermes auth add` behaviour in auth_commands.py. + if not provider.startswith(CUSTOM_POOL_PREFIX): + try: + from hermes_cli.auth import ( + _load_auth_store, + unsuppress_credential_source, + ) + suppressed = _load_auth_store().get("suppressed_sources", {}) + for src in list(suppressed.get(provider, []) or []): + unsuppress_credential_source(provider, src) + except Exception: + _log.exception("unsuppress after pool add failed (non-fatal)") + except HTTPException: + raise except Exception as exc: _log.exception("POST /api/credentials/pool failed") raise HTTPException(status_code=400, detail=str(exc)) from exc @@ -12527,8 +12545,20 @@ async def add_credential_pool_entry(body: CredentialPoolAdd): @app.delete("/api/credentials/pool/{provider}/{index}") async def remove_credential_pool_entry(provider: str, index: int): - """Remove a pool entry. ``index`` is 1-based (matches the list response).""" + """Remove a pool entry. ``index`` is 1-based (matches the list response). + + Removal must be sticky (#55217): ``load_pool()`` re-seeds entries from + their backing source (.env var, OAuth singleton file, custom-provider + config) on every call, so deleting only the pool row silently reverts on + the next dashboard refresh. We dispatch through the same RemovalStep + registry the CLI ``hermes auth remove`` uses: each source cleans up its + external state and suppresses ``(provider, source)`` so the seeders skip + it. Manual entries have no registered step — nothing external to clean, + no suppression needed (they aren't re-seeded). + """ from agent.credential_pool import load_pool + from agent.credential_sources import find_removal_step + from hermes_cli.auth import suppress_credential_source provider = (provider or "").strip().lower() try: @@ -12539,7 +12569,36 @@ async def remove_credential_pool_entry(provider: str, index: int): raise HTTPException(status_code=400, detail=str(exc)) from exc if removed is None: raise HTTPException(status_code=404, detail="No pool entry at that index") - return {"ok": True, "provider": provider, "count": len(pool.entries())} + + cleaned: List[str] = [] + hints: List[str] = [] + step = find_removal_step(provider, removed.source or "") + if step is not None: + try: + result = step.remove_fn(provider, removed) + cleaned = list(result.cleaned) + hints = list(result.hints) + if result.suppress: + suppress_credential_source(provider, removed.source) + except Exception: + # Cleanup is best-effort, but suppression is the actual bug fix — + # without it the entry resurrects on the next load_pool(). Apply + # it even when source-specific cleanup blew up. + _log.exception( + "credential source cleanup failed for %s/%s; suppressing anyway", + provider, removed.source, + ) + try: + suppress_credential_source(provider, removed.source) + except Exception: + _log.exception("suppress_credential_source failed") + return { + "ok": True, + "provider": provider, + "count": len(pool.entries()), + "cleaned": cleaned, + "hints": hints, + } # --------------------------------------------------------------------------- -
tests/hermes_cli/test_dashboard_admin_endpoints.py@@ -253,6 +253,104 @@ def test_empty_body_rejected(self): ) assert r.status_code == 400 + def test_env_seeded_delete_stays_deleted(self): + """#55217: DELETE must suppress the source or load_pool() resurrects it. + + load_pool() re-seeds from ~/.hermes/.env on every call, so removing + just the pool row silently reverts on the next dashboard refresh. + The endpoint must mirror `hermes auth remove`: clean up the backing + source and suppress (provider, source). + """ + from agent.credential_pool import load_pool + from hermes_cli.auth import is_source_suppressed + from hermes_cli.config import save_env_value + + fake_key = "sk-or-" + "x" * 20 # constructed, never a real key shape + save_env_value("OPENROUTER_API_KEY", fake_key) + + entries = load_pool("openrouter").entries() + assert [e.source for e in entries] == ["env:OPENROUTER_API_KEY"] + + r = self.client.delete("/api/credentials/pool/openrouter/1") + assert r.status_code == 200 + + # Suppressed exactly like the CLI removal path. + assert is_source_suppressed("openrouter", "env:OPENROUTER_API_KEY") + + # Even if the backing var comes back (shell export, another process + # rewriting .env), the removal must stay sticky. + save_env_value("OPENROUTER_API_KEY", fake_key) + assert load_pool("openrouter").entries() == [] + assert self.client.get("/api/credentials/pool").json()["providers"] == [] + + def test_post_readd_lifts_suppression(self): + """Re-adding via POST is an explicit re-engagement — suppressions lift. + + Mirrors `hermes auth add`, which clears every suppression for the + provider so a user who deleted a credential and re-adds one isn't + silently blocked from env re-seeding. + """ + from agent.credential_pool import load_pool + from hermes_cli.auth import is_source_suppressed + from hermes_cli.config import save_env_value + + fake_key = "sk-or-" + "y" * 20 + save_env_value("OPENROUTER_API_KEY", fake_key) + load_pool("openrouter") + assert self.client.delete("/api/credentials/pool/openrouter/1").status_code == 200 + assert is_source_suppressed("openrouter", "env:OPENROUTER_API_KEY") + + r = self.client.post( + "/api/credentials/pool", + json={"provider": "openrouter", "api_key": "sk-or-" + "z" * 20}, + ) + assert r.status_code == 200 + assert not is_source_suppressed("openrouter", "env:OPENROUTER_API_KEY") + + # Key back in .env + suppression lifted → env entry seeds alongside + # the manual one. + save_env_value("OPENROUTER_API_KEY", fake_key) + sources = sorted(e.source for e in load_pool("openrouter").entries()) + assert sources == ["env:OPENROUTER_API_KEY", "manual"] + + def test_manual_delete_adds_no_suppression(self): + """Manual entries aren't re-seeded — CLI parity: no suppression marker.""" + from hermes_cli.auth import _load_auth_store + + self.client.post( + "/api/credentials/pool", + json={"provider": "openrouter", "api_key": "sk-or-" + "m" * 20}, + ) + assert self.client.delete("/api/credentials/pool/openrouter/1").status_code == 200 + suppressed = _load_auth_store().get("suppressed_sources", {}) + assert not suppressed.get("openrouter") + + # Immediate re-add works. + r = self.client.post( + "/api/credentials/pool", + json={"provider": "openrouter", "api_key": "sk-or-" + "n" * 20}, + ) + assert r.status_code == 200 and r.json()["count"] == 1 + + def test_delete_does_not_clobber_other_providers(self): + """Deleting one provider's env entry leaves other providers' rows alone.""" + from agent.credential_pool import load_pool + from hermes_cli.auth import _load_auth_store, read_credential_pool + from hermes_cli.config import save_env_value + + self.client.post( + "/api/credentials/pool", + json={"provider": "anthropic", "api_key": "sk-ant-" + "k" * 20}, + ) + save_env_value("OPENROUTER_API_KEY", "sk-or-" + "q" * 20) + load_pool("openrouter") + + assert self.client.delete("/api/credentials/pool/openrouter/1").status_code == 200 + + assert len(read_credential_pool("anthropic")) == 1 + suppressed = _load_auth_store().get("suppressed_sources", {}) + assert list(suppressed.keys()) == ["openrouter"] + class TestMemoryEndpoints: @pytest.fixture(autouse=True)
-
3 PRs closed without merging
From the close comments: done elsewhere 2 · superseded by another PR 1
-
#13330 fix(auth): suppress codex CLI reseed on remove · done elsewhere →
b02833f,#12747,#12360 -
#13989 fix(auth_commands): normalise manual: prefix before singleton comparisons · done elsewhere →
2c69b3e -
#55219 fix(dashboard): suppress credential source on pool delete to prevent resurrection · superseded by another PR →
#55217,#67429
Closed issues · 4
- Closed#9528openai-codex provider impossible to remove — auto-restore from ~/.codex/auth.json
- Closed#12747auth remove openai-codex can re-seed a new device_code credential from ~/.codex/auth.json
- Closed#13967auth remove re-seeds manually added OAuth credentials from singleton auth store
- Closed#55217Bug: Dashboard credential pool delete succeeds but entries reappear on refresh
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.17: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the Lab