OAuth / account login for providers (OpenRouter, xAI) + subscription-proxy support#217
Conversation
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.
Zero automated PR reviewVerdict: No blockers found Blockers
Validation
ScopeHead: This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
🚧 Files skipped from review as they are similar to previous changes (5)
WalkthroughAdds comprehensive OAuth provider authentication to Zero: an ChangesOAuth Provider Authentication
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
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)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
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 winShow OAuth token login instead of an env var on the Ready screen.
For OAuth providers that do not mint API keys,
completeSetupintentionally stores no API key, but this summary falls through toenv 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 valueMinor: use
bytes.NewReaderto avoid string conversion.Line 144 converts
payload(already a[]byte) tostringjust to wrap it instrings.NewReader. Usingbytes.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 winDefensively copy preset scopes before returning.
Line 53 returns the shared preset slice. Any downstream mutation of
cfg.Scopescan 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 winUpdate Registry docs to match current behavior.
This comment says there are no built-ins, but
ResolveConfignow applieslookupOAuthPreset(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
📒 Files selected for processing (32)
docs/oauth-subscriptions.mdinternal/browser/open.gointernal/browser/open_test.gointernal/cli/app.gointernal/cli/auth.gointernal/cli/oauth_provider.gointernal/oauth/manager.gointernal/oauth/manager_test.gointernal/oauth/presets.gointernal/oauth/presets_test.gointernal/oauth/providers.gointernal/providercatalog/catalog.gointernal/providercatalog/catalog_test.gointernal/providercatalog/oauth_test.gointernal/provideroauth/openrouter.gointernal/provideroauth/openrouter_test.gointernal/providers/anthropic/provider.gointernal/providers/factory.gointernal/providers/openai/provider.gointernal/providers/providerio/auth.gointernal/providers/providerio/auth_test.gointernal/tui/model.gointernal/tui/mouse.gointernal/tui/mouse_test.gointernal/tui/oauth_device.gointernal/tui/oauth_device_test.gointernal/tui/onboarding.gointernal/tui/onboarding_test.gointernal/tui/provider_wizard.gointernal/tui/provider_wizard_discovery.gointernal/tui/provider_wizard_oauth_test.gointernal/tui/provider_wizard_test.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
left a comment
There was a problem hiding this comment.
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:
- 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. - 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
validateTokenEndpointnaming smell I flagged on #210 is re-introduced here (providers.gocalls it oncfg.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.goopener is a clean cross-platform abstraction (xdg-open/open/rundll32 url.dll,FileProtocolHandler). - The
internal/providers/providerio/auth.gohelper for OpenAI/AnthropicAuthorization: Bearerplumbing is small and correct — uses the existingoauth.Manager.GetFreshwith 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.
|
Thanks for the thorough reviews — pushed Blocker — hardcoded xAI CodeRabbit:
Added tests for the preset opt-in gate, the descriptor-clone isolation, and the method-option filtering. gofmt / vet / build (host+linux+windows) / Left the @coderabbitai review |
|
✅ Action performedReview finished.
|
|
@CodeRabbit rereview |
|
✅ Action performedFull 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).
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (1)
internal/oauth/presets.go (1)
68-73: ⚡ Quick winReturn a copy of preset scopes to avoid shared mutable state.
On Line 72,
scopesOrPresetreturns the preset slice directly. That aliases global preset memory; any in-place mutation of returnedConfig.Scopescan 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
📒 Files selected for processing (35)
docs/oauth-subscriptions.mdinternal/browser/open.gointernal/browser/open_test.gointernal/cli/app.gointernal/cli/auth.gointernal/cli/oauth_credential_test.gointernal/cli/oauth_provider.gointernal/cli/setup.gointernal/oauth/manager.gointernal/oauth/manager_test.gointernal/oauth/oauth.gointernal/oauth/presets.gointernal/oauth/presets_test.gointernal/oauth/providers.gointernal/providercatalog/catalog.gointernal/providercatalog/catalog_test.gointernal/providercatalog/oauth_test.gointernal/provideroauth/openrouter.gointernal/provideroauth/openrouter_test.gointernal/providers/anthropic/provider.gointernal/providers/factory.gointernal/providers/openai/provider.gointernal/providers/providerio/auth.gointernal/providers/providerio/auth_test.gointernal/tui/model.gointernal/tui/mouse.gointernal/tui/mouse_test.gointernal/tui/oauth_device.gointernal/tui/oauth_device_test.gointernal/tui/onboarding.gointernal/tui/onboarding_test.gointernal/tui/provider_wizard.gointernal/tui/provider_wizard_discovery.gointernal/tui/provider_wizard_oauth_test.gointernal/tui/provider_wizard_test.go
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
internal/oauth/manager.gointernal/oauth/scheduler.go
- 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.
|
@CodeRabbit review |
✅ Action performedReview finished.
|
|
@CodeRabbit approvable ? |
|
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 -80Length of output: 39572
🚫 Must-fix before approve (Major)
⚡ Quick wins (Minor/Nitpick)
The most impactful fix is the |
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.)
|
@CodeRabbit review |
✅ Action performedReview finished.
|
|
@CodeRabbit approvable ? |
|
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 -40Length of output: 73083
✅ All major blockers addressed in commits
⚡ Still genuinely open (nitpicks, not blocking)
Why it's still CHANGES_REQUESTEDThe previous CHANGES_REQUESTED reviews from both me and Bottom line: the safety and correctness blockers are all fixed. Knock out the 3 one-liner nitpicks above (the |
- 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.
|
Pushed
The flagged "Registry doc says no built-ins" was already corrected in the preset opt-in commit ( That clears the open items across all four rounds. @Vasanthdev2004 — re-requesting your review; the xAI hardcoded client_id you flagged is resolved in @coderabbitai review |
|
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 -C3Length of output: 32636
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 + 🐇 ✅ |
|
@CodeRabbit review |
✅ Action performedReview finished.
|
|
@CodeRabbit approvable ? |
|
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/nullLength of output: 5853
The 14 comments still showing The one remaining gate is [approve] |
|
@CodeRabbit approve |
✅ Action performedComments resolved and changes approved. |
Verdict: APPROVED ✅Reviewed locally in worktree Headline fix (resolves prior BLOCKER on hardcoded xAI client_id): The hardcoded
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):
Other things to flag (not blockers):
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 |
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
/providerwizard and first-run onboarding — no API key to paste. Two providers qualify today: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 sendAuthorization: Bearer <oauth>when a login exists for the profile (auto-refreshed; one refresh-and-retry on401), 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) andzero auth openrouter;zero auth login xai [--device].In-app UX
d(or automatic on SSH / headless Linux /ZERO_OAUTH_DEVICE=1) — shows a URL + code to enter on another device.Subscription proxies (ChatGPT / Claude)
chatgpt-proxypreset +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
go vet, build (host + linux + windows),go test ./... -race— all green.Notes
The interactive browser/device approval and the live xAI
/modelsresponse 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
auth openrouter(browser-based PKCE that prints an API key) and RFC 8628 device-code login where supported.Documentation
Bug Fixes