| 1 | # Plan: Fix Grok Auth Honesty |
| 2 | |
| 3 | **Date:** 2026-08-14 |
| 4 | **Status:** Implemented |
| 5 | **PR:** fix(grok): treat expired sessions as degraded, not ok |
| 6 | |
| 7 | ## Problem (measured 2026-08-14 Peter Steinberger run on the user's Mac) |
| 8 | |
| 9 | - grok binary on PATH. Doctor cached grok status ok / will use grok because `~/.grok/auth.json` existed with token markers. |
| 10 | - `stored_auth_status()` substring-scans for `refresh_token`/`access_token`/`auth_mode`. It never parses `expires_at`. |
| 11 | - The file had `expires_at 2026-08-14T01:26:53Z`, hours dead. |
| 12 | - A prior run at 07:47:26 UTC had `run_outcome` ok (2 items). Session was real. |
| 13 | - At 08:43 grok loaded auth, `is_expired` true, OIDC refresh → `invalid_grant` "Refresh token has been revoked". grok deleted auth.json. |
| 14 | - Engine exit 1 "Not signed in", fell back to bird (30 items via Safari cookies), lane flagged PARTIAL. |
| 15 | - Host told the user "Grok CLI is not signed in" as if it never was. |
| 16 | |
| 17 | ## Three states to distinguish |
| 18 | |
| 19 | 1. **No grok CLI** — silent fallback. Fine. Do not waste the user's time. Do not nag install on every research run. |
| 20 | 2. **CLI installed, never logged in** — silent fallback. Fine. |
| 21 | 3. **CLI installed, WAS logged in, session dead** — currently reports ok then partial. **This is the bug.** |
| 22 | |
| 23 | ## What was built |
| 24 | |
| 25 | 1. **`stored_auth_status` parses `expires_at` locally** (no network, no subprocess). Added `AUTH_EXPIRED` distinct from `AUTH_OK` / `AUTH_MISSING` / `AUTH_ERROR`. Never echoes token values. Finds `expires_at` anywhere in the vendor-keyed JSON object via recursive search. |
| 26 | |
| 27 | 2. **Doctor / `_probe_grok` does NOT map `AUTH_EXPIRED` to `health.OK`** or "will use: grok". Reports `DEGRADED`/warn + expiry timestamp + "refresh happens at run; if refresh was revoked, `grok login --device-auth`". |
| 28 | |
| 29 | 3. **Research-time `is_available` STILL attempts grok when a `refresh_token` marker is present** even if `access` `expires_at` is past. Expiry of the access token is not proof refresh is dead. Does not skip a refresh that might work. |
| 30 | |
| 31 | 4. **Auth revocation detection**: If grok exits "Not signed in" / RefreshTokenRejected / auth.json vanished mid-run: does not retry grok in that run. Falls back once. Typed outcome `auth-failed` (via `is_auth_revoked_error()` and `classify_run_failure()`), not a generic PARTIAL that reads as "the product half-worked." |
| 32 | |
| 33 | 5. **Host-facing copy for case 3**: SKILL.md updated with guidance: "X used <fallback> after the Grok session expired" + login hint. Not "Grok CLI is not signed in" when `run_outcome` shows it worked earlier. |
| 34 | |
| 35 | 6. **Doctor --probe still does not call xAI or grok.** Whole-doctor-path test patches `subprocess.run` to raise and still passes. `active_backend` stays a prediction; when `run_outcome.at` is stale or not ok, doctor says "will use grok, unverified since <time>". |
| 36 | |
| 37 | 7. **Tests**: Fixture stores (missing file, future `expires_at`, past `expires_at`, unparseable JSON). No network. |
| 38 | |
| 39 | 8. **SKILL.md**: Host reads `sources.x.run_outcome` and grok expiry warn; does not treat `active_backend` as verified; does not spend a turn installing grok unless the user asked for first-party X. |
| 40 | |
| 41 | 9. **Changelog fragment**: `changelog.d/+grok-auth-expired.fixed.md`. Tests pass with `uv run pytest`. |
| 42 | |
| 43 | ## Scope boundaries (NOT in this PR) |
| 44 | |
| 45 | - X query construction, fanout, `search_name`, retrieve-judge-retry, and handle promotion are unchanged. That is a separate PR. |
| 46 | |
| 47 | ## Success criteria (all met) |
| 48 | |
| 49 | - Past `expires_at` fixture → not grok ok. |
| 50 | - Future `expires_at` → still ok (not live-verified). |
| 51 | - No grok binary → no extra user-facing failure. |
| 52 | - Simulated "Not signed in" after prior ok `run_outcome` → typed `auth-failed` / fallback copy, not "never signed in." |
| 53 | - No-subprocess doctor test still passes. |
| 54 | |
| 55 | ## Files changed |
| 56 | |
| 57 | - `skills/last30days/scripts/lib/grok_x.py` — `AUTH_EXPIRED`, `stored_auth_status()` returns 3-tuple, `is_auth_revoked_error()`, `classify_run_failure()`, `_invoke()` sets `auth_revoked`, `_run_query()` returns 3-tuple, `search_x()` propagates `auth_revoked` |
| 58 | - `skills/last30days/scripts/lib/backends.py` — `_probe_grok()` handles `AUTH_EXPIRED` as `DEGRADED` |
| 59 | - `skills/last30days/scripts/lib/pipeline.py` — `_fetch_x_backend()` propagates `auth_revoked`, `_classify_source_failure()` recognizes grok markers |
| 60 | - `skills/last30days/SKILL.md` — Grok session expiry handling guidance |
| 61 | - `tests/test_grok_x.py` — expires_at and auth revocation tests |
| 62 | - `tests/test_backend_descriptors.py` — grok expiry state tests |
| 63 | - `changelog.d/+grok-auth-expired.fixed.md` — release notes fragment |
| 64 |