# Grok account parity — design

audience: AI coding agents first

## Purpose

Add a **Grok** tray section parallel to Codex and Claude Code:

- multi-account registry under `~/.systray-ai/grok-accounts/`
- add / set default / rename / remove / re-auth
- health + **billing-period usage only** (no 5h window)
- preserve and pin the live session as **`roy-grok`** without ever revoking it

## Critical token safety (HARD)

xAI OAuth refresh **rotates/revokes** prior refresh tokens. Dual refresh kills the only session.

| Rule | MUST |
|------|------|
| Import current session | **Copy** `~/.grok/auth.json` only — never call `auth.x.ai/oauth2/token` |
| Source of truth per account | `GROK_HOME/auth.json` under that account |
| Active link | `~/.grok/auth.json` → symlink to default account's `auth.json` (after copy-in) |
| Health probe | `GET https://cli-chat-proxy.grok.com/v1/billing` + `/v1/user` with **access** token only |
| Refresh | Only if access JWT `exp` passed **and** probe returns 401; write new tokens **only** into that account's `auth.json` under flock; never refresh two homes with the same refresh token |
| Remove account | Delete **local** files only — never call revoke endpoint |
| `roy-grok` | Protected slug: refuse `remove` / `reauthenticate` unless explicit env override `SYSTRAY_ALLOW_ROY_GROK_MUTATION=1` |

## Verified endpoints (read-only probes)

- `GET https://cli-chat-proxy.grok.com/v1/billing` → `{ config: { monthlyLimit.val, used.val, billingPeriodStart, billingPeriodEnd } }`
- `GET https://cli-chat-proxy.grok.com/v1/user` → email, principalId, organizationId, …
- `GROK_HOME` env overrides Grok config dir (auth at `$GROK_HOME/auth.json`)

No 5h window exists. Map monthly used/limit → **secondary** usage percent only. Primary stays `None` (UI hides 5h row).

## Architecture

Same pattern as Claude parity:

```
AccountRegistryKind.GROK
  accounts_dir: ~/.systray-ai/grok-accounts
  registry:     ~/.systray-ai/grok_accounts.json
  default:      ~/.systray-ai/grok_default_slug
  home name:    GROK_HOME

GrokAuthOperation  → lifecycle (stage_add + device login or import)
GrokHealthClient   → auth OK + secondary_used_pct from billing
GrokOAuthSession   → GET billing/user; guarded single-home refresh
```

Wire into `ProviderServiceMap` + `Indicator` third submenu **"Grok"**.

### Usage mapping

```
percent = min(100, round(100 * used / monthlyLimit))  # if limit > 0
secondary_used_pct = percent
secondary_reset_at = parse(billingPeriodEnd)
primary_* = None
```

### Import `roy-grok` (one-time bootstrap)

1. If slug `roy-grok` missing: `add_dir("roy-grok", "roy-grok")`
2. `shutil.copy2(~/.grok/auth.json, GROK_HOME/auth.json)`; `chmod 0o600`
3. Write identity sidecar from `/v1/user` **using copied access** (GET only)
4. `set_default(roy-grok)` → active symlink (only after copy succeeds)
5. Never touch token endpoint

### Add new account

1. `stage_add(alias)` → isolated `GROK_HOME`
2. Device-code OAuth into staged `auth.json` (same public Grok CLI client id) **or** `GROK_HOME=<staged> grok login --device-auth` if available
3. GET `/v1/user` for identity; reject collision on principalId/email
4. commit staged account

Re-auth: same as add into existing home (except `roy-grok` gated).

## Out of scope

- Pi `xai-oauth` auto-sync on switch (optional later, derive-only)
- 5h usage row for Grok
- API-key Grok accounts
- Revoking remote sessions
