返回 last30days-skill
2026-08-14-fix-grok-auth-honesty-plan.md
根目录 / docs / plans / 2026-08-14-fix-grok-auth-honesty-plan.md
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
64 lines MARKDOWN