fix: treat transient auth failures as retryable, not session-expired#262
Merged
Conversation
Contributor
|
🎉 PR Validation ✅ PASSED Commit: Checks:
Ready to merge! ✨ 🔗 View workflow run |
vreshch
added a commit
that referenced
this pull request
Jul 9, 2026
## Root cause
The CLI target host comes from `siteFqdn()` =
`normalizeFqdn(process.env.AGENTAGE_SITE_FQDN)`, defaulting to
`agentage.io` (production) when unset. But the stored credential in
`~/.agentage/auth.json` carries its OWN `siteFqdn` (the environment it
was issued for, e.g. `dev.agentage.io`).
`gatherStatus` / `introspectToken` validated the stored token against
the CURRENT target's auth server, ignoring the credential's own
`auth.siteFqdn`. So a credential issued for dev, checked against
production, was rejected and reported as `auth [x] session expired -
run: agentage setup` - misleading: the session is not expired, it
belongs to a DIFFERENT environment than the CLI is pointed at.
## Fix
New helper `src/lib/auth/env-match.ts` `detectEnvMismatch(auth,
targetFqdn)` compares normalized fqdns. `gatherStatus` calls it BEFORE
introspection: on a mismatch it renders a neutral line and does NOT
introspect cross-environment.
### Before (mismatch case)
```
auth [x] session expired - run: agentage setup
```
### After (mismatch case)
```
auth ! signed in to dev.agentage.io - CLI targets agentage.io (production); run: agentage setup to sign into production, or set AGENTAGE_SITE_FQDN=dev.agentage.io
```
- New marker: `!` (yellow, neutral - not check, not cross). Exit code
stays 0.
- `status --json` gains an additive `auth.mismatch` field: `{
credentialFqdn, credentialEnv, targetFqdn, targetEnv }`, `signedIn:
false`. Existing fields (`signedIn`, `endpoint`, `fqdn`, `env`,
`version`) unchanged - e2e helpers untouched.
## setup
When already signed in but the stored credential is for a DIFFERENT
environment than the current target, setup no longer says a bare
"Already signed in." It informs the user they are signed into
`<credEnv>` and this run signs them into `<targetEnv>`, then proceeds to
a fresh sign-in (the correct action). Matching-env behavior unchanged.
## Same-env path preserved
When credential env == target env, behavior is exactly as before:
introspect via the #262-hardened path (valid -> session active;
transient -> transient note; terminal -> the literal `session expired -
run: agentage setup` that e2e greps). The mismatch case is a NEW,
distinct message and does not reuse the expired literal.
## Tests
- `env-match.test.ts`: match/mismatch both directions + localhost/dev
detection + fqdn normalization.
- `status-info.test.ts`: mismatch (dev cred vs prod target, and reverse)
-> `mismatch` set, `signedIn: false`, not "session expired", no
cross-env introspection.
- `status.test.ts`: neutral `!` mismatch line naming both sides, never
expired.
- `setup.test.ts`: mismatched-env (both directions) -> informs + signs
into current target, no introspect; matched-env valid -> "Already signed
in" unchanged.
`npm run verify` green (481 tests).
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.
Root cause
agentage statusintermittently showedauth ✗ session expired - run: agentage setupon a healthy, refreshable session. The oldsrc/lib/auth/api.tsmisclassified TRANSIENT failures as TERMINAL: a 200 + null get-session (which the Better Auth AS also returns under transient load / 429 / 5xx / network blip) calledtryRefresh, andtryRefreshswallowed EVERY failure intofalse(catch -> false), so any blip during that refresh madeintrospectTokenthrowAuthRequiredError('no active session')-> rendered "session expired." This is the same transient-as-terminal class fixed server-side in web#219 (get-session resilience), never ported to the CLI.Classification taxonomy (mirrors web#219)
AuthRequiredError, session truly needs re-auth): HTTP 401, or OAuthinvalid_grant/invalid_clienton the refresh grant, or a definitive 200 + null get-session whose decisive refresh is itself terminal.TransientAuthError, never "expired"): 429, 5xx, network errors, timeouts, unexpected redirects, or any refresh failure that is not an explicit invalid_grant/invalid_client/401.refreshTokensnow throws a typedTokenRequestError { status, oauthError };isTerminalRefresh(in the newauth-errors.ts) does the split.refreshOrThrowdistinguishes the two instead ofcatch -> false.Behavior changes
introspectToken(now inintrospect.ts):AuthRequiredError; TRANSIENT ->TransientAuthError.statusauth line, one per state (exit code stays 0 in all three):✓ signed in (session active)~ signed in (could not re-verify - temporary)✗ session expired - run: agentage setup(exact literal preserved - e2e greps it)setupnow VALIDATES instead of trusting token presence:Already signed in.+ status (creds intact)Session expired - signing you in again.and auto-enters the sign-in flow (no--reauthneeded)You appear signed in (could not fully verify - temporary).+ status, creds NOT wipedTest delta
api.test.ts: terminal refresh now usesinvalid_grant(400 alone is transient); added transient 429/503 cases + the unexpired-token-blip signed-in case.status-info.test.ts: updated the old l.111-147 assertions (400 ->invalid_grantfor the expired literal; a 5xx blip on an unexpired token is signed-in, not "could not verify"); added an expired-token transient case.setup.test.ts: addedintrospectdep; valid -> "Already signed in"; terminal -> auto sign-in; transient -> no forced reauth, creds intact.npm run verifygreen (471 tests). Built + smokeddist/cli.js statusandsetupagainst an isolated config dir (unexpired/expired token, unreachable host) - each state renders as described, exit 0, creds preserved.