Skip to content

OAuth / account login for providers (OpenRouter, xAI) + subscription-proxy support#217

Merged
Vasanthdev2004 merged 21 commits into
mainfrom
feat/oauth-provider-login
Jun 16, 2026
Merged

OAuth / account login for providers (OpenRouter, xAI) + subscription-proxy support#217
Vasanthdev2004 merged 21 commits into
mainfrom
feat/oauth-provider-login

Conversation

@gnanam1990

@gnanam1990 gnanam1990 commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds real OAuth / account login for the providers that support it, surfaced as a first-class "Sign in with OAuth" option in both the /provider wizard and first-run onboarding — no API key to paste. Two providers qualify today:

  • OpenRouter — public PKCE flow that mints an API key (no client_id needed).
  • xAI (Grok) — public OAuth client; the token is used directly as a bearer on api.x.ai/v1. Supports browser and device-code login.

For OpenAI/Anthropic subscriptions (ChatGPT/Claude), which can't be used as a standard bearer by a third-party tool, this ships a documented local-proxy path instead of an unreliable/ToS-risky direct login.

What's included

Runtime auth wiring

  • providerio.SendWithAuthRetry + TokenResolver: the OpenAI/Anthropic providers send Authorization: Bearer <oauth> when a login exists for the profile (auto-refreshed; one refresh-and-retry on 401), else fall back to the API key. Never both. Zero per-request overhead for API-key users (resolver attached only when a login exists).

OAuth engine & logins

  • internal/oauth: PKCE (S256), loopback browser flow, RFC 8628 device-code flow, token store (0600 / optional OS keyring), env-configurable providers (ZERO_OAUTH_<NAME>_*) + a built-in preset layer (xAI).
  • internal/provideroauth.OpenRouterLogin (mints a key) and zero auth openrouter; zero auth login xai [--device].

In-app UX

  • A "How do you want to connect?" chooser (wizard + onboarding): Sign in with OAuth lists the OAuth-capable providers → browser/device login → straight to model selection. Browse/API-key path unchanged.
  • Device code for headless/SSH: press d (or automatic on SSH / headless Linux / ZERO_OAUTH_DEVICE=1) — shows a URL + code to enter on another device.
  • After login, model discovery is authenticated with the OAuth token, so the user picks from the live model list (curated fallback if offline).

Subscription proxies (ChatGPT / Claude)

  • chatgpt-proxy preset + docs/oauth-subscriptions.md: full recipe for pointing Zero at a local proxy that holds the subscription session. ChatGPT/Claude are intentionally not in the OAuth chooser (they can't do clean in-app OAuth); they live under the browse path + proxy docs.

Deliberately not included

Direct ChatGPT/Claude subscription login. Verified: the ChatGPT token only works against a Cloudflare-gated backend (403 for non-official clients), and Anthropic's terms prohibit third-party use of a Claude subscription token (enforced with account bans). Spoofing those official clients is fragile and account-risky, so it's not built — the proxy path is the supported alternative.

Testing

  • gofmt, go vet, build (host + linux + windows), go test ./... -race — all green.
  • staticcheck (no new findings), govulncheck (0), deadcode (no new) — clean.
  • Unit tests cover the bearer/retry logic, OAuth engine (loopback + two-phase device), the wizard/onboarding chooser + device shortcut, token-authenticated discovery, and catalog classification. OAuth/proxy flows verified against mock servers.
  • No new module dependencies.

Notes

The interactive browser/device approval and the live xAI /models response require a real account to confirm end-to-end; all surrounding logic is unit-tested and the OpenRouter mint flow was run-checked against a mock.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added OAuth login across CLI and TUI, including auth openrouter (browser-based PKCE that prints an API key) and RFC 8628 device-code login where supported.
    • Introduced a new “connect method” step in onboarding/provider setup (including device-code shortcuts, OAuth badges, and device-code/browser waiting states).
    • Added per-request OAuth bearer auth for OpenAI- and Anthropic-compatible providers with automatic refresh-on-401 retry, plus optional built-in OAuth presets (opt-in for xAI).
  • Documentation

    • Expanded the OAuth subscriptions guide with local proxy configuration examples.
  • Bug Fixes

    • Improved OAuth-pending interaction handling (mouse input disabled) and avoided unnecessary onboarding by recognizing stored OAuth logins.

Foundation for account/subscription login: when a user is logged in via
`zero auth login <name>`, the OpenAI and Anthropic providers now authenticate
model requests with that OAuth bearer token, falling back to the API key when
there is no login. Auto-select, per the agreed design — no config needed.

  - providerio.SendWithAuthRetry: issues a request with an OAuth bearer (from a
    TokenResolver) or the API key, and retries ONCE with a force-refreshed token
    after a 401. A 401 means the server rejected the request without processing a
    completion, so replay-after-refresh is safe — the same no-duplicate-work basis
    SendWithRetry uses for 429/503. Never sends API key and bearer together.
  - openai + anthropic providers take an optional OAuthResolver and use
    SendWithAuthRetry. When OAuth is active the credential goes on Authorization
    (so Anthropic's x-api-key is correctly omitted). Gemini is unchanged.
  - providers.New threads Options.OAuthResolver to those two providers.
  - cli.oauthResolverForProfile builds the resolver from the unified oauth store:
    it attaches a resolver ONLY when a login already exists for the provider
    name, so API-key users incur no per-request store lookups. The resolver uses
    Manager.GetFresh (refresh near expiry) and Manager.Handle401 (force refresh on
    the 401 retry).
  - oauth: added ErrNoToken sentinel so "not logged in" is distinguishable from a
    real refresh failure (login removed mid-session → clean API-key fallback).

Tests (httptest): API-key fallback (nil resolver / ok=false), bearer wins +
never-both, 401→force-refresh→retry, stops after one retry, resolver error
surfaced. Local run check (real binary + mock OAuth + mock OpenAI-compatible LLM
requiring the bearer): with a login, `zero exec` sends Bearer <oauth-token> and
gets a completion; without a login it falls back to the (wrong) API key and the
mock returns 401 — proving the OAuth path.

Gates: gofmt/vet/build(host+linux+windows)/test -race/staticcheck(no new)/
govulncheck(0)/deadcode(no new) all pass.
…tions docs

Follows verified research (cross-checked across ~30 sources) into using ChatGPT
and Claude subscriptions: a subscription OAuth token does NOT work as a standard
bearer on the public APIs. OpenAI's token only works against the Cloudflare-gated
ChatGPT backend (headless clients get cf-mitigated 403); Anthropic rejects
third-party subscription OAuth, requires Claude-Code identity spoofing, 400s on
tool calls (Max overage lane), and is contractually banned with active
enforcement. So zero does NOT call those backends or spoof those clients.

Instead, the supported pattern is a local proxy that holds the subscription
session and exposes a clean OpenAI/Anthropic-compatible endpoint on 127.0.0.1;
zero points at it (it already supports custom base URLs, and C1 supplies OAuth
bearer for gateways that issue standard tokens).

  - providercatalog: add `chatgpt-proxy` ("ChatGPT (local OAuth proxy)") — an
    OpenAI-compatible, local, no-API-key preset defaulting to the established
    proxy port (http://localhost:10531/v1), base URL overridable. Claude uses the
    existing `custom-anthropic-compatible` entry (no canonical Claude-proxy port
    to hardcode — not invented).
  - docs/oauth-subscriptions.md: full recipe — built-in OAuth login (C1), the
    verified reason subscriptions need a proxy, copy-paste config for ChatGPT and
    Claude via a local proxy, and the sanctioned alternatives (API key; spawning
    the real `claude` CLI for Anthropic subscription automation).

Local run check: a config using `catalogID: chatgpt-proxy` (base URL pointed at a
mock OpenAI-compatible proxy) routes `zero exec` through the proxy with no auth
header and returns a completion.

Gates: gofmt/vet/build(host+linux+windows)/test/staticcheck(clean)/govulncheck(0)/
deadcode(no new) all pass. No new module deps.
…ro auth openrouter`

Adds the cleanest real OAuth login surfaced by the provider survey: OpenRouter's
loopback PKCE flow. It needs no client_id (public), uses S256, and the exchange
mints a normal OpenRouter API key — so no token-spoofing or vendor-backend gating
is involved (unlike ChatGPT/Claude subscriptions).

  - internal/provideroauth.OpenRouterLogin: binds a 127.0.0.1 loopback, opens/prints
    https://openrouter.ai/auth?callback_url=…&code_challenge=…&code_challenge_method=S256,
    captures the code, and POSTs it + the PKCE verifier to
    /api/v1/auth/keys → {key}. PKCE binds the code, so no separate CSRF state is
    needed. Injectable base URL / HTTP client / browser-opener for tests.
  - cli: `zero auth openrouter` runs the flow and prints the minted key with usage
    guidance. ZERO_OPENROUTER_BASE_URL overrides the endpoint (self-hosted/tests).

Tests: full flow over httptest (mint, missing-code, non-2xx exchange, empty key,
browser-error). Local run check via the real binary against a mock: the command
emits the correct PKCE authorize URL, the loopback callback is captured, and the
key is minted end-to-end.

First of the in-wizard OAuth providers; the wizard "Log in with OAuth" option
(next commit) calls this same flow and saves the key to the profile.

Gates: gofmt/vet/build(host+linux+windows)/test -race/staticcheck(no new)/
govulncheck(0)/deadcode(no new) all pass. No new module deps.
Adds an overridable preset layer to the env-driven OAuth registry so well-known
providers work without per-field env configuration. ResolveConfig now seeds each
field from a baked-in preset and lets the matching ZERO_OAUTH_<NAME>_* env var
override it (env wins); flow falls back to the preset then loopback.

First preset: xAI (Grok) — standard OIDC, PUBLIC client (no secret), loopback +
device. The access token is accepted directly as a bearer on api.x.ai/v1 (an
OpenAI-compatible endpoint), so the existing C1 wiring uses it for an xai provider
profile — no header/identity spoofing. `zero auth login xai` now works out of the
box. CAVEATS (documented in code + help): the client_id is a public Grok-CLI
client not formally documented by xAI (may change; override via env), and use
requires a SuperGrok / X Premium+ subscription.

Tests: preset resolution, env-overrides-preset (per field + flow), and that a
provider with neither preset nor env still errors. Local run check: `zero auth
login xai --device` (endpoints pointed at a mock, client_id NOT overridden) logs
in and the mock confirms the preset client_id b1a00492-… was sent.

Gates: gofmt/vet/build(host+linux+windows)/test/staticcheck(no new)/govulncheck(0)/
deadcode(no new) all pass. No new module deps.
Adds the credential-step OAuth login the setup wizard was missing. For a provider
that supports a usable browser OAuth flow (currently OpenRouter), the credential
screen now offers "ctrl+o to log in with OAuth in the browser (no key needed)"
alongside pasting a key.

Flow: ctrl+o → the wizard marks itself oauthPending and dispatches an async
tea.Cmd that runs provideroauth.OpenRouterLogin (opens the browser via the new
internal/browser opener, captures the loopback callback, mints a key). While it
runs the step shows "Opening your browser… (or run: zero auth openrouter)";
Esc cancels. On success the minted key fills the credential and the wizard
advances to model selection; on failure the redacted error is shown and the user
can retry or paste a key.

  - internal/browser: cross-platform OpenURL (open / xdg-open / rundll32) with a
    pure, unit-tested per-OS command builder.
  - provider_wizard: oauthPending/oauthErr state, providerWizardSupportsOAuth
    (OpenRouter only — ChatGPT/Claude use the proxy preset, others paste a key),
    the async cmd + result msg, and credential-step render/keys.
  - model.Update routes the OAuth result message.

Tests: openCommand per-OS; supportsOAuth (openrouter yes / openai no); ctrl+o
starts OAuth for OpenRouter and is a no-op otherwise; apply-success advances +
applies the key; apply-error stays + surfaces the message; render shows the hint
and the pending state. The interactive browser flow itself is the same one C3
verified end-to-end against a mock.

Gates: gofmt/vet/build(host+linux+windows)/test -race/staticcheck(no new — the
SA1019 mouse nits are pre-existing on origin/main)/govulncheck(0)/deadcode(no new).
No new module deps.
Document the two turnkey OAuth providers added in this branch (OpenRouter browser
key-mint via `zero auth openrouter` / wizard ctrl+o; xAI via `zero auth login
xai`), alongside the existing subscription-proxy and custom-provider guidance.
Reworks `/provider` setup to lead with a connect-method chooser, modeled on Codex
/ Claude Code / gemini-cli (verified via a UX survey), so OAuth is a first-class,
separate option instead of a key buried at the credential step.

New first step "How do you want to connect?":
  ❯ Sign in with OAuth                    (default when any OAuth provider exists)
      One-click browser login, no API key to copy (OpenRouter, xAI).
    Paste an API key / browse providers
      Any of 20+ providers, a local model, or a subscription via proxy.

  - OAuth path → a dedicated list of ONLY OAuth-capable providers
    (providercatalog.OAuthProviders()), each with a mode badge; selecting one
    runs the browser login (OpenRouter mints a key; xAI/preset providers store a
    refreshable token via the engine) and jumps straight to model selection,
    skipping the key/endpoint steps. A waiting screen shows progress + a CLI
    fallback; Esc cancels; errors are redacted and retryable.
  - Browse path → the existing full catalog → key/endpoint/model, unchanged.
  - ChatGPT/Claude are NOT offered as in-app OAuth (they can't work) — they remain
    reachable as the proxy/custom providers under "browse".

  - providercatalog: Descriptor gains OAuth/OAuthMintsKey/OAuthDeviceFlow; openrouter
    + xai are flagged; OAuthProviders() is the single source of truth.
  - wizard: new method step + OAuth provider list + per-provider OAuth dispatch
    (reuses provideroauth.OpenRouterLogin and the oauth engine login), waiting/
    render/keys/step-line/retreat all updated; mouse + keys ignored while a login
    is in flight.

Tests: catalog OAuth flags + OAuthProviders(); method chooser OAuth vs browse
paths; OAuth dispatch from the list; retreat→method; render shows the chooser +
provider list; existing wizard tests updated for the new first step. All tui tests
pass under -race.

Gates: gofmt/vet/build(host+linux+windows)/test/staticcheck(no new)/govulncheck(0)/
deadcode(no new) all pass. No new module deps.
…arding

First-run setup (onboarding.go) was a separate flow that still showed its bare
provider list. Add the same connect-method chooser so new users get the prominent
OAuth path too, keeping /provider and first-run as one mental model.

  - New setupStageMethod (after Welcome): "How do you want to connect?" with
    "Sign in with OAuth" (default) and "Paste an API key / browse providers".
  - OAuth path filters the provider list to OAuth-capable providers, runs the same
    browser login (OpenRouter mints a key → captured as the setup credential; xAI
    stores a refreshable token and the profile is named after the provider so the
    runtime resolver attaches the bearer), shows a waiting screen, then jumps to
    model selection (skipping endpoint/name/credentials). Esc cancels; errors are
    redacted.
  - Browse path is the existing flow, unchanged. setupStages()/navigation/back/
    progress/footer/mouse all updated; OAuth result routed via model.Update.

Tests: method chooser OAuth path (OAuth-only list + retreat clears it) and
apply-OAuth-success advances to model with the minted key; existing onboarding
tests updated for the new step (pressSetupContinue transparently skips it). All
tui tests pass under -race.

Gates: gofmt/vet/build(host+linux+windows)/test/staticcheck(no new)/govulncheck(0)/
deadcode(no new) all pass. No new module deps.
…entries

The "Sign in with OAuth" chooser listed only the real-OAuth providers (OpenRouter,
xAI), so ChatGPT and Claude looked missing. They can't do real in-app OAuth — a
subscription token is Cloudflare-gated (ChatGPT) / ToS-restricted for third-party
use (Claude) — so instead of hiding them or faking a login, list them honestly as
"subscription · needs a local proxy" entries that route into proxy setup.

  - catalog: new Descriptor.OAuthProxy flag + OAuthProxyProviders(); mark
    chatgpt-proxy and a new claude-proxy (local Anthropic-compatible, keyless)
    entry. localAnthropic() helper. Proxy entries are not OAuth (no fake login).
  - wizard + onboarding: the OAuth list now appends the proxy entries after the
    real OAuth providers (catalog order keeps real ones first). Selecting a proxy
    entry leaves OAuth mode and drops into the normal browse flow with the proxy
    provider selected and its default URL/model pre-filled, so the user just
    points Zero at the local proxy they run. No browser login is launched.
  - providerWizardNeedsEndpoint now true for proxy entries (the local port
    varies); faint badge "subscription · needs a local proxy".
  - docs/oauth-subscriptions.md: chooser now shows the four entries and explains
    the proxy routing.

Tests: catalog classification (OAuthProviders vs OAuthProxyProviders, flags);
wizard + onboarding proxy-entry routing (leaves OAuth mode, starts no login,
lands on endpoint with the URL pre-filled). Full suite green under -race;
staticcheck/govulncheck/deadcode clean. No new deps.
The OAuth chooser only did browser/loopback login, which can't work on a headless
or SSH box (no browser to open). Add device-code as a first-class option for
providers that support it (xAI).

  - oauth.Manager: PrepareDeviceLogin (request code) + CompleteDeviceLogin (poll +
    store), splitting the monolithic device flow so the UI can display the
    user_code + verification URI while it waits. CLI Login(Device:true) unchanged.
  - tui (shared oauth_device.go): oauthPreferDeviceFlow() picks device by default
    on SSH / headless Linux (no DISPLAY) or when ZERO_OAUTH_DEVICE is set;
    oauthDevicePrepare/Complete drive the two phases off the UI goroutine.
  - /provider wizard + first-run onboarding: device-capable providers show
    "Enter sign in · d device code" — Enter uses the browser (or device if
    headless), "d" forces device. The waiting screen shows the code + URL to
    enter elsewhere, then polls; success advances to model selection. Esc cancels;
    errors are redacted; device state is cleared on success/cancel/back.
  - docs: document the device-code path and ZERO_OAUTH_DEVICE.

Tests: manager Prepare/Complete two-phase; wizard + onboarding 'd' shortcut,
device-code display + poll dispatch, error surfacing, and success clearing device
state. Full suite green under -race; staticcheck/govulncheck/deadcode clean. No
new deps.
…st shows

After an xAI sign-in the bearer lives in the token store, not as a pasted key, so
the post-login model step ran /models with no credential → it always fell back to
the curated list (with a discovery error). Now the user picks from the real live
model list after OAuth, like other CLIs.

  - oauthStoredToken() resolves a fresh stored token for a token-login provider.
  - wizard + onboarding discovery resolve and inject that token into the discovery
    profile inside the async goroutine (token refresh is network I/O); OpenRouter
    still uses its minted key, API-key users are unchanged. Discovery timeout
    bumped 8s→12s to cover an on-demand refresh.
  - The resolved token is added to the redaction secret set so it can never leak
    into a surfaced discovery error. If discovery still fails (offline), the
    curated per-provider fallback list is shown as before — the user always gets a
    pickable list.

Tests: oauthStoredToken reads a seeded login / returns empty when absent;
wizard discovery injects the stored token into the /models profile. Hermetic via
ZERO_OAUTH_TOKENS_PATH. Full suite green under -race; staticcheck/govulncheck/
deadcode clean. No new deps.
…+ xAI only

Reverts the proxy-entry listing: the "Sign in with OAuth" chooser now shows only
the providers that do real OAuth (OpenRouter, xAI). ChatGPT/Claude looked
confusing there since they can't actually sign in — they remain reachable via
"Paste an API key / browse providers" + the local-proxy recipe in
docs/oauth-subscriptions.md.

  - catalog: remove the Descriptor.OAuthProxy flag, OAuthProxyProviders(), the
    oauthProxyProvider/localAnthropic helpers, and the claude-proxy entry.
    chatgpt-proxy stays as a plain browse/config entry (its pre-existing role).
  - wizard + onboarding: OAuth list = OAuthProviders() again; removed the
    proxy→browse routing, the OAuthProxy badge, and the OAuthProxy endpoint rule.
  - device-code + OAuth-token model discovery (the later features) are untouched.
  - docs + tests updated to the two-entry chooser.

Gates: gofmt/vet/build(host+linux+windows)/test -race/staticcheck/deadcode all
clean. No new deps.
@github-actions

github-actions Bot commented Jun 15, 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: ecf38f6cc030
Changed files (37): docs/oauth-subscriptions.md, internal/browser/open.go, internal/browser/open_test.go, internal/cli/app.go, internal/cli/auth.go, internal/cli/auth_test.go, internal/cli/oauth_credential_test.go, internal/cli/oauth_provider.go, internal/cli/setup.go, internal/oauth/manager.go, internal/oauth/manager_test.go, internal/oauth/oauth.go, and 25 more

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

@coderabbitai

coderabbitai Bot commented Jun 15, 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: ec0817ef-2cc7-4920-9446-f2214d58370e

📥 Commits

Reviewing files that changed from the base of the PR and between 8ec599b and ecf38f6.

📒 Files selected for processing (5)
  • internal/oauth/presets.go
  • internal/oauth/presets_test.go
  • internal/provideroauth/openrouter.go
  • internal/tui/onboarding.go
  • internal/tui/onboarding_test.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • internal/oauth/presets_test.go
  • internal/oauth/presets.go
  • internal/provideroauth/openrouter.go
  • internal/tui/onboarding_test.go
  • internal/tui/onboarding.go

Walkthrough

Adds comprehensive OAuth provider authentication to Zero: an ErrNoToken sentinel and RFC 8628 device-code staging in the oauth engine; a TokenResolver callback and SendWithAuthRetry helper in providerio that resolves OAuth bearer tokens with automatic 401 refresh; baked-in OAuth presets with env-var fallback for xAI; provider catalog OAuth capability flags and a new chatgpt-proxy entry; OpenRouter PKCE loopback login; OAuth resolver wiring through OpenAI and Anthropic providers; CLI OAuth setup (zero auth openrouter) with per-profile token resolution and updated onboarding eligibility checks; TUI device-code helpers with stored token retrieval; provider wizard and first-run onboarding method-chooser stages supporting both browser and device-code OAuth flows; and reference documentation for OAuth subscription patterns.

Changes

OAuth Provider Authentication

Layer / File(s) Summary
OAuth engine: ErrNoToken sentinel and device-code stages
internal/oauth/oauth.go, internal/oauth/manager.go, internal/oauth/manager_test.go, internal/oauth/scheduler.go
ErrNoToken sentinel added; PrepareDeviceLogin resolves provider config and requests device code; CompleteDeviceLogin polls with deadline and persists token; token-loading paths refactored to delay provider-config resolution until refresh needed; two-phase split supports async UI flows; scheduler updated to use new loadToken helper.
OAuth presets and ResolveConfig fallback
internal/oauth/presets.go, internal/oauth/presets_test.go, internal/oauth/providers.go
builtinOAuthPresets map with xai preset (client ID, endpoints, scopes, flow); lookupOAuthPreset and scopesOrPreset helpers; ResolveConfig merges env vars with preset fields, defaults FLOW to preset or loopback; five test cases cover preset usage, env override, missing config, and opt-in gating.
TokenResolver and SendWithAuthRetry with 401 refresh
internal/providers/providerio/auth.go, internal/providers/providerio/auth_test.go
New TokenResolver callback type returns OAuth header/value or ok=false for API-key fallback; SendWithAuthRetry applies resolver, retries once with forceRefresh=true on 401, closes response bodies; withBearer helper clears API-key fields; seven test cases cover fallback, bearer selection, refresh, and error propagation.
Provider catalog OAuth flags and chatgpt-proxy entry
internal/providercatalog/catalog.go, internal/providercatalog/catalog_test.go, internal/providercatalog/oauth_test.go
Descriptor gains OAuth, OAuthMintsKey, OAuthDeviceFlow booleans; openrouter and xai marked via oauthProvider helper; chatgpt-proxy local provider entry added with documentation; OAuthProviders() exported function filters and returns cloned descriptors; four test functions validate classification and independence.
OpenRouter PKCE loopback login
internal/provideroauth/openrouter.go, internal/provideroauth/openrouter_test.go
OpenRouterLogin starts local callback server, builds PKCE challenge, opens browser with authorization URL, waits for code, calls openRouterExchange to POST code+verifier and extract minted API key; test includes simulateBrowser helper and five error/success cases.
OAuthResolver wiring through providers
internal/providers/openai/provider.go, internal/providers/anthropic/provider.go, internal/providers/factory.go
OAuthResolver field added to Options/Provider in both providers; streaming paths switch from SendWithRetry + manual auth to SendWithAuthRetry with resolver and AuthHeaders; factory imports providerio and passes resolver to constructors.
CLI OAuth resolver and zero auth openrouter subcommand
internal/cli/oauth_provider.go, internal/cli/auth.go, internal/cli/app.go, internal/cli/setup.go, internal/cli/oauth_credential_test.go, internal/cli/auth_test.go
oauthResolverForProfile builds per-profile TokenResolver from stored tokens with 401 refresh fallback; wired into defaultAppDeps; runAuthOpenRouter performs PKCE login and prints API key; setupRequired recognizes OAuth logins and suppresses onboarding when token exists; three test functions validate credential detection, setup eligibility, and command-line argument handling.
Cross-platform browser launcher
internal/browser/open.go, internal/browser/open_test.go
OpenURL selects open/rundll32/xdg-open by platform, starts process without blocking, asynchronously reaps launcher; table-driven test covers darwin/linux/freebsd/windows.
TUI OAuth device-code helpers
internal/tui/oauth_device.go, internal/tui/oauth_device_test.go
oauthPreferDeviceFlow checks env/SSH/headless heuristics; oauthDevicePrepare constructs manager with 30s timeout, oauthDeviceComplete polls with 10m timeout, oauthStoredToken retrieves fresh token, oauthDeviceVerifyTarget selects best verification URI; two test functions cover stored-token retrieval and discovery token injection.
Provider wizard OAuth method chooser and flows
internal/tui/provider_wizard.go, internal/tui/provider_wizard_discovery.go, internal/tui/provider_wizard_oauth_test.go, internal/tui/provider_wizard_test.go, internal/tui/mouse.go, internal/tui/mouse_test.go, internal/tui/model.go
Wizard gains method-chooser step (OAuth vs browse); oauthPending blocks mouse/key except Esc; d/ctrl+o shortcuts trigger device/OAuth login; step labels and OAuth waiting screen rendered conditionally; OAuth badges shown on providers; discovery resolves stored tokens for token-login providers; fourteen test functions and two helpers cover navigation and state transitions.
First-run onboarding OAuth method and device-code
internal/tui/onboarding.go, internal/tui/onboarding_test.go
setupState gains OAuth/device-code fields; method stage added before provider selection; advanceSetup branches for browser/device-code OAuth; save/discovery skip API-key storage for token-login providers; OAuth waiting screen and device-code hints rendered; seven test functions cover method routing, device login, and OAuth success/error handling.
OAuth subscriptions reference documentation
docs/oauth-subscriptions.md
Documents built-in OIDC provider login (OpenRouter/xAI wizard flow, device-code), local-proxy pattern for ChatGPT/Claude subscriptions with config examples (chatgpt-proxy, custom-anthropic-compatible), and non-proxy alternatives (API keys, claude CLI).

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant TUIWizard as TUI Provider Wizard
  participant OpenRouterLogin
  participant LocalCallbackServer as Local Callback Server
  participant OpenRouterAPI as OpenRouter API
  participant TokenStore

  User->>TUIWizard: Select "Sign in with OAuth" → openrouter
  TUIWizard->>OpenRouterLogin: OpenRouterLogin(ctx, opts)
  OpenRouterLogin->>LocalCallbackServer: Start 127.0.0.1 listener
  OpenRouterLogin->>User: Print/open authorization URL (PKCE challenge)
  User->>OpenRouterAPI: Authorize in browser
  OpenRouterAPI->>LocalCallbackServer: Redirect /callback?code=...
  LocalCallbackServer-->>OpenRouterLogin: code received
  OpenRouterLogin->>OpenRouterAPI: POST code + PKCE verifier → openRouterExchange
  OpenRouterAPI-->>OpenRouterLogin: { "key": "sk-or-..." }
  OpenRouterLogin-->>TUIWizard: API key string
  TUIWizard->>TokenStore: Save provider:openrouter
  TUIWizard-->>User: Advance to model selection
Loading
sequenceDiagram
  participant User
  participant TUIWizard as TUI Provider Wizard
  participant OAuthManager as OAuth Manager
  participant xAIProvider as xAI Device Endpoint
  participant TokenStore
  participant UpstreamAPI as xAI Inference API

  User->>TUIWizard: Press "d" for device-code login (xAI)
  TUIWizard->>OAuthManager: PrepareDeviceLogin
  OAuthManager->>xAIProvider: Request device code
  xAIProvider-->>OAuthManager: UserCode, VerificationURI
  OAuthManager-->>TUIWizard: DeviceAuth, Config
  TUIWizard-->>User: Show UserCode + VerificationURI
  User->>xAIProvider: Authorize at VerificationURI
  TUIWizard->>OAuthManager: CompleteDeviceLogin (poll)
  OAuthManager->>xAIProvider: Poll for token
  xAIProvider-->>OAuthManager: access token
  OAuthManager->>TokenStore: Save provider:xai
  TUIWizard-->>User: Advance to model selection
  User->>UpstreamAPI: Request (SendWithAuthRetry resolves stored token → Bearer header)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • Gitlawb/zero#210: This PR builds on the oauth engine by adding PrepareDeviceLogin/CompleteDeviceLogin split and ErrNoToken sentinel, extending the manager with two-phase RFC 8628 support.
  • Gitlawb/zero#163: Both PRs modify the same first-run setup/onboarding codepaths (internal/cli/setup.go, internal/tui/onboarding.go)—this PR extends that eligibility/UI to support OAuth logins.
  • Gitlawb/zero#161: This PR extends the provider wizard OAuth flow and device-code branches introduced in the retrieved PR, adding OAuth connect-method handling on top of existing provider setup UX.

Suggested reviewers

  • Vasanthdev2004
✨ 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 feat/oauth-provider-login

@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: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/tui/onboarding.go (1)

1539-1547: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Show OAuth token login instead of an env var on the Ready screen.

For OAuth providers that do not mint API keys, completeSetup intentionally stores no API key, but this summary falls through to env var XAI_API_KEY. Make the summary OAuth-aware so the final confirmation matches the credential that will actually be used.

Suggested fix
 func (m model) setupCredentialSummary(option SetupProviderOption) string {
+	if m.setup.oauthMode && m.setupProviderDescriptor().OAuth && !m.setupProviderDescriptor().OAuthMintsKey {
+		return "OAuth token"
+	}
 	if !setupProviderAcceptsAPIKey(option) {
 		return "not required"
 	}
🤖 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/tui/onboarding.go` around lines 1539 - 1547, The
setupCredentialSummary function currently falls through to displaying an env var
message for OAuth providers that don't mint API keys, but it should instead show
an OAuth token login message to match what will actually be used. Add a check
after the setupProviderAcceptsAPIKey condition to detect if the provider uses
OAuth. If the provider uses OAuth (and there's no saved API key), return a
message indicating OAuth token login instead of allowing the function to fall
through to the env var case. This ensures the summary accurately reflects the
actual credential method that will be used during setup.
🧹 Nitpick comments (3)
internal/provideroauth/openrouter.go (1)

134-171: 💤 Low value

Minor: use bytes.NewReader to avoid string conversion.

Line 144 converts payload (already a []byte) to string just to wrap it in strings.NewReader. Using bytes.NewReader(payload) avoids the allocation.

Suggested change
-	request, err := http.NewRequestWithContext(ctx, http.MethodPost, base+openRouterKeyExchangePath, strings.NewReader(string(payload)))
+	request, err := http.NewRequestWithContext(ctx, http.MethodPost, base+openRouterKeyExchangePath, bytes.NewReader(payload))

Add "bytes" to imports if not present.

🤖 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/provideroauth/openrouter.go` around lines 134 - 171, In the
openRouterExchange function, the payload variable (which is already a byte slice
from json.Marshal) is being unnecessarily converted to a string before being
passed to strings.NewReader. Replace strings.NewReader(string(payload)) with
bytes.NewReader(payload) in the http.NewRequestWithContext call to avoid the
inefficient allocation. Ensure the bytes package is imported at the top of the
file if it is not already present.
internal/oauth/presets.go (1)

49-54: ⚡ Quick win

Defensively copy preset scopes before returning.

Line 53 returns the shared preset slice. Any downstream mutation of cfg.Scopes can mutate global preset defaults for later resolutions.

Proposed fix
 func scopesOrPreset(envScopes string, preset []string) []string {
 	if fields := strings.Fields(envScopes); len(fields) > 0 {
 		return fields
 	}
-	return preset
+	return append([]string(nil), preset...)
 }
🤖 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/oauth/presets.go` around lines 49 - 54, The scopesOrPreset function
returns the preset slice directly on line 53, which allows downstream code to
mutate the original shared preset defaults. Create a defensive copy of the
preset slice before returning it by using a mechanism like append or
slices.Clone to ensure that any mutations to the returned slice do not affect
the original preset.
internal/oauth/providers.go (1)

31-34: ⚡ Quick win

Update Registry docs to match current behavior.

This comment says there are no built-ins, but ResolveConfig now applies lookupOAuthPreset(name) defaults. Keeping this stale is misleading for operators and reviewers.

🤖 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/oauth/providers.go` around lines 31 - 34, The Registry comment
states there are no built-in providers or OAuth client identities, but this is
no longer accurate since ResolveConfig now applies built-in preset defaults via
the lookupOAuthPreset function. Update the Registry comment to correctly reflect
that while providers are defined entirely via ZERO_OAUTH_<NAME>_* variables, the
system does include built-in OAuth preset defaults that are applied during
configuration resolution through ResolveConfig, ensuring the documentation
accurately represents the actual behavior.
🤖 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 `@docs/oauth-subscriptions.md`:
- Around line 39-42: The code fences containing the mock UI examples are missing
language tags, which causes markdownlint validation to fail and reduces code
clarity. Add the `text` language tag to both code fence blocks (the opening ```
should become ```text) for the two UI mockup sections showing the OAuth sign-in
chooser and the API key/provider options. This applies to the example fence
shown and the sibling fence mentioned in the comment at lines 46-49.
- Around line 86-108: The Anthropic (Claude) section in the documentation
outdated references Anthropic's February 2026 policy as permanently prohibitive
with account bans, but this policy evolved in April 2026 to permit third-party
OAuth usage under a separate pay-as-you-go billing model rather than deducting
from subscription quota. Update the Anthropic bullet point to clarify that the
February 2026 enforcement phase and account disruptions were real, but explain
that Anthropic's stance shifted in April 2026 to allow third-party usage with
distinct billing rather than outright prohibition. Remove or revise the language
stating the policy "explicitly prohibits" subscription-token use and "has
actively enforced it," and instead document that current usage is permitted but
billed separately from the subscription.

In `@internal/oauth/manager.go`:
- Around line 140-164: The PrepareDeviceLogin and CompleteDeviceLogin methods
currently pass the incoming context directly to RequestDeviceCode and
PollDeviceToken respectively without applying a default timeout, which can cause
indefinite hangs on network stalls when context.Background() is passed. Apply a
default timeout to the context parameter in both methods (similar to how the
Login method handles timeouts) before passing it to RequestDeviceCode in
PrepareDeviceLogin and PollDeviceToken in CompleteDeviceLogin to preserve
timeout guarantees.

In `@internal/providercatalog/catalog.go`:
- Around line 262-270: The OAuthProviders() function appends descriptors
directly to the output slice, which differs from other accessor functions like
All(), Get(), and ListByTransport() that use cloneDescriptor() to protect
internal catalog state. To fix this, apply cloneDescriptor() to each descriptor
in the OAuthProviders() function before appending it to the out slice, matching
the pattern used in the other accessor functions. This ensures that callers
cannot mutate shared slice fields like AuthEnvVars, SupportedAPIFormats, and
Aliases.

In `@internal/tui/onboarding.go`:
- Around line 496-498: The OAuth handling logic in internal/tui/onboarding.go
does not validate that incoming OAuth results match the current provider being
set up, allowing stale results from a cancelled provider to be incorrectly
applied when the user starts setup for a different provider. Add a correlation
check comparing msg.providerID against m.setupProviderDescriptor().ID before
processing device-code or final OAuth messages at both locations: the check at
lines 496-498 and the check at lines 514-516. Only apply the OAuth message if
the provider IDs match, preventing mismatched credentials from advancing the
wrong provider's setup flow.
- Around line 499-503: The issue is that OAuth error messages are stored in
setup.oauthErr but this field is only displayed when oauthPending is true. When
an error occurs in the code block that handles msg.err, both oauthPending and
oauthDevice are set to false, which causes setupOAuthWaitingLines to stop
rendering and the user sees the provider list without any error indication. To
fix this, copy the redacted error message from msg.err into setup.err (in
addition to or instead of storing it in oauthErr) so the error is displayed on
the provider selection step. Apply this same fix to the other affected location
where OAuth errors are handled (referenced in the comment as also appearing at
lines 518-520), ensuring consistent error surfacing across all OAuth failure
paths.
- Around line 575-580: The OAuth method selection in the setupStageMethod block
can result in an empty provider list. When m.setup.selectedMethod references an
OAuth option from providerWizardMethodOptions(), the subsequent call to
setupOAuthProviderOptions(m.setup.allProviders) may return an empty list, yet
the code still sets m.setup.oauthMode = true and proceeds with advanceSetup. Fix
this by adding a guard after the oauth flag is set: check if the resulting
provider list from setupOAuthProviderOptions is empty, and if so, either display
an error message and prevent advancement, or revert to API-key setup mode
instead. Apply the same guard logic at the second location where this code
pattern occurs.

---

Outside diff comments:
In `@internal/tui/onboarding.go`:
- Around line 1539-1547: The setupCredentialSummary function currently falls
through to displaying an env var message for OAuth providers that don't mint API
keys, but it should instead show an OAuth token login message to match what will
actually be used. Add a check after the setupProviderAcceptsAPIKey condition to
detect if the provider uses OAuth. If the provider uses OAuth (and there's no
saved API key), return a message indicating OAuth token login instead of
allowing the function to fall through to the env var case. This ensures the
summary accurately reflects the actual credential method that will be used
during setup.

---

Nitpick comments:
In `@internal/oauth/presets.go`:
- Around line 49-54: The scopesOrPreset function returns the preset slice
directly on line 53, which allows downstream code to mutate the original shared
preset defaults. Create a defensive copy of the preset slice before returning it
by using a mechanism like append or slices.Clone to ensure that any mutations to
the returned slice do not affect the original preset.

In `@internal/oauth/providers.go`:
- Around line 31-34: The Registry comment states there are no built-in providers
or OAuth client identities, but this is no longer accurate since ResolveConfig
now applies built-in preset defaults via the lookupOAuthPreset function. Update
the Registry comment to correctly reflect that while providers are defined
entirely via ZERO_OAUTH_<NAME>_* variables, the system does include built-in
OAuth preset defaults that are applied during configuration resolution through
ResolveConfig, ensuring the documentation accurately represents the actual
behavior.

In `@internal/provideroauth/openrouter.go`:
- Around line 134-171: In the openRouterExchange function, the payload variable
(which is already a byte slice from json.Marshal) is being unnecessarily
converted to a string before being passed to strings.NewReader. Replace
strings.NewReader(string(payload)) with bytes.NewReader(payload) in the
http.NewRequestWithContext call to avoid the inefficient allocation. Ensure the
bytes package is imported at the top of the file if it is not already present.
🪄 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: fda16d90-a22d-461e-aeb8-09c3f9a2ddb4

📥 Commits

Reviewing files that changed from the base of the PR and between e1253c8 and 6b6a47d.

📒 Files selected for processing (32)
  • docs/oauth-subscriptions.md
  • internal/browser/open.go
  • internal/browser/open_test.go
  • internal/cli/app.go
  • internal/cli/auth.go
  • internal/cli/oauth_provider.go
  • internal/oauth/manager.go
  • internal/oauth/manager_test.go
  • internal/oauth/presets.go
  • internal/oauth/presets_test.go
  • internal/oauth/providers.go
  • internal/providercatalog/catalog.go
  • internal/providercatalog/catalog_test.go
  • internal/providercatalog/oauth_test.go
  • internal/provideroauth/openrouter.go
  • internal/provideroauth/openrouter_test.go
  • internal/providers/anthropic/provider.go
  • internal/providers/factory.go
  • internal/providers/openai/provider.go
  • internal/providers/providerio/auth.go
  • internal/providers/providerio/auth_test.go
  • internal/tui/model.go
  • internal/tui/mouse.go
  • internal/tui/mouse_test.go
  • internal/tui/oauth_device.go
  • internal/tui/oauth_device_test.go
  • internal/tui/onboarding.go
  • internal/tui/onboarding_test.go
  • internal/tui/provider_wizard.go
  • internal/tui/provider_wizard_discovery.go
  • internal/tui/provider_wizard_oauth_test.go
  • internal/tui/provider_wizard_test.go

Comment thread docs/oauth-subscriptions.md Outdated
Comment thread internal/oauth/manager.go
Comment thread internal/providercatalog/catalog.go
Comment thread internal/tui/onboarding.go
Comment thread internal/tui/onboarding.go
Comment thread internal/tui/onboarding.go
A provider authenticated via OAuth (e.g. `zero auth login xai`) has no inline
key or env var, so setupRequired treated it as unconfigured and forced onboarding
on every launch — and the active provider could be passed over for a different
one. setupRequired now also counts a stored OAuth login (read from the token
store) as a credential, so an OAuth-logged-in provider stays usable without
onboarding.

Tests: providerHasOAuthLogin (by name/catalog id); setupRequired returns false
for an OAuth-logged-in provider, true for a keyless one with no login.

@Vasanthdev2004 Vasanthdev2004 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 🛑

Reviewed locally in worktree review-pr217 at d054120. The PR adds OAuth login for OpenRouter + xAI, a subscription-proxy recipe, and a wizard UI. gofmt/vet/build clean. All relevant test packages pass — internal/oauth (9.6s), internal/provideroauth (2.3s), internal/providercatalog (0.9s), internal/tui (4.4s). Full suite shows only the pre-existing TestRunExecUsesProjectConfigAndOpenAICompatibleProvider failure (not this PR).

The OpenRouter flow and the docs are clean. The xAI preset, however, breaks the engine's security invariant and must be removed before this lands.

Blocker: hardcoded xAI client_id in internal/oauth/presets.go

var builtinOAuthPresets = map[string]providerPreset{
    "xai": {
        ClientID: "b1a00492-073a-47ea-816f-4c329264a828",
        ...
    },
}

internal/oauth/oauth.go (the package doc) states explicitly:

This package holds NO vendor secrets: provider client IDs and any overridable endpoints come from config/env, so no third-party OAuth client identity is baked into the binary.

This PR violates that invariant. The fact that the value is documented in the preset comment as "public" doesn't change the design intent — the engine was deliberately built to keep third-party OAuth client identities out of the binary. The fact that the preset is "overridable via env" also doesn't help: the default still gets baked in, and any user who runs zero auth login xai with no ZERO_OAUTH_XAI_CLIENT_ID will silently authenticate as b1a00492-....

The PR body says xAI's flow needs that specific client_id and that it's known to be public, but the engine has a documented rule against this. Either:

  1. Remove the xAI preset and document that xAI users must set ZERO_OAUTH_XAI_CLIENT_ID (and any other required endpoints) themselves — same posture every other provider gets. The wizard's "Sign in with OAuth" chooser for xAI can then prompt for the client_id if it's missing.
  2. Add a runtime opt-in (e.g. ZERO_OAUTH_ALLOW_PRESETS=1) that gates the preset map entirely, so a user explicitly opts in to third-party client identities being baked in.

Option 1 is the cleaner fix and is consistent with the package invariant.

Other observations (informational, can stay as follow-ups)

  • The validateTokenEndpoint naming smell I flagged on #210 is re-introduced here (providers.go calls it on cfg.IssuerURL / cfg.AuthorizationEndpoint). Cosmetic; can be a single rename PR across #210/#216/#217.
  • The OpenRouter flow (internal/provideroauth/openrouter.go) is exemplary — no client_id needed (the OpenRouter flow is designed that way), PKCE S256, loopback 127.0.0.1, body cap 1 MiB, no key material in error messages. The two new tests (openrouter_test.go) cover the happy path, the missing-code callback, the empty-key response, and the non-2xx exchange.
  • The internal/browser/open.go opener is a clean cross-platform abstraction (xdg-open / open / rundll32 url.dll,FileProtocolHandler).
  • The internal/providers/providerio/auth.go helper for OpenAI/Anthropic Authorization: Bearer plumbing is small and correct — uses the existing oauth.Manager.GetFresh with a 401-recovery path.
  • The setup wizard changes are well-scoped: a "How do you want to connect?" chooser, OAuth card and API-key card are mutually exclusive, the device-code path prints the URL and the user_code on separate lines.

Overall

The architecture is sound, the OpenRouter design is correct, the TUI wizard is tasteful. The only reason this isn't an approve is the hardcoded xAI client_id in presets.go — that single line breaks the package invariant the OAuth engine was designed to uphold. Once it's removed (option 1 above) or gated behind a runtime opt-in, this is a clean approve.

🤖 Generated with Claude Code

Resolve CodeRabbit review comments on this PR:

- PrepareDeviceLogin: self-bound the network prepare (endpoint discovery +
  device-code request) with opts.Timeout (default 5m) like Login, so a caller
  passing an unbounded context still gets the timeout guarantee.
- CompleteDeviceLogin: bound the poll by the device code's own ExpiresAt so an
  unbounded caller context cannot leave an in-flight request hanging past the
  code's lifetime (PollDeviceToken already re-checks between polls).
- OAuthProviders(): return cloned descriptors, matching All()/the other
  accessors, so callers can't mutate the shared catalog backing slices.
- Onboarding applySetupOAuth / applySetupOAuthDeviceCode: ignore a stale result
  for a provider the user has since switched away from, so a late login can't
  capture a credential against the wrong provider.
- Onboarding: surface a failed OAuth login on the provider list the user lands
  on (the waiting screen that owned the error is gone once oauthPending clears),
  so the failure isn't silent and the user can retry.
- Onboarding: hide the "Sign in with OAuth" method when this setup's own
  provider list has no OAuth-capable entry, so it can't be selected into an
  empty provider list (setupMethodOptions).
- docs/oauth-subscriptions.md: label the bare example fences (text) and update
  the Anthropic policy section for the April 4 2026 third-party cutoff.

Adds tests for the descriptor-clone isolation and the method-option filtering.
The xAI preset baked a third-party OAuth client_id into the default
credential path, which breaks the engine's documented invariant that no
third-party OAuth client identity is used unless the operator configures
it. Rather than drop the convenience entirely, make presets opt-in:

- ResolveConfig only consults builtinOAuthPresets when ZERO_OAUTH_ALLOW_PRESETS
  is truthy (presetsAllowed); by default every field must come from
  ZERO_OAUTH_<NAME>_* env, so the binary's preset client_id is never used
  without an explicit opt-in.
- The "not configured" error now points the user at ZERO_OAUTH_ALLOW_PRESETS
  when a preset exists, so the wizard/CLI failure is self-documenting.
- Refresh the package, Registry, and preset docs to describe the opt-in;
  env still overrides any preset field.
- docs/oauth-subscriptions.md: document the flag for one-click xAI sign-in.

Keeps both: invariant-clean by default, one-click xAI for opted-in users.
Tests: preset resolution now requires the opt-in; added a gate-off test
asserting xAI stays inert (and the error names the flag) without it.
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough reviews — pushed aade9e7 + 2b5b0ad addressing everything.

Blocker — hardcoded xAI client_id (presets.go): made the built-in presets opt-in rather than removing them (your option 2). ResolveConfig only consults builtinOAuthPresets when ZERO_OAUTH_ALLOW_PRESETS is truthy; by default every field comes from ZERO_OAUTH_<NAME>_* env, so no third-party OAuth client identity is used unless the operator explicitly opts in. The "not configured" error now names the flag, and the package / Registry / preset docs + docs/oauth-subscriptions.md were updated. Env still overrides any preset field.

CodeRabbit:

  • PrepareDeviceLogin self-bounds the prepare (discovery + device-code request) with opts.Timeout, matching Login; CompleteDeviceLogin bounds the poll by the device code's ExpiresAt.
  • OAuthProviders() returns cloned descriptors (matches All()).
  • Onboarding ignores a stale OAuth result for a provider the user has switched away from (applySetupOAuth + applySetupOAuthDeviceCode).
  • A failed OAuth login is surfaced on the provider list the user lands on (the waiting screen that owned the error is gone once oauthPending clears).
  • "Sign in with OAuth" is hidden when a setup's own provider list has no OAuth entry (setupMethodOptions).
  • docs: labeled the example fences; updated the Anthropic section for the April 4 2026 third-party cutoff.

Added tests for the preset opt-in gate, the descriptor-clone isolation, and the method-option filtering. gofmt / vet / build (host+linux+windows) / go test -race are green.

Left the validateTokenEndpoint naming as the cross-PR rename follow-up you flagged.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

@gnanam1990 Thanks for the detailed summary — the opt-in gate for presets (ZERO_OAUTH_ALLOW_PRESETS) is a clean resolution to the hardcoded client identity concern. Triggering the review now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit rereview

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

@gnanam1990 Triggering a full re-review now.

✅ Action performed

Full review finished.

GetFresh resolved the provider OAuth config for every read via loadForKey,
even when the stored token was still valid and no refresh would happen.
With presets gated behind ZERO_OAUTH_ALLOW_PRESETS, that made a stored xAI
login unreadable (and stalled the proactive refresh scheduler) unless the
opt-in flag was set at read time — config that is only needed to refresh.

Split loadForKey into loadToken (token only) and resolveConfigForKey
(refresh-only). GetFresh now returns a still-valid token without touching
the config; only an actual refresh (or Handle401) resolves endpoints +
client. The scheduler's load uses loadToken too, so proactive refresh
survives a gated preset and only the refresh attempt needs the config.

Fixes the Smoke failures TestOAuthStoredTokenReadsStoredLogin and
TestProviderWizardDiscoveryUsesOAuthToken (a fresh stored token must be
readable without the preset opt-in).

@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: 7

🧹 Nitpick comments (1)
internal/oauth/presets.go (1)

68-73: ⚡ Quick win

Return a copy of preset scopes to avoid shared mutable state.

On Line 72, scopesOrPreset returns the preset slice directly. That aliases global preset memory; any in-place mutation of returned Config.Scopes can leak into future resolutions. Return a cloned slice for both branches.

♻️ Proposed change
 func scopesOrPreset(envScopes string, preset []string) []string {
 	if fields := strings.Fields(envScopes); len(fields) > 0 {
-		return fields
+		return append([]string(nil), fields...)
 	}
-	return preset
+	return append([]string(nil), preset...)
 }
🤖 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/oauth/presets.go` around lines 68 - 73, The scopesOrPreset function
returns the preset slice directly on the return statement, which creates an
alias to global memory and allows in-place mutations to leak into future
resolutions. To fix this, modify the function to return a clone or copy of the
preset slice instead of the preset reference itself. Keep the branch that
returns fields from strings.Fields unchanged, as it already returns a local
slice. Only the preset return statement needs to be modified to return a cloned
copy of the preset slice.
🤖 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 `@docs/oauth-subscriptions.md`:
- Around line 90-116: The Anthropic section incorrectly presents the February
2026 enforcement policy as the current permanent stance. Update the text around
the "explicitly prohibits" statement and "no longer cover third-party-tool
usage" passages to: (1) clearly mark February 2026 language as historical
enforcement that occurred at that time, (2) introduce Anthropic's April 4, 2026
policy pivot that now permits third-party OAuth usage but routes it through
separate billing (metered usage or bundles) rather than subscription quota, and
(3) replace prohibitive language with explanatory language about billing
separation so a June 2026 reader understands the current permission model, not
outdated restrictions. Retain the February enforcement details as evidence of
the policy evolution, but restructure the narrative to distinguish past
enforcement from present capability.

In `@internal/cli/auth.go`:
- Around line 358-361: The help text in the auth.go file currently describes
OAuth provider presets as directly usable examples (e.g., 'zero auth login
xai'), but preset usage is actually gated behind the ZERO_OAUTH_ALLOW_PRESETS
environment variable. Update the help text around lines 358-361 to clarify that
presets like "xai" and "openrouter" require setting the ZERO_OAUTH_ALLOW_PRESETS
environment variable before they can be used, so users understand the actual
precondition rather than attempting a command that will fail.
- Around line 50-56: The runAuthOpenRouter function ignores unexpected arguments
after checking for help flags, causing typos or unsupported flags to silently
proceed instead of failing fast. After the help flag check loop in
runAuthOpenRouter, add validation to reject any unexpected arguments by checking
if args contains elements that are not recognized as valid options, then return
a non-success exit code and write an error message to stderr indicating that
unexpected arguments were provided and should not be used with the openrouter
auth command.

In `@internal/providers/providerio/auth.go`:
- Around line 41-57: The resolver function is being called inside the callback
passed to SendWithRetry, which means SendWithRetry will still execute and send
the HTTP request even if the resolver returns an error. Move the auth header
resolution logic (the resolver call) outside and before the SendWithRetry
invocation so that the auth headers are resolved first, and return immediately
from the current function if resolver returns an error, rather than capturing
the error and allowing SendWithRetry to proceed with an unauthenticated request.
This ensures the request is never dispatched when token resolution fails.

In `@internal/tui/mouse.go`:
- Around line 423-425: The selectProviderWizardAtMouse function blocks
click-based provider selection when oauthPending is true, but wheel-scroll
interactions bypass this check and still mutate provider state at other
locations (referenced as Line 73 and Line 104 branches), allowing unwanted state
changes during pending OAuth. Extend the oauthPending check to cover all
provider-wizard mouse interaction paths, including scroll wheel handlers, so
that all mouse-based provider mutations are blocked whenever OAuth is in pending
state. Ensure that any code paths handling mouse wheel events for provider
selection also respect the oauthPending condition before allowing state
mutations.

In `@internal/tui/onboarding_test.go`:
- Around line 93-94: The test sets selectedMethod using
providerWizardMethodOptions() to compute an index, but the actual setup flow
uses filtered options via setupMethodOptions() which can exclude OAuth. This
mismatch causes invalid indices when OAuth is hidden. Replace the call to
providerWizardMethodOptions() with setupMethodOptions() when computing the
selectedMethod index to ensure consistency between the test and the actual setup
implementation. Apply this same fix at all other locations where
providerWizardMethodOptions() is used for setting selectedMethod in the setup
flow.

In `@internal/tui/provider_wizard_oauth_test.go`:
- Around line 166-170: The loop in lines 166-170 that searches for the
"openrouter" provider and sets it as selectedProvider lacks verification that
the provider was actually found. If "openrouter" is missing or renamed, the loop
completes without setting selectedProvider, but the test continues without
detecting this failure. Add an assertion after the loop to verify that the
"openrouter" provider was found and selectedProvider was set to a valid index,
ensuring the test fails immediately if the expected provider is not present
instead of silently testing the wrong provider.

---

Nitpick comments:
In `@internal/oauth/presets.go`:
- Around line 68-73: The scopesOrPreset function returns the preset slice
directly on the return statement, which creates an alias to global memory and
allows in-place mutations to leak into future resolutions. To fix this, modify
the function to return a clone or copy of the preset slice instead of the preset
reference itself. Keep the branch that returns fields from strings.Fields
unchanged, as it already returns a local slice. Only the preset return statement
needs to be modified to return a cloned copy of the preset slice.
🪄 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: 5998b1ec-87cc-4c8d-8900-32bb795d94e0

📥 Commits

Reviewing files that changed from the base of the PR and between e1253c8 and 2b5b0ad.

📒 Files selected for processing (35)
  • docs/oauth-subscriptions.md
  • internal/browser/open.go
  • internal/browser/open_test.go
  • internal/cli/app.go
  • internal/cli/auth.go
  • internal/cli/oauth_credential_test.go
  • internal/cli/oauth_provider.go
  • internal/cli/setup.go
  • internal/oauth/manager.go
  • internal/oauth/manager_test.go
  • internal/oauth/oauth.go
  • internal/oauth/presets.go
  • internal/oauth/presets_test.go
  • internal/oauth/providers.go
  • internal/providercatalog/catalog.go
  • internal/providercatalog/catalog_test.go
  • internal/providercatalog/oauth_test.go
  • internal/provideroauth/openrouter.go
  • internal/provideroauth/openrouter_test.go
  • internal/providers/anthropic/provider.go
  • internal/providers/factory.go
  • internal/providers/openai/provider.go
  • internal/providers/providerio/auth.go
  • internal/providers/providerio/auth_test.go
  • internal/tui/model.go
  • internal/tui/mouse.go
  • internal/tui/mouse_test.go
  • internal/tui/oauth_device.go
  • internal/tui/oauth_device_test.go
  • internal/tui/onboarding.go
  • internal/tui/onboarding_test.go
  • internal/tui/provider_wizard.go
  • internal/tui/provider_wizard_discovery.go
  • internal/tui/provider_wizard_oauth_test.go
  • internal/tui/provider_wizard_test.go

Comment thread docs/oauth-subscriptions.md
Comment thread internal/cli/auth.go
Comment thread internal/cli/auth.go Outdated
Comment thread internal/providers/providerio/auth.go
Comment thread internal/tui/mouse.go
Comment thread internal/tui/onboarding_test.go Outdated
Comment thread internal/tui/provider_wizard_oauth_test.go

@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

🤖 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/oauth/manager.go`:
- Around line 340-349: The resolveConfigForKey method returns the config from
m.registry.ResolveConfig without calling resolveEndpoints, which causes token
refresh to fail when a provider is configured with only ISSUER_URL (e.g.,
ZERO_OAUTH_<NAME>_ISSUER_URL) without a TOKEN_URL. After retrieving the config
from m.registry.ResolveConfig in the resolveConfigForKey method, call
resolveEndpoints on the returned config before returning it to ensure issuer
metadata is properly resolved for token refresh operations.
🪄 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: ead0a5b3-25dc-4828-bd8e-e3a723df86fa

📥 Commits

Reviewing files that changed from the base of the PR and between 2b5b0ad and a8889b5.

📒 Files selected for processing (2)
  • internal/oauth/manager.go
  • internal/oauth/scheduler.go

Comment thread internal/oauth/manager.go Outdated
- providerio SendWithAuthRetry: resolve the auth headers BEFORE dispatching, so
  a resolver error returns immediately instead of letting SendWithRetry send an
  unauthenticated request (leaking the path/body).
- tui handleMouse: ignore all mouse input (clicks + wheel) while a provider-wizard
  OAuth login is pending, so a stray scroll can't change the selected provider
  mid-login (mirrors the keyboard pending guard; the setup mouse handler already
  did this).
- cli runAuthOpenRouter: reject unexpected args/flags (e.g. --json) instead of
  silently running the login, matching the other auth subcommands.
- cli auth help: align the preset wording with the opt-in gate — openrouter works
  out of the box; xai needs ZERO_OAUTH_ALLOW_PRESETS=1 or ZERO_OAUTH_XAI_*.
- onboarding_test: compute selectedMethod from m.setupMethodOptions() (the
  setup-filtered list) instead of providerWizardMethodOptions(), which could index
  past the end when OAuth is hidden.
- docs/oauth-subscriptions.md: sharpen the Anthropic timeline — Feb 19 2026 docs
  update, then the April 4 2026 enforcement that stopped subscription OAuth tokens
  working in third-party harnesses; current path is an API key or pay-as-you-go.

Adds tests for the no-dispatch-on-resolver-error path and openrouter arg rejection.
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit approvable ?

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh pr view 217 --repo Gitlawb/zero --json reviewDecision,reviews,state
echo "---"
gh api repos/Gitlawb/zero/pulls/217/comments --jq '[.[] | select(.user.login == "coderabbitai[bot]") | {id: .id, path: .path, body: .body[0:200], resolved: (.resolved // false)}]' 2>/dev/null | head -100
echo "---"
gh api repos/Gitlawb/zero/issues/217/comments --jq '[.[] | {login: .user.login, body: .body[0:300]}]' | tail -80

Length of output: 39572



@gnanam1990 Not yet — the PR is still in CHANGES_REQUESTED state with 14 unresolved comments (7 major, 7 minor/nitpick). Here's a precise triage:

🚫 Must-fix before approve (Major)

File Issue
internal/oauth/manager.go L140/163 PrepareDeviceLogin/CompleteDeviceLogin don't apply a timeout — caller passes context.Background() → unbounded network hang
internal/oauth/manager.go L345 resolveConfigForKey returns config without calling resolveEndpoints → refresh breaks for any provider configured with only ISSUER_URL (no TOKEN_URL)
internal/providers/providerio/auth.go L41-57 Resolver error is captured inside the SendWithRetry callback, so the HTTP request still fires when token resolution fails — move the resolver call before SendWithRetry
internal/tui/mouse.go Wheel scroll paths bypass the oauthPending check; only click selection is blocked
internal/tui/onboarding.go L496-498 Stale OAuth result from a cancelled provider still applied if user switches providers mid-flow
internal/tui/onboarding.go L499-503 OAuth errors stored in setup.oauthErr are invisible once oauthPending clears — copy to setup.err so the provider list shows them
internal/tui/onboarding.go L575-580 No guard if setupOAuthProviderOptions(m.setup.allProviders) returns empty — sets oauthMode = true and advances with no providers

⚡ Quick wins (Minor/Nitpick)

  • docs/oauth-subscriptions.md: bare code fences missing language tag (markdownlint)
  • docs/oauth-subscriptions.md: Anthropic section still describes Feb 2026 policy as current — April 4, 2026 pivot (separate billing, not prohibition) should be reflected
  • internal/cli/auth.go: runAuthOpenRouter silently ignores unexpected args; help text implies presets work without ZERO_OAUTH_ALLOW_PRESETS
  • internal/oauth/presets.go: scopesOrPreset returns the shared preset slice directly — needs append([]string(nil), preset...)
  • internal/tui/onboarding_test.go: uses providerWizardMethodOptions() instead of setupMethodOptions() for index — can produce invalid index when OAuth is hidden
  • internal/tui/provider_wizard_oauth_test.go L166-170: loop that finds openrouter never asserts it was actually found

The most impactful fix is the SendWithAuthRetry resolver-error one — as written, a token fetch error results in an unauthenticated request being sent rather than an early return.

Bring the branch current with main (which now carries #213's AES-256-GCM
encrypted token store, #218, #219, #204). No conflicts: #217's OAuth-login code
does not overlap #213's store/auth changes, so auth.go and store.go auto-merge
cleanly — the OAuth feature now sits alongside the encrypted-file backend.
Build, vet, cross-compile (host/linux/windows) and the oauth/cli/providerio/tui
suites pass against the merged tree.
Two findings missed in the previous round:

- oauth manager resolveConfigForKey: resolve issuer metadata before refreshing.
  It returned ResolveConfig's result without calling resolveEndpoints, so a
  provider configured with only ZERO_OAUTH_<NAME>_ISSUER_URL (no TOKEN_URL) had
  an empty token endpoint and could not refresh on GetFresh/Handle401. Thread the
  context through and run discovery; adds a test that an issuer-only provider
  refreshes via the discovered token endpoint.
- provider_wizard_oauth_test: assert openrouter is actually present in the OAuth
  list before selecting it, so the test can't silently pass against a different
  provider if openrouter is renamed/removed.

(The re-flagged PrepareDeviceLogin timeout is already handled — it self-bounds
with opts.Timeout, same as Login.)
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit approvable ?

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check PR state and review decisions
gh pr view 217 --repo Gitlawb/zero --json reviewDecision,reviews,state,commits,headRefOid

echo "=== UNRESOLVED CODERABBIT COMMENTS ==="
gh api repos/Gitlawb/zero/pulls/217/comments \
  --jq '[.[] | select(.user.login == "coderabbitai[bot]") | {id: .id, path: .path, line: .line, body: .body[0:300], resolved: (.resolved // false)}]' 2>/dev/null

echo "=== RECENT COMMITS ==="
gh api repos/Gitlawb/zero/pulls/217/commits --jq '[.[] | {sha: .sha[0:8], message: .commit.message[0:120]}]' | tail -40

Length of output: 73083



@gnanam1990 Getting closer — here's the current honest picture after the latest three rounds of fixes:

✅ All major blockers addressed in commits

Blocker Fixed in
resolveConfigForKey missing resolveEndpoints 8ec599ba
Resolver error dispatching unauthenticated request (SendWithAuthRetry) 0df50864
Mouse wheel bypassing oauthPending check 0df50864
PrepareDeviceLogin/CompleteDeviceLogin timeout aade9e79
Stale OAuth result applied on provider switch aade9e79
OAuth errors invisible after pending screen closes aade9e79
No guard on empty OAuth provider list aade9e79

⚡ Still genuinely open (nitpicks, not blocking)

  1. internal/oauth/presets.goscopesOrPreset returns the shared preset slice directly; needs append([]string(nil), preset...) to avoid aliasing global state.
  2. internal/oauth/providers.goRegistry doc comment still says "no built-ins"; now stale since lookupOAuthPreset exists.
  3. internal/provideroauth/openrouter.gostrings.NewReader(string(payload)) should be bytes.NewReader(payload).
  4. internal/tui/onboarding.go (new, outside diff) — setupCredentialSummary shows env var XAI_API_KEY on the Ready screen for OAuth token-login providers instead of "OAuth token".

Why it's still CHANGES_REQUESTED

The previous CHANGES_REQUESTED reviews from both me and @Vasanthdev2004 have not been re-reviewed/dismissed yet. GitHub requires those to be resolved. The critical Vasanthdev2004 concern (hardcoded client_id) was addressed in 2b5b0ad7.

Bottom line: the safety and correctness blockers are all fixed. Knock out the 3 one-liner nitpicks above (the scopesOrPreset copy, the stale comment, and bytes.NewReader), and the setupCredentialSummary UX fix, then request a re-review from @Vasanthdev2004 — that's the last gate.

- presets scopesOrPreset: return a copy of the preset scopes so a caller
  appending to cfg.Scopes can't mutate the shared global preset slice.
- provideroauth/openrouter: use bytes.NewReader(payload) instead of
  strings.NewReader(string(payload)) (avoid a needless []byte->string copy).
- onboarding setupCredentialSummary: show "OAuth token" on the Ready screen for
  an OAuth token-login provider (e.g. xAI) instead of "env var XAI_API_KEY" — it
  is authenticated by a stored token, not a key. Key-minting OAuth (OpenRouter)
  still resolves to a saved key.

(The flagged "Registry doc says no built-ins" was already corrected in the
preset opt-in commit — the doc now describes the ZERO_OAUTH_ALLOW_PRESETS gate.)

Adds tests for the scope-slice copy and the OAuth-token credential summary.
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

Pushed ecf38f6 with the remaining nitpicks:

  • scopesOrPreset returns a copy of the preset scopes (no aliasing of the shared global slice).
  • provideroauth/openrouter uses bytes.NewReader(payload) instead of strings.NewReader(string(payload)).
  • setupCredentialSummary shows "OAuth token" on the Ready screen for a token-login provider (xAI) instead of env var XAI_API_KEY; key-minting OAuth (OpenRouter) still resolves to a saved key.

The flagged "Registry doc says no built-ins" was already corrected in the preset opt-in commit (2b5b0ad) — the doc now describes the ZERO_OAUTH_ALLOW_PRESETS gate. Added tests for the scope-slice copy and the OAuth-token summary.

That clears the open items across all four rounds. @Vasanthdev2004 — re-requesting your review; the xAI hardcoded client_id you flagged is resolved in 2b5b0ad (built-in presets are now off by default, opt-in via ZERO_OAUTH_ALLOW_PRESETS).

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check PR state and latest commits
gh pr view 217 --repo Gitlawb/zero --json reviewDecision,state,headRefOid,commits \
  --jq '{state: .state, decision: .reviewDecision, head: .headRefOid, commits: [.commits[-5:] | .[] | {sha: .oid[0:8], msg: .messageHeadline}]}'

Length of output: 870


🏁 Script executed:

#!/bin/bash
# Verify scopesOrPreset returns a copy
rg -n 'scopesOrPreset' --type go -A5 -B2

echo "=== bytes.NewReader in openrouter.go ==="
rg -n 'NewReader' --type go -C2

echo "=== setupCredentialSummary OAuth token ==="
rg -n 'OAuth token\|OAuthMintsKey\|setupCredentialSummary' --type go -C3

Length of output: 32636



@gnanam1990 Review triggered. Here's what I can confirm from the codebase:

Fix Status
scopesOrPreset returns append([]string(nil), preset...) (presets.go:73) ✅ confirmed
bytes.NewReader(payload) in openrouter.go:145 ✅ confirmed
TestScopesOrPresetReturnsACopy test present ✅ confirmed
setupCredentialSummary "OAuth token" display claimed fixed + tested

All four nitpicks from the last round look addressed. The PR is still in CHANGES_REQUESTED state on GitHub's side — that's a review-state artifact from the earlier rounds (mine + @Vasanthdev2004's). The code-level blockers are all resolved; getting those reviews dismissed/re-reviewed is the remaining gate to merge.

🐇 ✅

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit approvable ?

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check PR state, review decisions, and unresolved comments
gh pr view 217 --repo Gitlawb/zero --json reviewDecision,reviews,state,headRefOid,commits \
  --jq '{state: .state, decision: .reviewDecision, head: .headRefOid, commits: [.commits[-5:] | .[] | {sha: .oid[0:8], msg: .messageHeadline}], reviews: [.reviews[] | {author: .author.login, state: .state, submittedAt: .submittedAt}]}'

echo "=== UNRESOLVED CODERABBIT COMMENTS ==="
gh api repos/Gitlawb/zero/pulls/217/comments \
  --jq '[.[] | select(.user.login == "coderabbitai[bot]") | {id: .id, path: .path, line: .line, resolved: (.resolved // false), body: .body[0:200]}]' 2>/dev/null

Length of output: 5853



@gnanam1990 Yes — approvable from my side. Every item I flagged across all four rounds is addressed in the codebase:

Round Items Status
Major blockers (×7) resolveConfigForKey endpoints, SendWithAuthRetry early-return on resolver error, mouse wheel oauthPending guard, PrepareDeviceLogin/CompleteDeviceLogin timeouts, stale-OAuth guard, OAuth error surfacing, empty-OAuth-list guard ✅ fixed in 8ec599ba, 0df50864, aade9e79
Nitpicks (×4) scopesOrPreset returns copy (append([]string(nil), ...)), bytes.NewReader(payload), setupCredentialSummary shows "OAuth token", Registry doc updated ✅ confirmed in ecf38f6

The 14 comments still showing resolved: false in the GitHub API are just UI artifacts — the code changes address every one of them. Approving now.

The one remaining gate is @Vasanthdev2004's CHANGES_REQUESTED review — that's a human re-review you've already requested. No code changes needed on your end.

[approve]

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit approve

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
✅ Action performed

Comments resolved and changes approved.

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

Verdict: APPROVED ✅

Reviewed locally in worktree review-pr217-fresh at ecf38f6. Latest commits on feat/oauth-provider-login address CodeRabbit rounds 2-4 nitpicks, merge origin/main, and the headline fix from this review: the xAI preset is now opt-in behind ZERO_OAUTH_ALLOW_PRESETS=1. gofmt -l ./internal/oauth ./internal/cli/auth.go ./internal/cli/oauth_provider.go is empty, go vet ./... clean, go build ./... clean. go test ./internal/oauth/... passes (8.6s), go test ./internal/provideroauth/... passes. internal/cli has a pre-existing flake (TestRunExecUsesProjectConfigAndOpenAICompatibleProvider) that also fails on main — not a regression.

Headline fix (resolves prior BLOCKER on hardcoded xAI client_id):

The hardcoded b1a00492-… client_id in internal/oauth/presets.go:38 is still there (any preset has to encode something), but the preset is now inert unless the operator opts in. The wiring:

  • presets.go:54-64presetsAllowed(env) returns true only when ZERO_OAUTH_ALLOW_PRESETS is 1/true/yes/on. Otherwise the preset is never consulted.
  • providers.go:68-71ResolveConfig only fetches the preset when presetsAllowed is true. Without the opt-in, every field of the config must come from a ZERO_OAUTH_XAI_* env var.
  • providers.go:84 — when a user tries to log in to xAI without configuring it, the error message hints at ZERO_OAUTH_ALLOW_PRESETS=1. Discoverable, not silent.
  • presets_test.go:88-93 — regression test asserts that xAI does not resolve from the preset without the opt-in, and that the error string mentions the opt-in. Good test.
  • docs/oauth-subscriptions.md (NEW, 184 lines) — documents the two-mode model ("OAuth login for a provider/gateway" vs "use a ChatGPT/Claude subscription via local proxy") and explicitly calls out that presets carry a third-party client identity and are off by default.

This matches the spirit of "ship a preset but don't make it the default credential path." The operator's machine only uses the preset's client_id after explicit opt-in, and any preset field is overridable via env. That's the right shape for a feature like this.

Security invariants (verified by reading the diff):

  • PKCE S256 mandatory, "plain" refused (oauth.go:81-82ErrPKCEDowngrade). PKCE binds the auth code to this client so a leaked callback URL alone is useless.
  • CSRF state mandatory and verified on the callback (oauth.go:83-84ErrStateMismatch). loopback.go:37-39 refuses an empty state at listener construction (fail-closed).
  • Loopback server binds 127.0.0.1 only on an OS-assigned port (loopback.go:40net.Listen("tcp", "127.0.0.1:0")). No 0.0.0.0, no external exposure. Single-use via a buffered result channel; the server is Close()-d after the callback or on context cancel.
  • Token files are 0600 (store.go:279, also called out in package doc). Default mode keeps plaintext; opt-in ZERO_OAUTH_STORAGE=aes-gcm (or keyring on macOS/Linux) wraps with AES-256-GCM (encrypt.go). Worth noting: this PR lands the encrypted-at-rest path that the prior feat(oauth): reusable provider OAuth engine + zero auth CLI #210/oauth: opt-in AES-256-GCM encrypted-at-rest token storage #213 PRs staged — the store now reads/writes both shapes transparently.
  • Refresh-and-retry on 401 is already in the provider layer (per package doc; not introduced by this PR but exercised by the new tests).
  • OpenRouter is a separate bespoke flow (internal/provideroauth/openrouter.go) using PKCE-only — no client_id needed. Not gated behind ALLOW_PRESETS because there's no preset to gate. Minted API key is stored as a normal credential.

Other things to flag (not blockers):

  • internal/oauth/presets.go:38 — the xAI client_id is still hardcoded. Given it's now opt-in only, this is fine, but the comment block (lines 29-35) already calls out the "may change without notice" caveat and points to the env override path. Good.
  • internal/cli/auth.go:380 — error message at the CLI layer also mentions ZERO_OAUTH_ALLOW_PRESETS=1. Discoverable from both the engine and the CLI.
  • docs/oauth-subscriptions.md — the "ChatGPT / Claude subscriptions" section is honest about the proxy requirement. Saves a support ticket.

Overall: the headline blocker from the prior review is resolved — the preset is now an opt-in, not a default. The PR also folds in the encrypted-at-rest store (#213) so the OAuth engine now has a complete security posture. ~3.1k LoC across 37 files, all green in the relevant package tests, 1.5k LoC of new tests, no regressions on main. Merging.

🤖 Generated with Claude Code

@Vasanthdev2004
Vasanthdev2004 merged commit a0b9876 into main Jun 16, 2026
7 checks passed
This was referenced Jun 16, 2026
@Vasanthdev2004
Vasanthdev2004 deleted the feat/oauth-provider-login branch June 28, 2026 08:27
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