fix(auth): refresh provider credentials per request (ENG-2116) - #421
Open
lucas-koontz wants to merge 1 commit into
Open
fix(auth): refresh provider credentials per request (ENG-2116)#421lucas-koontz wants to merge 1 commit into
lucas-koontz wants to merge 1 commit into
Conversation
Refs: ENG-2116
This was referenced Sep 1, 2026
There was a problem hiding this comment.
🔵 Needs a closer look
It changes core authentication semantics (typed 401 handling + bounded retries) while also upgrading the OpenAI SDK major version, which can have broad runtime impact beyond the touched call sites.
Pull request overview
This PR updates Anton’s provider-auth handling so long-running sessions can transparently recover from a single typed 401 by re-reading refreshed credentials per request (not rebuilding the turn), while ensuring confirmed credential refusals propagate with an attributed failing role.
Changes:
- Introduces a canonical
ProviderAuthError(typed 401) and updates OpenAI/Anthropic mappers to raise it for HTTP 401. - Adds a bounded “confirm once” retry at the
LLMClientboundary for both non-streaming and streaming calls (streaming retries only before the first event), stamping the failing role on confirmed refusals. - Enables OpenAI dynamic credentials via an async API-key supplier, and updates dependency constraints/tests to pin this behavior.
File summaries
| File | Description |
|---|---|
| uv.lock | Pins openai>=2.21.0 in the lockfile to support async API-key suppliers. |
| pyproject.toml | Updates runtime dependency constraint for OpenAI SDK to >=2.21.0. |
| anton/cli.py | Updates the CLI runtime dependency requirement for OpenAI SDK. |
| anton/core/llm/provider.py | Adds ProviderAuthError(ConnectionError) with optional role attribution. |
| anton/core/llm/openai.py | Maps 401 → ProviderAuthError and supports async API-key suppliers for per-request credential refresh. |
| anton/core/llm/anthropic.py | Maps 401 → ProviderAuthError for canonical auth refusal handling. |
| anton/core/llm/client.py | Implements one-time auth confirmation retry and role attribution for confirmed refusals (including streaming before first event). |
| anton/core/session.py | Updates auth predicate to canonical type and ensures confirmed verifier auth errors aren’t swallowed by broader exception handling. |
| tests/test_status_error_mapper.py | Updates expectations to type-based 401 mapping and pins provider-auth predicate behavior. |
| tests/test_session_auth_error_reraise.py | Ensures the session does not spend retry budget on confirmed auth refusals and uses canonical typing. |
| tests/test_client.py | Adds coverage for confirmation retry, role attribution, and streaming replay guard behavior. |
| tests/test_thalamus.py | Adds coverage that router auth refusal confirms once then fails open (fallback to planning). |
| tests/test_verifier_truncation.py | Ensures confirmed verifier auth refusal is terminal and not retried as truncation. |
| tests/test_openai_dynamic_api_key.py | Regression test proving OpenAI requests re-read the API key without rebuilding the provider. |
| tests/test_chat_error_action_default.py | Pins default UI action behavior to canonical typed auth errors (type-based setup vs retry). |
Review details
Suppressed comments (1)
anton/core/llm/client.py:71
- Same as above: stamping
roleby mutating the raised ProviderAuthError instance can leak state if the same exception object is reused (e.g., in mocks or cached exceptions). Prefer re-raising a new ProviderAuthError withfrom exconce confirmation is exhausted.
if yielded or not confirmation.take():
exc.role = role
raise
- Files reviewed: 14/15 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+44
to
+47
| except ProviderAuthError as exc: | ||
| if not confirmation.take(): | ||
| exc.role = role | ||
| raise |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
User story
As a signed-in Cowork user
I want an active turn to adopt refreshed MindsHub credentials and recover from one isolated authentication refusal
So that long-running work is not discarded or blamed on my account
Why this matters
A long-running turn can retain a ten-minute MindsHub JWT after the desktop refreshes it. The next model call then fails as if the user's session were invalid, discarding substantial work and suggesting a reconnect that cannot repair the stale in-memory credential. Seven observed failures discarded about 1.47 million tokens, including work from a paid external user.
Acceptance criteria
provider_auth; optional probes remain fail-open after confirmation.provider_auth.How to test
ConnectionError, auth-looking text, a bare 401, and non-auth gateway statuses and verify their existing mappings remain intact.Notes for the reviewer
Provider instances stay alive while credentials rotate. A runtime supplier feeds the OpenAI SDK before each main-process request. Static provider settings remain static, and exported scratchpad subprocess configuration keeps its construction-time credential because subprocess hot-swap needs a separate IPC contract.
Confirmation is typed and bounded. Only
ProviderAuthErroris retried, only once, and streaming retries only before the first event. Required planning, coding, and verifier calls propagate a confirmed refusal; optional router, history-summary, and background-memory probes retain their fail-open contract.The terminal error carries the failing role. cowork-server uses that role to preserve the correct Reconnect or update-key action in mixed-provider configurations.
Merge this first. cowork-server pins this exact staging commit before its own PR can ship.
Verified locally
git diff --checkShips with
Merge order: this Anton PR first, then cowork-server, then cowork desktop.
Deployment: cowork-server is the sole preview/deploy anchor.