Shutting down the callback server stopped the serve thread but left the
worker spinning in _xai_wait_for_callback (which polls callback_result)
until the timeout. Flag callback_result as cancelled on DELETE so the
wait returns promptly and the daemon thread exits — avoids thread
buildup on repeated cancel/retry.
- onboarding: openSignInUrl now falls back to window.open when the desktop
bridge's openExternal throws/rejects (OS handler missing, user denied),
not just when the bridge is absent
- web_server: cancelling a loopback session shuts down the 127.0.0.1
callback server + joins its thread immediately, freeing the port instead
of holding it until the wait times out (+ regression test)
- web_server: document the new "loopback" flow in the /api/providers/oauth
enum, the poll-endpoint docstring, and the Phase 2 flow comment block
- web_server: join the callback-server thread in the start error path so a
failed discovery/URL build doesn't leave a daemon thread running
- web_server: loopback worker now bails if the session was cancelled while
waiting for the callback or exchanging the code, instead of persisting
tokens the user no longer wants (+ regression test)
- onboarding: fall back to window.open when the desktop bridge's
openExternal is unavailable, so the flow never silently stalls
xAI Grok was only reachable via the "I have an API key" form. xAI's
OAuth (SuperGrok / Premium+) flow already exists in the backend
(`hermes auth add xai-oauth`) but was never surfaced in the desktop
onboarding launcher.
Add a loopback PKCE flow: the local backend binds the 127.0.0.1
callback listener, the client opens the browser, and the redirect lands
back automatically — no code to copy/paste. Reuses the existing xAI
OAuth helpers (discovery, callback server, token exchange, persist)
rather than duplicating them.
- web_server: catalog entry (flow: loopback) + status dispatch +
_start_xai_loopback_flow + background worker + route branch
- desktop: 'loopback' flow type, awaiting_browser status, xAI Grok card
(PROVIDER_DISPLAY / FLOW_SUBTITLES / FlowPanel waiting render)
- tests: catalog listing, start authorize-url, worker persist, state
mismatch rejection
* fix(desktop): codex OAuth onboarding now resolves on fresh install
The desktop codex device-code worker persisted tokens with a hand-rolled
pool.add_entry(), writing only credential_pool.openai-codex. It never set
active_provider, so on a fresh install the onboarding setup.runtime_check
resolved provider "auto", couldn't detect the Codex OAuth session, and raised
"No inference provider configured" — while setup.status (which sniffs the pool)
reported configured. The disagreement surfaced as the onboarding banner
"Connected, but Hermes still cannot resolve a usable provider."
Use the canonical _save_codex_tokens() instead, matching the CLI's
`hermes auth add openai-codex` path and the Nous/MiniMax dashboard workers.
It writes the providers.openai-codex singleton (setting active_provider) and
syncs the pool.
* fix(auth): align Codex OAuth persistence paths
Ensure desktop and CLI Codex OAuth logins both write the canonical provider state so fresh installs resolve a usable runtime provider.
---------
Co-authored-by: teknium1 <127238744+teknium1@users.noreply.github.com>
Handle MiniMax OAuth expiry values consistently across CLI and dashboard
flows, fix CLI status/add behavior, and force pooled OAuth runtime
requests through Anthropic Messages.
- web_server._minimax_poller: parse expired_in via the shared resolver
so unix-ms absolute timestamps stop landing as TTL seconds and crashing
with 'year 583911 is out of range' when a user connects MiniMax OAuth
from the dashboard.
- auth._minimax_oauth_login / _refresh_minimax_oauth_state: same fix on
the CLI login + refresh paths.
- auth.get_auth_status: dispatch minimax-oauth to its dedicated status
function instead of falling through.
- auth_commands.auth_add_command: 'hermes auth add minimax-oauth' now
starts the device-code login flow and persists a pool entry with the
access + refresh tokens, instead of requiring credentials to already
exist.
- runtime_provider._resolve_runtime_from_pool_entry: pin pooled
minimax-oauth credentials to anthropic_messages so a stale
model.api_mode: chat_completions can't send requests to
/anthropic/chat/completions and trigger MiniMax nginx 404s.
Co-authored-by: Cursor <cursoragent@cursor.com>
Fixes#22832.
## Root cause
`hermes_cli/web_server.py:start_oauth_login` dispatched OAuth flows by
the catalog's `flow` field rather than provider id:
if catalog_entry["flow"] == "pkce":
return _start_anthropic_pkce()
The catalog had two `flow: "pkce"` entries — `anthropic` and
`minimax-oauth` — so clicking "Login" on MiniMax in the dashboard's
Keys tab unconditionally launched the Anthropic/Claude PKCE flow.
## Fix
Three changes in `hermes_cli/web_server.py`:
1. Catalog entry for `minimax-oauth` changed from `flow: "pkce"` to
`flow: "device_code"`. From a UX perspective MiniMax is a
verification-URI + user-code flow (open URL, enter code, backend
polls) — same shape as Nous's device-code flow. The PKCE bit
(verifier + challenge from `_minimax_pkce_pair`) is a security
extension that doesn't change the operator experience; the existing
dashboard modal already renders `device_code` correctly for this UX.
2. New MiniMax branch in `_start_device_code_flow`, mirroring the
existing Nous branch but calling MiniMax-specific helpers
(`_minimax_request_user_code`, `_minimax_pkce_pair`). Stashes
verifier + state in the session for the poller to consume. Handles
the overloaded `expired_in` field (could be unix-ms timestamp OR
seconds-from-now duration) the same way `_minimax_poll_token` does.
3. New `_minimax_poller` background thread mirroring `_nous_poller`.
Calls `_minimax_poll_token` → on success builds the same
`auth_state` dict the CLI flow (`_minimax_oauth_login`) builds, and
persists via `_minimax_save_auth_state` so the dashboard path leaves
the system in the same state as `hermes auth add minimax-oauth`.
Plus a dispatcher tightening to prevent regression: the `pkce` branch
now requires `provider_id == "anthropic"`, so any future PKCE provider
added without a proper start function gets a clean
`400 Unsupported flow` rather than silently launching Anthropic OAuth.
## Test
New `tests/hermes_cli/test_web_oauth_dispatch.py`:
- Regression test asserting MiniMax start does NOT return claude.ai
- Sanity test that Anthropic PKCE still works after the dispatcher
tightening
- Forward-looking test: a hypothetical pkce-flagged provider without
an explicit branch is rejected cleanly rather than misrouted
## Limitations
- The dashboard MiniMax path defaults to `region="global"`. CN-region
operators can still use the CLI flow which supports `--region cn`.
Adding a region toggle to the dashboard UI is a follow-up.