Skip to content

fix: use OAuth credentials in provider health checks - #828

Open
anandh8x wants to merge 2 commits into
mainfrom
fix/oauth-doctor-connectivity
Open

fix: use OAuth credentials in provider health checks#828
anandh8x wants to merge 2 commits into
mainfrom
fix/oauth-doctor-connectivity

Conversation

@anandh8x

@anandh8x anandh8x commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • pass the existing provider OAuth resolver into health probes
  • authenticate connectivity checks with the stored OAuth credential
  • refresh the OAuth credential once when a safe, non-generating health request returns 401
  • preserve the existing API-key and stored-key paths

Why

OAuth-authenticated providers worked for normal model requests, but zero doctor --connectivity only recognized static API keys and auth headers. A valid OAuth login was therefore reported as missing credentials before the connectivity request was attempted.

User impact

Doctor and provider-health checks now use the same OAuth credential source as normal provider requests. Users receive the provider's real connectivity result instead of a false missing-credentials failure.

Verification

  • make fmt-check
  • go vet ./...
  • go test ./...
  • go run ./cmd/zero-release build
  • go run ./cmd/zero-release smoke
  • make lint-static
  • make vulncheck
  • verified the saved xAI OAuth profile reaches the provider and reports its actual account response rather than requires API credentials

Summary by CodeRabbit

  • New Features
    • Connectivity diagnostics now support OAuth-based authentication.
    • Provider health checks can use OAuth credentials when available, including automatic retry with refreshed tokens after an unauthorized response.
  • Bug Fixes
    • Fixed connectivity probing to correctly authenticate using stored OAuth tokens instead of requiring an API key.
  • Tests
    • Added coverage for OAuth header usage, OAuth unavailability handling, and OAuth refresh behavior during connectivity checks.

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Tests: go test ./...
  • [pass] Build: go run ./cmd/zero-release build
  • [pass] Smoke build: go run ./cmd/zero-release smoke

Scope

Head: 10596d1d2c90
Changed files (4): internal/cli/app.go, internal/cli/observability_test.go, internal/providerhealth/providerhealth.go, internal/providerhealth/providerhealth_test.go

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 199eb710-5b4f-46ae-a113-607453851d13

📥 Commits

Reviewing files that changed from the base of the PR and between 6114791 and 10596d1.

📒 Files selected for processing (2)
  • internal/providerhealth/providerhealth.go
  • internal/providerhealth/providerhealth_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/providerhealth/providerhealth.go

Walkthrough

Provider health probing now supports OAuth credential resolution and refresh after unauthorized responses. CLI dependency wiring supplies OAuth login to connectivity probes, with tests covering bearer-token requests, token refresh, and doctor connectivity output.

Changes

OAuth Provider Health Probing

Layer / File(s) Summary
OAuth credential resolution
internal/providerhealth/providerhealth.go, internal/providerhealth/providerhealth_test.go
Options accepts an OAuth resolver; probing applies resolved OAuth credentials to profiles and validates authenticated connectivity.
Unauthorized connectivity retry
internal/providerhealth/providerhealth.go, internal/providerhealth/providerhealth_test.go
A 401 response triggers OAuth refresh, request reconstruction, and a retry using the refreshed bearer token.
CLI OAuth probe wiring
internal/cli/app.go, internal/cli/observability_test.go
CLI provider health dependencies supply an OAuth resolver, and doctor connectivity tests verify the bearer token and passing connectivity check.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DoctorCLI
  participant ProviderHealth
  participant OAuthLogin
  participant ProviderServer
  DoctorCLI->>ProviderHealth: probe connectivity
  ProviderHealth->>OAuthLogin: resolve OAuth token
  OAuthLogin-->>ProviderHealth: bearer token
  ProviderHealth->>ProviderServer: connectivity request
  ProviderServer-->>ProviderHealth: response
  ProviderHealth-->>DoctorCLI: connectivity check result
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: provider health checks now use OAuth credentials.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/oauth-doctor-connectivity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/providerhealth/providerhealth_test.go (1)

183-273: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Solid coverage of the connectivity paths; consider a companion test for the non-connectivity case.

Both new tests are useful and correctly validate the resolver-driven bearer header and the 401 refresh-and-retry flow. Neither exercises Probe with Connectivity: false and an OAuthResolver that would return ok=false — the scenario tied to the gating gap flagged in providerhealth.go (Lines 211-230), where a config-only probe could report "has OAuth credentials configured" without ever calling the resolver.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/providerhealth/providerhealth_test.go` around lines 183 - 273, The
provider health tests cover OAuth connectivity but miss the non-connectivity
gating path. Add a companion test near TestProbeConnectivityUsesOAuthCredential
that calls Probe with Connectivity false and an OAuthResolver returning
ok=false, then assert the result reflects the unavailable OAuth credential state
and confirms the resolver is invoked as required by the providerhealth.go flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/providerhealth/providerhealth.go`:
- Around line 211-230: The authentication flow around profileWithOAuthCredential
must resolve OAuth credentials for every probe, not only when
options.Connectivity is enabled. Invoke it whenever oauthConfigured indicates an
OAuth resolver exists, while preserving resolveErr failure handling and updating
oauthConfigured from the resolver result before credentialRequired and pass/fail
checks.

---

Nitpick comments:
In `@internal/providerhealth/providerhealth_test.go`:
- Around line 183-273: The provider health tests cover OAuth connectivity but
miss the non-connectivity gating path. Add a companion test near
TestProbeConnectivityUsesOAuthCredential that calls Probe with Connectivity
false and an OAuthResolver returning ok=false, then assert the result reflects
the unavailable OAuth credential state and confirms the resolver is invoked as
required by the providerhealth.go flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 572e0b03-771d-4544-8d58-14336b0b87a8

📥 Commits

Reviewing files that changed from the base of the PR and between 097c265 and 6114791.

📒 Files selected for processing (4)
  • internal/cli/app.go
  • internal/cli/observability_test.go
  • internal/providerhealth/providerhealth.go
  • internal/providerhealth/providerhealth_test.go

Comment thread internal/providerhealth/providerhealth.go

@gnanam1990 gnanam1990 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict

Changes requested. The feature is right and the wiring is correct, but one line in it silently disables redaction of the user's static API key, and I have a reproduction.

Reviewed at 10596d1, base 097c265 (level with origin/main). Author has write access, so this is the internal cycle rather than the issue-approved gate.

What I verified

  • The OAuth credential does reach the probe, and Probe passes the OAuth-mutated profile through to connectivityCheck (providerhealth.go:239), so the first connectivity request carries the token rather than only the retry.
  • Redirect-stripping is recomputed after a refresh. sensitiveAuthHeaderNames(profile, kind) is re-derived from the refreshed profile before the retry client is built (providerhealth.go:321), so the new header name is protected too. That is the easy thing to get wrong here and it is right.
  • Concurrency: not an issue, checked rather than assumed. I expected trouble from a 401-retry inside a probe that might run concurrently, and there is none — this is a single sequential probe.
  • The double Close is harmless. The first defer binds the original body by value, and the explicit _ = response.Body.Close() at :314 closes it again; http.body.Close is safe to call twice. Redundant, not a bug.
  • Gauntlet on darwin/arm64 from a non-/tmp checkout: make fmt-check, go vet ./..., git diff HEAD --check all clean; go test ./internal/providerhealth/ ./internal/cli/ ./internal/providers/... green; go run ./cmd/zero-release build and smoke both pass.
  • Passes executed: all 12, since this is credential-handling on a network path.

Blocking

  1. Clearing profile.APIKey removes the API key from the redaction set, and the provider's response body is echoed into the check message.internal/providerhealth/providerhealth.go:373

    The trace, each hop by name:

    • profileWithOAuthCredential sets profile.APIKey = "" (:373).
    • That same profile is what Probe hands to connectivityCheck (:239), and what every check(...) call receives as its last argument.
    • check redacts via redact(message, profile) (:767), which builds its secret list from providerSecrets(profile) = {profile.APIKey, profile.AuthHeaderValue} (:810).
    • With APIKey blanked, the static key is no longer in that list.
    • The provider's response body is placed directly into the message: message := responseMessage(response.StatusCode, body) (:358).

    So a provider that echoes the offending key in an error body — which several do on 401 — gets it printed verbatim in zero doctor --connectivity, including --json. I ran both paths against the patched build with a static key that matches no built-in secret pattern, so only the profile-derived redaction could catch it:

    API-key path         : static key leaked = false
       message: Provider endpoint returned 401: {"error":"invalid key [REDACTED]"}
    
    OAuth path (this PR) : static key leaked = true
       message: Provider endpoint returned 401: {"error":"invalid key STATICKEYVALUE1234567890abcdef"}
    

    Same profile, same body, same assertions — the only difference is whether OAuthResolver is set. This is a regression against current main behaviour for any user who has both a stored API key and an OAuth login.

    The fix is one line: drop profile.APIKey = "". Clearing it buys nothing on the request path — ApplyAuthHeaders (internal/providers/providerio/headers.go:36-41) only consults APIKey when AuthHeaderValue is empty, and writes exactly one auth header — so the OAuth credential still wins. I applied that change and re-ran: the leak closes, and the full internal/providerhealth suite including TestProbeConnectivityUsesOAuthCredential still passes.

    OAuth path (with fix): static key leaked = false
       message: Provider endpoint returned 401: {"error":"invalid key [REDACTED]"}
    

    If you would rather keep the clear for defence-in-depth, then providerSecrets needs to retain the pre-OAuth key instead — but the redaction set must not shrink when OAuth is in play.

  2. This duplicates providerio.withBearer and providerio.SendWithAuthRetry, which already exist and already do exactly this.internal/providerhealth/providerhealth.go:365-380 and :307-330

    withBearer (internal/providers/providerio/auth.go:80-89) applies an OAuth credential to an auth config with the identical rule set: default the header to Authorization, clear APIKey, clear AuthScheme, set AuthHeaderValue. SendWithAuthRetry (:31-77) implements the same resolve-then-retry-once-on-401-with-forceRefresh loop that connectivityCheck now open-codes.

    The two implementations agree today — I compared them field by field, so this is drift risk rather than a present defect. But it is drift risk on the rule that decides which credential leaves the machine, and withBearer's own comment states the invariant it exists to hold: "the API key is cleared so the two auth methods can never both be sent." Finding 1 is what that duplication already cost, in the form of a redaction property the original author never had to think about because their copy was not used for redaction.

    Health probing has real reasons not to call SendWithAuthRetry wholesale (it must not inherit the 429/503 retry budget, and it needs its own SSRF-guarded client), so I am not asking for that. Reusing withBearer — or exporting an equivalent — for the credential-application half would remove the duplicated rule while leaving the probe's transport alone.

Non-blocking

  • The OAuth lookup error is discarded.internal/cli/app.go:589. options.OAuthResolver, _ = oauthLoginForProfile(options.Profile) drops the error, so a genuinely broken keyring is indistinguishable from "no OAuth login configured" and the user is told their provider requires API credentials. Diagnostics is precisely the context where that distinction is the answer the user came for. Surfacing it as a warn check would be more useful than the current silence.
  • The refresh reads options.Profile where everything around it uses profile.providerhealth.go:308. These are equivalent today, because profileWithOAuthCredential overwrites all four auth fields and nothing else mutates the profile between Probe and here. It is a readability trap rather than a bug: a future mutation applied to profile before the connectivity check would be silently dropped on the retry path only.
  • The explicit close at :314 is redundant given the deferred close bound at :305. Harmless, but it reads as if one of them is load-bearing.
  • hasCredential(profile) is already true by the time !oauthConfigured is checked.providerhealth.go:221. Once profileWithOAuthCredential has set AuthHeaderValue, hasCredential returns true (:831), so && !oauthConfigured is belt-and-braces. Correct, just worth a comment so nobody removes the wrong half.

Questions for the author

  • Was the profile.APIKey = "" line copied deliberately from withBearer, or arrived at independently? It matters for finding 2: if it was copied, the case for reusing the original rather than duplicating it is stronger.

Not covered by this PR

  • No test asserts the "never send both credentials" property. I mutated the APIKey clear away and the whole internal/providerhealth suite stayed green, which is how finding 1 surfaced. Whichever way finding 1 is resolved, a test that pins the redaction behaviour with both a static key and an OAuth login present is the one worth adding.
  • The 401-refresh path is exercised for success and for resolver error, but not for a refresh that returns ok=false. That path falls through and reports the original 401, which is right — it just is not pinned.

Not verified

  • The full go test ./... run. I ran internal/providerhealth, internal/cli and internal/providers/..., plus the release build and smoke; the whole-repo run exceeded my time budget and I would rather say so than imply it passed.
  • Any platform other than darwin/arm64.
  • A real xAI OAuth profile end to end. The PR description reports that manually; my verification is through the package's own harness with a fake resolver and transport.

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