Skip to content

symfonic.core.grok_credentials

grok_credentials

Grok credential store — auto-load + auto-refresh of the grok CLI token.

DEV/TEST USE ONLY. Reads the plaintext OAuth credential file written by a grok login (~/.grok/auth.json, or $GROK_HOME/auth.json) and yields an always-valid short-lived access token, refreshing transparently against auth.x.ai when the cached token is within expiry_skew_seconds of expiry.

File shape (as written by grok login)

.. code-block:: json

{"https://auth.x.ai::<uuid>": {
    "key": "<882-char JWT access token>",
    "refresh_token": "<86-char rotating refresh token>",
    "expires_at": "2026-10-20T09:57:05.096631Z",
    "oidc_client_id": "<the CLI's public client id>",
    "...": "other profile fields, preserved verbatim"
}}

The store uses the FIRST entry carrying a non-empty key. The refresh grant reuses the entry's own oidc_client_id — no client identity is embedded here, so there is nothing to rotate when xAI rotates theirs.

Side effect (deliberate and necessary)

The xAI OAuth refresh_token ROTATES: each refresh returns a NEW refresh_token and INVALIDATES the previous one. To keep the developer's grok login working, the rotated token MUST be written back to the credential file in its native shape (key / refresh_token / expires_at). This module therefore mutates the local credential file on refresh. The write is atomic (temp file + os.replace) and guarded by an exclusive file lock so a concurrent grok refresh cannot corrupt the file or race the rotation.

Tokens are NEVER logged; only a short non-leaking fingerprint is emitted.

GrokCredentialError

Bases: RuntimeError

Raised when a Grok subscription token cannot be resolved or refreshed.

GrokCredentialStore

GrokCredentialStore(credentials_path: Path | None = None, *, token_url: str = _DEFAULT_TOKEN_URL, expiry_skew_seconds: int = 300)

Yields an always-valid grok CLI access token, refreshing as needed.

Parameters:

Name Type Description Default
credentials_path Path | None

Override the credential file. Defaults to the GROK_OAUTH_CREDENTIALS_FILE env var, then $GROK_HOME, then ~/.grok/auth.json.

None
token_url str

OAuth token endpoint. Defaults to auth.x.ai.

_DEFAULT_TOKEN_URL
expiry_skew_seconds int

Refresh this many seconds BEFORE the cached token's expires_at so an in-flight request never carries a just-expired token. Default 300s, matching the CLI's own GROK_AUTH_EARLY_INVALIDATION_SECS (token life is ~6h).

300
Source code in src/symfonic/core/grok_credentials.py
def __init__(
    self,
    credentials_path: Path | None = None,
    *,
    token_url: str = _DEFAULT_TOKEN_URL,
    expiry_skew_seconds: int = 300,
) -> None:
    self._path = (
        Path(credentials_path).expanduser()
        if credentials_path is not None
        else default_credentials_path()
    )
    self._lock_path = self._path.with_name(self._path.name + ".lock")
    self._token_url = token_url
    self._skew = expiry_skew_seconds

access_token

access_token() -> str

Return a valid access token, refreshing if within the skew window.

Source code in src/symfonic/core/grok_credentials.py
def access_token(self) -> str:
    """Return a valid access token, refreshing if within the skew window."""
    data, entry = self._load()
    if self._is_fresh(entry):
        token = _entry_key(entry)
        assert token is not None  # _load guarantees a keyed entry
        return token
    return self._refresh()

peek_access_token

peek_access_token() -> str | None

Return the cached token without refreshing; None if unavailable.

For logging / fingerprinting at construction time — must never raise or trigger a network refresh.

Source code in src/symfonic/core/grok_credentials.py
def peek_access_token(self) -> str | None:
    """Return the cached token without refreshing; ``None`` if unavailable.

    For logging / fingerprinting at construction time — must never raise or
    trigger a network refresh.
    """
    try:
        _, entry = self._load()
    except GrokCredentialError:
        return None
    return _entry_key(entry)

default_credentials_path

default_credentials_path() -> Path

Resolve the grok CLI credential file path.

Precedence: GROK_OAUTH_CREDENTIALS_FILE override, then $GROK_HOME/auth.json (the CLI's own relocation knob), then ~/.grok/auth.json.

Source code in src/symfonic/core/grok_credentials.py
def default_credentials_path() -> Path:
    """Resolve the grok CLI credential file path.

    Precedence: ``GROK_OAUTH_CREDENTIALS_FILE`` override, then
    ``$GROK_HOME/auth.json`` (the CLI's own relocation knob), then
    ``~/.grok/auth.json``.
    """
    override = os.environ.get(_CREDENTIALS_ENV)
    if override:
        return Path(override).expanduser()
    home = os.environ.get("GROK_HOME")
    if home:
        return Path(home).expanduser() / "auth.json"
    return Path.home() / ".grok" / "auth.json"