fix(grok): read the xai-oauth login from the host credential pool - #115
Merged
Conversation
`talk doctor` reported a working Grok lane as unconfigured. The read-only
parse of the host store knew only one of the two shapes a Hermes `xai-oauth`
login lives in -- `providers["xai-oauth"]["tokens"]` -- and a current host
writes a device-code login into `credential_pool["xai-oauth"]` instead: a
list whose rows carry `access_token` / `refresh_token` FLAT rather than
nested under `tokens`. On a box whose `providers` block is empty, the parse
returned `missing` and doctor printed
[FAIL] auth: no usable Grok authentication lane was found
receipt: winner=none, xai-oauth=missing, preference=absent
while the live probe in the same process resolved that exact login and got
a 200 from `/v1/realtime/client_secrets` and a 101 on the socket. Only the
diagnostic was blind; the call itself always worked.
`_inspect_store` now mirrors the host's own resolver end to end --
`hermes_cli.auth._xai_oauth_state_from_store` (hermes_cli/auth.py:5287-5321),
the function behind `resolve_xai_oauth_runtime_credentials`, which is the
resolver `_resolve_via_host` calls -- so the diagnostic predicts the lane it
is describing:
* `providers` first, then the pool, in the host's order (auth.py:5289-5320);
* both tokens required on either shape (auth.py:5291-5294, :5307-5311) --
the same pair check that rejects a quarantined login, since the host
quarantines by POPPING both tokens (auth.py:7891-7896);
* pool rows walked in stored order, no `priority` sort and no `last_status`
read, because the host's resolver does neither there;
* a non-list pool slice yields nothing, matching the host's own type check
(auth.py:2277-2282, :5303) rather than claiming a login it would refuse.
Every existing state string (`missing` / `invalid` / `expired` / `valid`) and
the read-only contract are unchanged: no network, no refresh, no write to any
auth store. A non-dict `providers` block is no longer a dead end -- it now
falls through to the pool instead of short-circuiting to `invalid`.
The receipt gained `xai_oauth_source` (`providers` / `credential_pool` /
`None`) and `talk doctor` prints `xai-oauth=valid (via credential_pool)`, so
the next person to debug this does not have to read the store by hand.
Siblings checked, not assumed: `talk_auth.py` resolves `$CODEX_HOME/auth.json`
(`_codex_auth_path`, talk_auth.py:116-121), a different file, and
`talk_core_provider.py` delegates to it -- neither is affected.
`talk_grok_auth.py` is the only reader of `HERMES_HOME/auth.json`.
Verified on the live box: same store, `xai_oauth` goes `missing` -> `valid`,
`winning_lane` `null` -> `xai-oauth`, `configured` false -> true.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
hermes talk doctorreported a working Grok lane as unconfigured:…while the live probe in the same process resolved that exact login and connected:
Only the read-only diagnostic was blind. The call itself always worked — which is the
worst shape for this bug, because the receipt is the thing an operator trusts when the
lane doesn't work.
Root cause
talk_grok_auth._inspect_storeknew one of the two shapes a Hermesxai-oauthlogin lives in:
providers["xai-oauth"]["tokens"]. A current host writes a device-codelogin into
credential_pool["xai-oauth"]instead — a list whose rows carryaccess_token/refresh_tokenflat, not nested undertokens.On this box
providersis{}and the pool holdsopenai-api, copilot, gemini, openai-codex, xai-oauth. So the parse fell offproviders.get("xai-oauth") is Noneandreturned
missing— for every operator who logged in on a current Hermes.The host rules I mirrored (read, not guessed)
_inspect_storenow mirrorshermes_cli.auth._xai_oauth_state_from_store(
hermes_cli/auth.py:5287-5321) end to end. That is the right function to mirrorbecause it is what
resolve_xai_oauth_runtime_credentialsreads(
auth.py:5846), and that resolver is what_resolve_via_host()calls — so thediagnostic now predicts the lane it is describing.
providersblock is read first, pool secondauth.py:5289-5295then:5297-5320access_tokenandrefresh_tokenrequired,.strip()-non-blankauth.py:5291-5294(providers),:5307-5311(pool)auth.py:7891-7896prioritysort, nolast_statusreadauth.py:5303-5320auth.py:5305-5306auth.py:5303, and the pool reader:2277-2282Two places I deliberately did not follow the brief, and why
Order is
providers→ pool, not pool → providers. The host has two entrypoints and they disagree: its status helper
get_xai_oauth_auth_status(
auth.py:7828-7845) is pool-first viaload_pool(), but the resolver Talkactually calls is providers-first. Since this diagnostic exists to predict
_resolve_via_host(), I followed the resolver. (On the live box both orders givethe same answer —
providersis empty.)A single-dict pool slice is not tolerated. The brief asked for it; the host
refuses it in both readers (
isinstance(entries, list)). Tolerating it wouldreport
validfor a shape the host will never read — a false positive, which isstrictly worse than the bug being fixed here. Test:
test_a_non_list_pool_slice_yields_no_login_like_the_host.Contract preserved
missing/invalid/expired/valid.new tests assert byte-and-mtime identity of the store via the existing
_Snapshot._EXPIRY_MARGIN_Slogic untouched.providersblock no longershort-circuits to
invalid— it falls through to the pool(
test_a_pool_login_survives_a_non_dict_providers_block).Where the login was found is now in the receipt
New field
xai_oauth_source→providers/credential_pool/null, allowlisted intalk_diagnostics.py.talk doctorrenders:So the next person debugging this can tell an empty store from an unread one without
opening
auth.json.Before / after on the live box
Same store, same process, tokens never read into the output —
grok_auth_diagnostic()returns no token-bearing keys, and the assertion below is on state strings only.
Before (
origin/main'stalk_grok_auth.py, loaded against the live store):{ "configured": false, "winning_lane": null, "preference": "absent", "xai_oauth": "missing", "host_refresh_available": false, "metered_key_present": false, "refresh_required": false, "blocked_by": "no-usable-auth" }After (this branch):
{ "configured": true, "winning_lane": "xai-oauth", "preference": "absent", "xai_oauth": "valid", "xai_oauth_source": "credential_pool", "host_refresh_available": false, "metered_key_present": false, "refresh_required": false, "blocked_by": null }hermes talk doctor --probewas not re-run — it makes live calls and had alreadypassed.
Tests
13 new in
tests/test_grok_auth.py(62 in that file, up from 49) + 1 new intests/test_doctor.py, and 1 existing doctor test updated for the new receipt suffix.test_pool_only_login_is_valid_and_names_the_pooltest_pool_only_login_resolves_without_touching_the_storetest_legacy_providers_login_still_wins_and_names_providerstest_providers_is_read_before_the_pool_like_the_hosttest_unusable_providers_entry_falls_through_to_the_poolprovidersblock can't mask a good pool rowtest_empty_pool_is_missing_not_invalidtest_quarantined_pool_row_is_invalidtest_expired_pool_token_is_expiredtest_malformed_rows_are_skipped_for_the_first_usable_onetest_pool_row_without_a_refresh_token_is_refusedtest_a_non_list_pool_slice_yields_no_login_like_the_hosttest_a_pool_login_survives_a_non_dict_providers_blocktest_the_pool_receipt_never_carries_the_tokentest_human_report_names_the_credential_pool_as_the_source12 of the 13 fail against
origin/main— including the headlineassert 'missing' == 'valid'. (The 13th, the no-token-in-receipt check, is a safetyinvariant that correctly holds on both.)
Mutation spot-check — 3 mutants, all killed:
refresh_tokenrequirementtest_pool_row_without_a_refresh_token_is_refused,test_malformed_rows_are_skipped_for_the_first_usable_one,test_file_fallback_blank_refresh_token_is_invalidmissingtest_quarantined_pool_row_is_invalid,test_pool_row_without_a_refresh_token_is_refused,test_file_fallback_blank_refresh_token_is_invalidtest_a_non_list_pool_slice_yields_no_login_like_the_hostGates
uv run --extra dev pytest -q→ 1651 passed, 40 skipped, 5 xfailed, 0 failed.Note: the expected baseline of 12 failures in
test_capabilities.py/test_cli.py(test: host-summary and capabilities tests fail on any box where hermes-agent is importable #93) did not reproduce on this box — and this diff touches neither file.
uv run --extra dev ruff check .→ All checks passed!git diff --stat==git diff --ignore-all-space --stat(byte-identical; no line-ending drift).Sibling scan — checked by reading, not assumed
git grep '"providers"'over the repo returns exactly one production reader of theHermes auth store:
talk_grok_auth.py. The others are not affected:talk_auth.pyreads$CODEX_HOME/auth.json(_codex_auth_path,talk_auth.py:116-121) — a different file with a different schema. Confirmed byreading the path builder, not by the module name.
talk_core_provider.py:497only callstalk_auth.auth_diagnostic()(that same Codexlane); its
auth.jsonmention is a docstring.talk_vault.py:87isgetattr(manager, "providers", ())— a memory-managerattribute, unrelated to any auth store.
One deviation from the brief worth flagging
The CHANGELOG entry went under the existing
## [Unreleased]→### Fixed. There is no## [0.17.0] — 2026-09-03section onmain(9f39e46) — the top section is[Unreleased]and the repo is still at0.16.0in bothpyproject.tomlandplugin.yaml. I did not invent a version heading, to avoid colliding with the releasetagging. Move it if 0.17.0 is cut from this branch.
🤖 Generated with Claude Code