Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions hermes_cli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2470,6 +2470,32 @@ def do_GET(self) -> None: # noqa: N802
"error_description": params.get("error_description", [None])[0],
}

# Diagnostic logging — emits at INFO so reporters of loopback bugs
# (#27385 — "callback received but Hermes times out") can produce
# actionable evidence without a code change. Logged values are
# fingerprints / booleans only; no actual code/state strings leak
# into the log file. Run with ``HERMES_LOG_LEVEL=INFO`` (or check
# ``~/.hermes/logs/agent.log`` which captures INFO+ unconditionally).
try:
logger.info(
"xAI loopback callback received: path=%s has_code=%s has_state=%s has_error=%s "
"ua=%s",
parsed.path,
incoming["code"] is not None,
incoming["state"] is not None,
incoming["error"] is not None,
(self.headers.get("User-Agent") or "")[:80],
)
if incoming["error"]:
logger.info(
"xAI loopback callback carries error=%s error_description=%s",
incoming["error"],
(incoming["error_description"] or "")[:200],
)
except Exception:
# Logging must never break the OAuth flow.
pass

# Treat a hit on the callback path with neither `code` nor `error`
# as a missing OAuth callback (e.g. xAI's auth backend failed to
# redirect and the user navigated to the bare loopback URL by hand).
Expand Down Expand Up @@ -2574,6 +2600,17 @@ def _xai_wait_for_callback(
server.shutdown()
server.server_close()
thread.join(timeout=1.0)
# Diagnostic: distinguish "no callback ever arrived" from "callback
# arrived but result wasn't populated" (#27385). The per-hit handler
# also logs at INFO; if neither line appears, xAI's IDP never reached
# the loopback at all (firewall, port-binding, IPv6/IPv4 mismatch).
logger.info(
"xAI loopback wait timed out after %.0fs with no usable callback "
"(result.code=%s result.error=%s)",
max(5.0, timeout_seconds),
result["code"] is not None,
result["error"] is not None,
)
raise AuthError(
"xAI authorization timed out waiting for the local callback.",
provider="xai-oauth",
Expand Down
Loading