98 lines
5.3 KiB
Markdown
98 lines
5.3 KiB
Markdown
# grok-token-broker
|
|
|
|
Holds a refreshable xAI **session** credential so a gateway can serve the Grok Build coding
|
|
plan — without touching the credential the operator's `grok` CLI depends on.
|
|
|
|
seed copy the CLI credential into the broker's own store (read-once)
|
|
probe-rotation MEASURE whether refresh rotates AND invalidates <- gate
|
|
token --raw emit a valid access token, refreshing if near expiry
|
|
refresh refresh now
|
|
status report state, touching nothing
|
|
|
|
## The problem
|
|
|
|
The coding plan (`https://cli-chat-proxy.grok.com/v1`, serving **grok-4.6**, 500k context)
|
|
authenticates with an OIDC session token — `api_key: null`, `env_key: null` in the CLI's own
|
|
model cache — that expires roughly every six hours. LiteLLM and every other gateway here hold
|
|
a **static** credential, so a naive alias works until the session lapses and then fails closed.
|
|
|
|
## ⛔ Why the loop is disarmed until measured
|
|
|
|
`https://auth.x.ai/oauth2/token` supports the refresh grant. But **a refresh may rotate the
|
|
refresh token, and many providers invalidate the old one server-side the moment a new one
|
|
issues.** The `grok` CLI holds its refresh token in `~/.grok/auth.json`.
|
|
|
|
**Not writing to that file is necessary and not sufficient.** If xAI rotates-and-invalidates,
|
|
a broker refreshing the *same* credential kills the CLI login anyway, server-side. heid's
|
|
`groa_http_dispatch.py` refuses to refresh at all for exactly this reason — correct, absent an
|
|
answer. This broker exists to get the answer, then act on it.
|
|
|
|
So `probe-rotation` is a gate, not a diagnostic: `refresh` and `token`-near-expiry both refuse
|
|
until a verdict exists and says safe.
|
|
|
|
| verdict | meaning |
|
|
|---|---|
|
|
| `non-rotating` | same refresh token returned. Coexistence safe. Armed. |
|
|
| `rotating-old-still-valid` | rotation happens, old token still works. Coexistence safe. Armed. |
|
|
| `rotating-and-invalidating` | **the CLI login is already dead.** Broker must not share this credential — it needs its own login. Stays disarmed. |
|
|
|
|
⚠ **The probe spends one refresh, and there is no way to ask the question without spending
|
|
it.** If the answer is the bad one, the CLI is broken at that moment and needs an interactive
|
|
`grok` re-login. Hence the required `--i-accept-this-may-end-the-cli-session` flag.
|
|
|
|
⚠⚠ **AND THE BLAST RADIUS IS BOTH GRÓA TRANSPORTS, NOT ONE — verified, 2026-09-16.** An earlier
|
|
draft of this file implied only the HTTP path was exposed. Wrong: `heid/scripts/groa_dispatch.py`
|
|
invokes the `grok` CLI directly (`argv = ["grok", "-p", prompt, ...]`), and the CLI authenticates
|
|
from the same `~/.grok/auth.json`. The bwrap in the process table is the CLI's own Landlock
|
|
sandbox, not something Heid wraps around it. So the read-jail transport and the direct-endpoint
|
|
transport **share one session**: one reaches it through the CLI, the other reads the file the CLI
|
|
wrote. An invalidating probe takes **Gróa down on every transport** until a human re-logs in — not
|
|
"the HTTP path degrades". Caught by `heid`, confirmed here against their source.
|
|
|
|
## Current state — ⛔ SHELVED, deliberately, 2026-09-16
|
|
|
|
**Seeded, committed, disarmed, and with no consumer. Do NOT arm `probe-rotation`.** This is a
|
|
finished resting place, not a half-built tool — the gate is working exactly as designed and the
|
|
thing it was gating for went away.
|
|
|
|
Operator ruling the same day, relayed by `heid`: *"keep the jail stop the a/b."* Heid dispatches
|
|
Gróa through the read jail; `groa_http_dispatch.py` is kept as a documented fallback with no
|
|
scheduled use. **Nothing in the fleet is asking for a renewable xAI session.**
|
|
|
|
So the trade inverted while the tool was being built:
|
|
|
|
the RISK did not shrink -- an invalidating probe still reaches BOTH Gróa transports through
|
|
the shared `~/.grok/auth.json` session, taking the arm down until an interactive re-login
|
|
the PAYOFF went to zero -- it buys token renewal on a transport nobody dispatches
|
|
|
|
⭐ **If something later needs a renewable xAI session, the argument reopens on its own merits
|
|
and this is sitting here ready.** That is the whole reason it was left seeded rather than torn
|
|
out. Re-read the probe warning below before arming it; none of that risk expired.
|
|
|
|
Scope `openid profile email offline_access grok-cli:access api:access`, client `b1a00492-…`.
|
|
Probe verdict: **UNMEASURED**, and correctly so.
|
|
|
|
⚠ **One question this never answered, and it is a billing one:** the jail reaches the coding
|
|
plan already paid for; the HTTP path reaches the metered API, whose responses carry a
|
|
`cost_in_usd_ticks` field. Whether that bills *on top of* the plan was never measured and was
|
|
not part of the ruling. It is one look at the xAI billing console, which this fleet holds no
|
|
credential for.
|
|
|
|
## For a consumer
|
|
|
|
```bash
|
|
TOKEN=$(broker.py token --raw)
|
|
curl https://cli-chat-proxy.grok.com/v1/... -H "Authorization: Bearer $TOKEN"
|
|
```
|
|
|
|
⚠ **The coding plan speaks the Responses API (`api_backend: "responses"`), not
|
|
`/chat/completions`.** That is a second, independent obstacle to a LiteLLM alias and this
|
|
broker does not solve it — it solves the credential half only.
|
|
|
|
## Invariants
|
|
|
|
- Never writes `~/.grok/auth.json`. Reads it once, on `seed`.
|
|
- Never prints a token except under `token --raw`.
|
|
- Never logs a token value — expiry, scope and subject only.
|
|
- Store is `~/.config/grok-token-broker/` at 0700, credential at 0600.
|