Skip to content

fix(coding-agent): trust structured auth classification; explicit model switch clears stale auth - #2046

Open
snimu wants to merge 8 commits into
sebastian/retry-owner-2026-09-04from
sebastian/retry-auth-heuristics-2026-09-04
Open

fix(coding-agent): trust structured auth classification; explicit model switch clears stale auth#2046
snimu wants to merge 8 commits into
sebastian/retry-owner-2026-09-04from
sebastian/retry-auth-heuristics-2026-09-04

Conversation

@snimu

@snimu snimu commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Deletes the session-layer 401/403 message-text auth sniffing and gives users an explicit recovery path from stale-auth lockouts (Linear: RES-1271; discussion evidence: #1501, #1777, #1611-diagnosability).

Stacked on #2045: it relies on every provider recording structured provider_stream_failure diagnostics (added there for openai-completions and codex).

Why

What

Deletion:

  • Both regex branches in _isConcreteProviderAuthFailure (and the now-unused _getProviderStreamFailureAuthStatus); auth-stale marking now requires structured kind auth.
  • status === 403 removed from the auth classification; a 403 is auth only with an explicit authentication/permission error type. Bare 403 becomes a normal request error (bounded visible retries, no provider lockout).

Recovery:

  • modelRegistry.clearProviderAuthStale(provider) clears both the request-config and auth-storage stale maps.
  • Explicit model selection calls it: AgentSession.setModel plus the in-process and daemon set_model lookups (the lookups must clear before refreshAvailableModels, otherwise the stale provider's models stay excluded and the switch fails with Model not found). A structured auth failure on the next request re-marks the provider.

Not changed: the interactive-mode /model flow additionally offers a login prompt when it sees an unconfigured/stale provider; that path already recovers via re-login and is untouched.

Size

Total src: +102/−65 (net +37); tests: +190/−43 (net +147).
Net src LOC: +32/−37 (net negative; the additions are the clear method and three one-line call sites). Tests: 2 new pins (registry clear restores availability; explicit setModel clears a lockout), 1 new negative pin (unstructured 401 text no longer marks stale), 2 reworked (bare-401 pins converted to structured), 2 classify-table rows.

Validation

  • Sandbox: root tsgo clean; packages/ai + packages/agent suites pass; packages/coding-agent matches the 82-test/14-file environmental baseline exactly.
  • Targeted local: 4491, model-registry, agent-session-retry-events, stream-failure all pass.

Linear: RES-1271


Note

Medium Risk
Changes how auth failures are detected and when providers are marked stale, which affects retries, credential UX, and model availability; mistakes could leave bad keys unflagged or skip legitimate recovery.

Overview
Provider stream failures now distinguish bad credentials from access denials: bare HTTP 403 and permission-shaped errors map to a new permission kind (permanent, no retry) instead of auth, and extractStreamFailureInfo no longer infers auth/permission from error message text alone—only structured provider error types or status codes (401/403) decide.

Coding-agent stale-auth behavior stops treating regex-matched 401/403 text or structured permission failures as reason to mark a whole provider auth-stale; only structured diagnostics with kind auth trigger lockout. Explicit model selection can recover: when the only blocker is staleness, setModel validates with assumeAuthConfigured, then clearProviderAuthStale commits; failed model lookups or entitlement checks leave the lockout intact. Daemon/in-process set_model can resolve models hidden from the “available” list while stale. Prime private-model entitlements are preserved across stale-auth refreshes (same team, serialized refresh) so re-selection can validate against cached authorization.

Reviewed by Cursor Bugbot for commit 567dba4. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Trust structured auth classification and clear stale auth on explicit model switch

  • classifyStreamFailure now requires structured evidence (status 401 or auth/unauthorized type) to classify a failure as auth; 403 and permission-type errors classify as permission.
  • Unstructured error message text alone no longer establishes auth or permission verdicts; only status codes do when no structured provider error type exists.
  • Adds permission as a non-retryable stream failure kind in isPermanentProviderFailureKind.
  • _isConcreteProviderAuthFailure returns true only for structured auth failures, removing status-code and message heuristics from stale-auth decisions.
  • Explicit model selection from a stale provider validates auth before clearing stale state via clearProviderAuthStale; failed lookups leave the provider locked out.
  • Entitlement refresh is serialized through runSerializedEntitlementRefresh, and same-team Prime inference entitlements are preserved across stale-auth refreshes.
  • Risk: Providers that previously triggered stale-auth on bare 401/403 text will no longer be marked stale; any out-of-tree callers relying on classifyStreamFailure or _isConcreteProviderAuthFailure treating 403 as auth must update.

Macroscope summarized 567dba4.

Comment thread packages/coding-agent/src/core/agent-session.ts Outdated
@snimu
snimu force-pushed the sebastian/retry-auth-heuristics-2026-09-04 branch 2 times, most recently from f4077f2 to d45b0de Compare September 4, 2026 12:12
Comment thread packages/coding-agent/src/core/agent-session.ts Outdated
@snimu
snimu force-pushed the sebastian/retry-auth-heuristics-2026-09-04 branch 4 times, most recently from 6a6236e to b220ab5 Compare September 4, 2026 14:17
Comment thread packages/coding-agent/.changes/res-1271-auth-heuristics.md
Comment thread packages/ai/src/utils/stream-failure.ts

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit eee8f74. Configure here.

Comment thread packages/coding-agent/src/core/model-registry.ts Outdated
@snimu
snimu force-pushed the sebastian/retry-auth-heuristics-2026-09-04 branch from 2c4c36e to 1513dd3 Compare September 4, 2026 16:55
Comment thread packages/coding-agent/src/core/model-registry.ts Outdated
Comment thread packages/coding-agent/src/core/model-registry.ts
…cit model selection clear stale auth

- Delete the 401/403 message-text regexes in _isConcreteProviderAuthFailure:
  every provider now records provider_stream_failure diagnostics, so the
  session trusts only structured kind "auth". The regexes classified
  non-auth 403s (region blocks, org policy, model-access denials) as
  concrete auth failures and locked out the whole provider.
- classifyStreamFailure no longer maps a bare 403 status to "auth";
  an explicit authentication/permission error type or 401 is required.
- Explicit model selection (session.setModel and the in-process/daemon
  set_model lookups) clears the provider's stale-auth marking so the
  request runs again; a structured auth failure on that request re-marks
  the provider. This unwedges sessions stuck on "Model not found" after
  a transient auth-shaped incident.
…lockouts only on successful explicit selection

Fresh-eyes review fixes:

- New "permission" failure kind: 403s and permission/forbidden/access-denied
  error types (Anthropic permission_error, SDK PermissionDeniedError, AWS
  AccessDeniedException) are entitlement or policy denials, not bad
  credentials. They are permanent (no retry) but never mark auth stale, so
  a model/org/region-scoped 403 cannot lock out the whole provider.
- An auth verdict now needs structured evidence (401 status or an explicit
  authentication error type); free-form message text alone no longer
  launders into a stale-marking "structured" auth diagnostic.
- AgentSession.setModel is the single owner of the stale-auth clear and
  commits it only when staleness is the sole blocker of an explicit
  selection; the in-process and daemon set_model lookups consult the full
  catalog for stale-auth providers instead of mutating stale state before
  validation, so a mistyped model id or failed refresh no longer unlocks
  a provider that was proven bad.
…e-auth lockout

Macroscope follow-up: setModel cleared the stale markers before
canUseModel, so a failed private-model (team availability) validation
still unlocked the provider.

canUseModel gains an assumeAuthConfigured option that evaluates
availability under a hypothetical clear: for private prime-inference
models it answers the team-authorization question directly when stale
auth excludes the provider from the available list. setModel validates
first and commits the clear only after every check passes, making the
"failed selections never unlock a provider" claim true for all paths.
… verdicts

Review follow-up: the message-text demotion only fired when both type and status were missing, so a 500 whose text mentioned authentication still produced a provider-locking auth kind. Without a structured error type, auth/permission now come solely from the status (401/403).
…le-auth validation

Review follow-up: the hypothetical-clear validation called
refreshAvailableModels, whose keyless run (stale prime-inference auth)
cleared authorizedPrivatePrimeInferenceModelIds - the recovery path
destroyed the cached entitlement it was validating against, so a
previously authorized private model failed with a team-availability
error and the lockout stayed.

- canUseModel with assumeAuthConfigured is now side-effect-free: it
  answers from the current catalog and entitlement caches, no refresh.
- The keyless entitlement refresh preserves the previously fetched ids
  while prime-inference auth is merely stale (a stale marking is not a
  logout; the auth filter still hides the models until recovery), and
  still clears them when auth is actually gone.
…omments

Reduction pass, no behavior change: shared structured-failure and lockout helpers, merged the two private-model selection pins into one sequential pin, table-driven text-verdict rows, and one-line invariant comments (34 -> 15 added comment lines, 193 -> 154 added test lines vs the stack base).
…t preserve

Review follow-ups on the stale-preserve mechanism:

- The keyless refresh restored entitlements even after the user selected a
  different prime-inference team, so wrong-team private models validated
  and then failed at the provider. The preserve now requires the current
  team selection to match the snapshot's team; a switch clears instead.
- Concurrent refreshAvailableModels calls could snapshot the entitlement
  set between another call's clear (inside refresh()) and its restore,
  then restore that empty snapshot - losing the cached authorization
  permanently despite the preserve rule. Refresh sequences that snapshot
  and restore the sets now run serialized through one promise chain
  (getExecutableModels' standalone entitlement refresh included).
@snimu
snimu force-pushed the sebastian/retry-auth-heuristics-2026-09-04 branch from 1513dd3 to 567dba4 Compare September 4, 2026 17:07
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.

1 participant