Skip to content

fix(auth): refresh provider credentials per request (ENG-2116) - #421

Open
lucas-koontz wants to merge 1 commit into
stagingfrom
fix/eng-2116-refresh-active-jwt
Open

fix(auth): refresh provider credentials per request (ENG-2116)#421
lucas-koontz wants to merge 1 commit into
stagingfrom
fix/eng-2116-refresh-active-jwt

Conversation

@lucas-koontz

@lucas-koontz lucas-koontz commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

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

  • An active MindsHub-backed main-session model call reads the refreshed credential without rebuilding the turn; the cross-repo resume handoff applies only when a required planning or coding role uses the runtime MindsHub credential and never blocks direct-provider turns.
  • One typed provider 401 triggers exactly one confirmation attempt with the current credential, and success preserves the turn.
  • Two typed refusals on required planning, coding, or verifier calls propagate as provider_auth; optional probes remain fail-open after confirmation.
  • Generic connection failures, incidental auth-looking text, and bare 401s do not become provider_auth.
  • Existing 402, 403, 429, 5xx, billing, rate-limit, and model-error handling remains unchanged.
  • A comparable post-deployment window has no auth-typed failures for the affected paid install.

How to test

  1. Build an OpenAI provider with token A and a live credential supplier, change the supplier to token B, and verify the next SDK request sends token B.
  2. Return one typed 401 followed by success and verify exactly two calls, normal completion, and no terminal auth error.
  3. Return two typed 401s from required planning, coding, and verifier calls and verify the typed error propagates with the failing role.
  4. Repeat the consecutive failure through router, history-summary, and background-memory probes and verify the required call can still continue.
  5. Return a generic 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 ProviderAuthError is 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

Check Observed result
Focused auth, client, session, verifier, and dynamic-key pytest union 168 passed
Real OpenAI SDK token A → token B header regression Passed
Mutation isolation for live lookup, one retry, pre-first-event streaming, typed classification, role propagation, required propagation, and optional fail-open behavior Each targeted test failed when its behavior was removed and passed after restoration
Pre-PR sweep and git diff --check Clean

Ships with

Merge order: this Anton PR first, then cowork-server, then cowork desktop.
Deployment: cowork-server is the sole preview/deploy anchor.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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 LLMClient boundary 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 role by 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 with from exc once 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 thread anton/core/llm/client.py
Comment on lines +44 to +47
except ProviderAuthError as exc:
if not confirmation.take():
exc.role = role
raise
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants