Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dev-notes/architecture/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ For the practical frontend contract, see [Building a Custom TUI](../custom-tui.m
- [Phase 23: Advanced TUI and Product Polish](./phase-23-tui-polish.md)
- [Bounded TUI Transcript Rendering](./tui-long-transcript-performance.md)
- [Phase 24: Session Tree Branching](./phase-24-session-tree-branching.md)
- [OAuth provider parity](./oauth-provider-parity.md)
- [xAI SuperGrok / X Premium OAuth](./xai-oauth.md)

Phase 21 extensions are implemented; see the phase note and the user guide at
`website/content/guides/extensions.md`.
4 changes: 3 additions & 1 deletion dev-notes/architecture/oauth-provider-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ tau_coding.oauth_registry
│ OAuthProvider protocol
├── OpenAI Codex
├── Anthropic
└── GitHub Copilot
├── GitHub Copilot
└── xAI
FileCredentialStore ── OAuthRuntimeCredentialResolver ── tau_ai adapter
Expand Down Expand Up @@ -135,6 +136,7 @@ users can update or delete old local entries safely.

## Follow-ups

- xAI SuperGrok / X Premium device-code OAuth is implemented separately; see [xAI OAuth](./xai-oauth.md).
- Add OpenAI Codex device-code login and an explicit browser/device selector.
- Add a frontend-neutral non-TUI login command for SSH-only use.
- Consider process-level refresh locking if Tau introduces concurrent processes
Expand Down
97 changes: 97 additions & 0 deletions dev-notes/architecture/xai-oauth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# xAI SuperGrok / X Premium OAuth

Issue: [#676](https://github.com/huggingface/tau/issues/676)

## What this adds

xAI was already a built-in OpenAI-compatible catalog provider, but login was
API-key only. This change registers a Tau-owned device-code OAuth provider so
SuperGrok / X Premium users can run `/login xai` without a paid API key.

The existing `XAI_API_KEY` path stays available:

```text
auth_methods = ["api_key", "oauth"]
```

This is a provider-specific follow-up to the registry from
[OAuth provider parity](./oauth-provider-parity.md). It does not change
`tau_agent` or add a new auth architecture.

## Architecture

```text
Textual OAuthLoginScreen
│ OAuthLoginCallbacks (device code, progress, cancellation)
tau_coding.oauth_registry
│ OAuthProvider protocol
└── XaiOAuthProvider (oauth_xai.py)
FileCredentialStore ── OAuthRuntimeCredentialResolver ── OpenAI-compatible adapter
```

`tau_coding` owns the device-code flow, refresh, credential storage, and
`/login` policy. The existing `api.x.ai` OpenAI-compatible adapter in `tau_ai`
receives only a Bearer access token. `tau_agent` is unchanged.

Device-code requests identify Tau as the client with `referrer=tau`. Login,
refresh, and runtime auth follow the same `OAuthProvider` contract as Anthropic,
Codex, and Copilot.

## Behavior

- `/login` → Subscription lists xAI (SuperGrok / X Premium).
- `/login xai` and `/login xai-subscription` start RFC 8628 device authorization
against `auth.x.ai`: show the verification URL and user code, then poll until
authorized, denied, cancelled, or expired. `/login xai-api` saves an API key.
- Successful login stores `access` / `refresh` / `expires` under
`~/.tau/credentials.json`.
- Runtime uses the access token as the Bearer credential for `https://api.x.ai/v1`.
- Refresh happens before expiry. If xAI omits `refresh_token` on refresh, Tau
keeps the previous refresh token.
- `/logout xai` removes the local credential. It does not remotely revoke the
grant.
- Print mode works after login: `tau --provider xai --model <id> -p "..."`.

API-key login remains available through `/login xai-api`, `/login` → API key,
`XAI_API_KEY`, or a saved key.

## Security choices

- Device verification URLs are accepted only with an `https` scheme and host.
- Failed OAuth responses may include structured `error` / `error_description`
text. Request secrets such as refresh tokens are scrubbed from that text.
- Credentials remain in `~/.tau/credentials.json` with mode `0600`. The file is
not encrypted.
- Tests use `httpx.MockTransport` and fake credentials. CI does not contact
`auth.x.ai` or require real secrets.

Device-code requests send `referrer=tau`. The current public client ID is the
one `auth.x.ai` already accepts for CLI device login; replace it if xAI issues a
Tau-owned client.

## How to test

```bash
uv run pytest tests/test_oauth_providers.py tests/test_provider_catalog.py::test_builtin_catalog_oauth_and_opencode_auth_methods
uv run ruff check .
uv run ruff format --check .
uv run mypy
```

The mocked tests cover successful login with `referrer=tau`, authorization
pending, denial, expiry, untrusted verification URIs, malformed JSON, refresh
token rotation, and omitted `refresh_token` on refresh.

Live subscription smoke tests are not automated. Before a release, verify
`/login xai` once with a SuperGrok or X Premium account, including one
headless/SSH device-code path. Do not copy tokens, device codes, or credential
file contents into GitHub.

## Rollback

Remove `XaiOAuthProvider` from `oauth_registry.py` and drop `oauth` from the xAI
catalog `auth_methods`. Existing local OAuth objects remain parseable so users
can `/logout xai` or keep using `XAI_API_KEY`.
4 changes: 4 additions & 0 deletions src/tau_coding/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
LOGIN_PROVIDER_ALIASES = {
"anthropic-api": ("anthropic", "api-key"),
"anthropic-subscription": ("anthropic", "subscription"),
"xai-api": ("xai", "api-key"),
"xai-subscription": ("xai", "subscription"),
}


Expand Down Expand Up @@ -761,6 +763,8 @@ def _login_command(context: CommandContext) -> CommandResult:
aliased_provider = LOGIN_PROVIDER_ALIASES.get(provider_name)
if aliased_provider is not None:
provider_name, login_method = aliased_provider
elif provider_name == "xai":
login_method = "subscription"
else:
login_method = None
entry = builtin_provider_entry(provider_name)
Expand Down
1 change: 1 addition & 0 deletions src/tau_coding/data/catalog.toml
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,7 @@ kind = "openai-compatible"
base_url = "https://api.x.ai/v1"
api_key_env = "XAI_API_KEY"
credential_name = "xai"
auth_methods = ["api_key", "oauth"]
models = ["grok-3", "grok-3-fast", "grok-4.20-0309-non-reasoning", "grok-4.20-0309-reasoning", "grok-4.3", "grok-build-0.1", "grok-code-fast-1"]
default_model = "grok-4.20-0309-reasoning"
docs_url = "https://docs.x.ai"
Expand Down
8 changes: 7 additions & 1 deletion src/tau_coding/oauth_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@
from tau_coding.oauth_anthropic import AnthropicOAuthProvider
from tau_coding.oauth_github_copilot import GitHubCopilotOAuthProvider
from tau_coding.oauth_types import OAuthProvider
from tau_coding.oauth_xai import XaiOAuthProvider

_BUILTIN_PROVIDERS: tuple[OAuthProvider, ...] = tuple(
[AnthropicOAuthProvider(), GitHubCopilotOAuthProvider(), OpenAICodexOAuthProvider()]
[
AnthropicOAuthProvider(),
GitHubCopilotOAuthProvider(),
OpenAICodexOAuthProvider(),
XaiOAuthProvider(),
]
)
_registry: dict[str, OAuthProvider] = {provider.id: provider for provider in _BUILTIN_PROVIDERS}

Expand Down
Loading