From 1c9275ee83cefc15987019f7f2ac86a384d312e5 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:07:20 -0700 Subject: [PATCH] =?UTF-8?q?refactor(hermes=5Fcli):=20auth=5Fxai=20?= =?UTF-8?q?=E2=80=94=20one=20URL=20classifier=20behind=20both=20origin=20v?= =?UTF-8?q?alidators;=20pack=20marker=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hermes_cli/approvals_suggest.py | 14 +++------ hermes_cli/auth_minimax.py | 1 - hermes_cli/auth_xai.py | 52 ++++++++++++++++++++------------- hermes_cli/auth_zai_kimi.py | 1 - 4 files changed, 35 insertions(+), 33 deletions(-) diff --git a/hermes_cli/approvals_suggest.py b/hermes_cli/approvals_suggest.py index 231338cf14..0dcd67f764 100644 --- a/hermes_cli/approvals_suggest.py +++ b/hermes_cli/approvals_suggest.py @@ -81,16 +81,10 @@ _UNSAFE_ROOT_PREFIXES = ("mkfs",) # Substrings in a role='tool' result that mean the command did NOT execute with user consent # (blocked, denied, timed out, or still pending). Kept in sync with tools/approval.py templates. _BLOCK_MARKERS = ( - "BLOCKED (hardline)", - "BLOCKED: User denied", - "BLOCKED: Action ", - "BLOCKED: Command flagged as dangerous", - "BLOCKED: approval required", - "BLOCKED: Failed to send approval request", - "The user has NOT consented", - "Asking the user for approval", - "approval_required", - "BLOCKED by user deny rule", + "BLOCKED (hardline)", "BLOCKED: User denied", "BLOCKED: Action ", + "BLOCKED: Command flagged as dangerous", "BLOCKED: approval required", + "BLOCKED: Failed to send approval request", "The user has NOT consented", + "Asking the user for approval", "approval_required", "BLOCKED by user deny rule", ) diff --git a/hermes_cli/auth_minimax.py b/hermes_cli/auth_minimax.py index 3dc798ea14..95b51e3fb5 100644 --- a/hermes_cli/auth_minimax.py +++ b/hermes_cli/auth_minimax.py @@ -27,7 +27,6 @@ if TYPE_CHECKING: # annotation-only; the runtime import would be a cycle # Log-record parity with the origin module (caplog tests pin "hermes_cli.auth"). logger = logging.getLogger("hermes_cli.auth") - _MINIMAX_OAUTH_ERROR_BODY_LIMIT = 16 * 1024 diff --git a/hermes_cli/auth_xai.py b/hermes_cli/auth_xai.py index 92b546cea2..1b43082b3e 100644 --- a/hermes_cli/auth_xai.py +++ b/hermes_cli/auth_xai.py @@ -211,6 +211,19 @@ def _is_xai_origin_host(host: str) -> bool: return host == "x.ai" or host.endswith(".x.ai") +def _xai_url_problem(url: str) -> tuple[Optional[str], str]: + """``(problem, host)`` — problem is ``"scheme"``, ``"host"``, ``"origin"`` or None when *url* is HTTPS on x.ai.""" + parsed = urlparse(url) + host = (parsed.hostname or "").lower() + if parsed.scheme != "https": + return "scheme", host + if not host: + return "host", host + if not _is_xai_origin_host(host): + return "origin", host + return None, host + + def _xai_validate_oauth_endpoint(url: str, *, field: str) -> str: """Refuse any OIDC discovery endpoint that isn't HTTPS on the xAI origin. @@ -218,21 +231,20 @@ def _xai_validate_oauth_endpoint(url: str, *, field: str) -> str: ``token_endpoint`` that receives the refresh_token forever. Pinning scheme + host (RFC 8414 §2) removes that persistence. """ - parsed = urlparse(url) - if parsed.scheme != "https": - raise _xai_err(f"xAI OIDC discovery returned a non-HTTPS {field}: {url!r}.", "xai_discovery_invalid") - host = (parsed.hostname or "").lower() - if not host: - raise _xai_err(f"xAI OIDC discovery {field} is missing a hostname: {url!r}.", "xai_discovery_invalid") - if not _is_xai_origin_host(host): - raise _xai_err( + problem, host = _xai_url_problem(url) + if problem is None: + return url + message = { + "scheme": f"xAI OIDC discovery returned a non-HTTPS {field}: {url!r}.", + "host": f"xAI OIDC discovery {field} is missing a hostname: {url!r}.", + "origin": ( f"xAI OIDC discovery {field} host {host!r} is not on the xAI origin " f"(expected x.ai or a *.x.ai subdomain). Refusing to use a cached " f"endpoint that may have been substituted by a MITM during initial " - f"discovery; re-authenticate with `hermes model` to re-fetch.", - "xai_discovery_invalid", - ) - return url + f"discovery; re-authenticate with `hermes model` to re-fetch." + ), + }[problem] + raise _xai_err(message, "xai_discovery_invalid") def _xai_validate_inference_base_url(value: str, *, fallback: str) -> str: @@ -246,22 +258,21 @@ def _xai_validate_inference_base_url(value: str, *, fallback: str) -> str: if not candidate: return fallback try: - parsed = urlparse(candidate) + problem, host = _xai_url_problem(candidate) except Exception: logger.warning("Ignoring malformed xAI base_url override %r; using %s instead.", candidate, fallback) return fallback - if parsed.scheme != "https": + if problem is None: + return candidate + if problem == "scheme": logger.warning( "Refusing non-HTTPS xAI base_url override %r (xai-oauth bearer would " "be sent in cleartext); falling back to %s.", candidate, fallback, ) - return fallback - host = (parsed.hostname or "").lower() - if not host: + elif problem == "host": logger.warning("Ignoring xAI base_url override %r with no hostname; using %s instead.", candidate, fallback) - return fallback - if not _is_xai_origin_host(host): + else: logger.warning( "Refusing xAI base_url override %r — host %r is not on the xAI origin " "(expected x.ai or a *.x.ai subdomain). The xai-oauth bearer is only " @@ -269,8 +280,7 @@ def _xai_validate_inference_base_url(value: str, *, fallback: str) -> str: "the credential. Falling back to %s.", candidate, host, fallback, ) - return fallback - return candidate + return fallback def _xai_oauth_discovery(timeout_seconds: float = 15.0) -> Dict[str, str]: diff --git a/hermes_cli/auth_zai_kimi.py b/hermes_cli/auth_zai_kimi.py index 64ab2733d3..78b206a9cf 100644 --- a/hermes_cli/auth_zai_kimi.py +++ b/hermes_cli/auth_zai_kimi.py @@ -16,7 +16,6 @@ from hermes_cli.auth_constants import httpx # Log-record parity with the origin module (caplog tests pin "hermes_cli.auth"). logger = logging.getLogger("hermes_cli.auth") - # Kimi Code (kimi.com/code) issues "sk-kimi-" keys that only work on api.kimi.com/coding; legacy # platform.moonshot.ai keys work on api.moonshot.ai/v1 (the old default). Intentionally NO /v1 # suffix: the /coding endpoint speaks Anthropic Messages and the SDK appends "/v1/messages"