
    `gj              	       *   U d Z ddlZddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddlZddlZddlmZ ddlmZmZ ddlmZ ddlmZ ddlmZmZ ddlmZ  ej8                  e      Zd	Z	 dd
l m!Z! ddl"m#Z#m$Z$m%Z%m&Z& dZ	 ddl)m*Z*  G d de+      Z,da-e.dz  e/d<    ej`                  dd      Z1de/d<    ej`                  dd	      Z2de/d<    e3h d      Z4dZ5dHde6ez  dz  defdZ7de6de6fdZ8de.fdZ9i Z:de/d<   d Z;de.fd!Z<d"d#de.dz  fd$Z=d"d#de6dz  fd%Z>de?fd&Z@d'e6ddfd(ZAed)        ZBed*        ZCde?fd+ZDd,edeEdz  fd-ZFd,ed.eEddfd/ZG G d0 d1      ZHdeIeJeEf   fd2ZKdHd3e.d4e6dz  fd5ZLdeIe6e6dz  f   fd6ZMd3e.fd7ZNd8eEddfd9ZOdd:d;e6de6ez  dz  ddfd<ZP	 dHd=eEd"d#de.fd>ZQd=eEd3e.de6fd?ZRd=eEdd@fdAZSd"d1d=eEdBd@ddfdCZT	 dHd;e6dDe6dEeEdz  ddFfdGZUy# e'$ r ejQ                  d       Y zw xY w# e'$ r dZ*Y w xY w)Ia  
MCP OAuth 2.1 Client Support

Implements the browser-based OAuth 2.1 authorization code flow with PKCE
for MCP servers that require OAuth authentication instead of static bearer
tokens.

Uses the MCP Python SDK's ``OAuthClientProvider`` (an ``httpx.Auth`` subclass)
which handles discovery, dynamic client registration, PKCE, token exchange,
refresh, and step-up authorization automatically.

This module provides the glue:
    - ``HermesTokenStorage``: persists tokens/client-info to disk so they
      survive across process restarts.
    - Callback server: ephemeral localhost HTTP server to capture the OAuth
      redirect with the authorization code.
    - ``build_oauth_auth()``: entry point called by ``mcp_tool.py`` that wires
      everything together and returns the ``httpx.Auth`` object.

Configuration in config.yaml::

    mcp_servers:
      my_server:
        url: "https://mcp.example.com/mcp"
        auth: oauth
        oauth:                                  # all fields optional
          client_id: "pre-registered-id"        # skip dynamic registration
          client_secret: "secret"               # confidential clients only
          scope: "read write"                   # default: server-provided
          redirect_port: 0                      # 0 = auto-pick free port
          redirect_uri: "https://proxy/callback"  # default: loopback callback
          redirect_host: "localhost"            # loopback hostname (WAF-safe)
          client_name: "My Custom Client"       # default: "Hermes Agent"
    N)contextmanager)BaseHTTPRequestHandler
HTTPServer)Path)Any)parse_qsurlparse)secure_parent_dirF)OAuthClientProvider)OAuthClientInformationFullOAuthClientMetadataOAuthMetadata
OAuthTokenTz8MCP OAuth types not available -- OAuth MCP auth disabled)AnyUrlc                       e Zd ZdZy)OAuthNonInteractiveErrorzHRaised when OAuth requires browser interaction in a non-interactive env.N)__name__
__module____qualname____doc__     B/root/.hermes/venv/lib/python3.12/site-packages/tools/mcp_oauth.pyr   r   X   s    Rr   r   _oauth_port_oauth_interactive_enabled)defaultzcontextvars.ContextVar[bool]_oauth_interactive_forced>   nqsnoquitskipcancel__hermes_user_skipped__hermes_homereturnc           
         	 ddl m} | t        |       nt         |             }|dz  S # t        $ rN t        t        j
                  j                  dt        t        j                         dz                    }Y |dz  S w xY w)zReturn the directory for MCP OAuth token files.

    Uses HERMES_HOME so each profile gets its own OAuth tokens.
    Layout: ``HERMES_HOME/mcp-tokens/``
    r   )get_hermes_homeHERMES_HOMEz.hermesz
mcp-tokens)	hermes_constantsr)   r   ImportErrorosenvirongetstrhome)r&   r)   bases      r   _get_token_dirr3      sx    Q4$/$;tK oFWAX ,  QBJJNN=#diikI6M2NOP,Qs   #* AB Bnamec                 \    t        j                  dd|       j                  d      dd xs dS )zBSanitize a server name for use as a filename (no path separators).z[^\w\-]_N   r   )resubstrip)r4   s    r   _safe_filenamer;      s,    66*c4(..s3DS9FYFr   c                      t        j                   t         j                  t         j                        5 } | j                  d       | j	                         d   cddd       S # 1 sw Y   yxY w)z(Find an available TCP port on localhost.	127.0.0.1r      N)socketAF_INETSOCK_STREAMbindgetsockname)r    s    r   _find_free_portrE      sL    	v~~v'9'9	: "a	 }}q!" " "s   $A!!A*zdict[int, socket.socket]_reserved_sockets   c                  F   t        j                   t         j                  t         j                        } 	 | j                  d       | j                         d   }t        t              t        k\  r}t        t        t        j                                     \  }}t        j                  t        t        t                    d       	 |j                          t        t              t        k\  r}| t        |<   |S # t        $ r | j                           w xY w# t        $ r Y Kw xY w)a?  Pick an ephemeral callback port and keep its socket bound.

    Returns the port. The bound (not yet listening) socket is parked in
    ``_reserved_sockets`` so no other process can bind the port before
    ``_wait_for_callback`` adopts it. Adoption (or ``server_close``) owns
    the socket's lifetime from there.
    r=   r?   N)r@   rA   rB   rC   OSErrorcloserD   lenrF   _MAX_RESERVED_SOCKETSnextiteritemspop)r    portr6   stales       r   _reserve_callback_portrS      s     	fnnf&8&89A	  ==?1D

 $9
9.446785d4(9#:;TB	KKM	 
 $9
9  dK  		  		s   C6 D 6D	D D storagezHermesTokenStorage | Nonec                    | y	 t        | j                               }|sy|j                  d      xs g D ]i  }	 t        t        |            }|j                  dk(  s(|j                  dv s7|j                  dk(  sG|j                  Tt        |j                        c S  y# t        t        t        f$ r Y yw xY w# t        t        f$ r Y w xY w)a  Return the loopback callback port from cached client registration.

    OAuth providers bind a dynamically-registered ``client_id`` to the exact
    redirect URI that was registered with it. If Hermes restarts and chooses a
    new random callback port while reusing the stored ``client_id``, providers
    such as Summ reject the authorization request with ``redirect_uri does not
    match any registered URIs``. Reusing the cached redirect port keeps the
    authorization request consistent with the stored client registration.
    Nredirect_urishttp>   	localhostr>   	/callback)
_read_json_client_info_pathAttributeError	TypeError
ValueErrorr/   r	   r0   schemehostnamepathrQ   intrT   datauriparseds       r   _cached_redirect_portrg      s     '3356 xx(.B $	c#h'F MMV##=={*'v{{##$ # Iz2  :& 		s"   B# B=#B:9B:=CCc                 T   | y	 t        | j                               }|xs i j                  d      xs g D ]@  }	 t        t        |            }|j                  dk(  s(|j                  s5t        |      c S  y# t        t        t        f$ r Y yw xY w# t        t        f$ r Y ow xY w)zAReturn a cached non-loopback redirect URI, if one was registered.NrV   https)
rZ   r[   r\   r]   r^   r/   r	   r0   r_   netlocrc   s       r   _cached_redirect_urirk      s    '3356 
06B 	c#h'F ==G#s8O  Iz2 
 :& 		s"   A; B;BBB'&B'c                      t         j                         syt        j                         ry	 t        j                  j                         S # t        t        f$ r Y yw xY w)z@Return True if we can reasonably expect to interact with a user.FT)r   r/   r   sysstdinisattyr\   r^   r   r   r   _is_interactiverp      sO    %))+ $$&yy!!J' s   A
 
AAleadc                 4    t               st        |  d      y)a7  Raise ``OAuthNonInteractiveError`` unless an interactive session exists.

    ``lead`` is the boundary-specific first sentence; this helper appends the
    shared, actionable ``hermes mcp login`` next-step so the guidance wording
    lives in one place across every non-interactive OAuth boundary (#57836).
    zd Run `hermes mcp login <server>` interactively to (re)authorize, then restart or reload the gateway.N)rp   r   )rq   s    r   _raise_if_non_interactivers     s+     &f 2 2
 	
 r   c               #      K   t         j                  d      } 	 d t         j                  |        y# t         j                  |        w xY ww)ua  Treat the current execution context as interactive despite no TTY.

    For GUI-driven auth (dashboard/desktop REST endpoint): the user IS present
    — just not on stdin. Opens the browser + localhost callback flow that the
    TTY heuristic would otherwise refuse. Same ContextVar propagation story as
    suppress_interactive_oauth() (#35927).
    TN)r   setresettokens    r   force_interactive_oauthry     s=      &))$/E/!''.!''.   A3 AA

Ac               #      K   t         j                  d      } 	 d t         j                  |        y# t         j                  |        w xY ww)u}  Disable stdin-based OAuth prompts for the current execution context.

    Uses a ContextVar so the suppression propagates from a background-discovery
    thread onto the coroutine scheduled (via run_coroutine_threadsafe) on the
    dedicated MCP event-loop thread — where the OAuth callback actually runs
    (#35927). A threading.local would not cross that thread boundary.
    FN)r   ru   rv   rw   s    r   suppress_interactive_oauthr|   %  s=      '**51E0"((/"((/rz   c                     t         j                  j                  d      st         j                  j                  d      ryt         j                  dk(  ry	 t        j                         j
                  dk(  ry	 t         j                  j                  d      st         j                  j                  d      ryy# t        $ r Y Kw xY w)	z3Return True if opening a browser is likely to work.
SSH_CLIENTSSH_TTYFntTDarwinDISPLAYWAYLAND_DISPLAY)r-   r.   r/   r4   unamesysnamer\   r   r   r   _can_open_browserr   5  s     
zz~~l#rzz~~i'@	ww$88:) *
 
zz~~i BJJNN3D$E  s   !B8 8	CCra   c                     | j                         sy	 t        j                  | j                  d            S # t        j                  t
        f$ r!}t        j                  d| |       Y d}~yd}~ww xY w)zCRead a JSON file, returning None if it doesn't exist or is invalid.Nutf-8encodingzFailed to read %s: %s)existsjsonloads	read_textJSONDecodeErrorrI   loggerwarning)ra   excs     r   rZ   rZ   H  s\    ;;=zz$..'.:;;  '* .c:s   $8 A2A--A2rd   c                 2   | j                   j                  dd       t        |        | j                  dt	        j
                          dt        j                  d             }	 t	        j                  t        |      t        j                  t        j                  z  t        j                  z  t        j                  t        j                  z        }t	        j                   |dd      5 }t#        j$                  ||d	t        
       |j'                          t	        j(                  |j+                                ddd       t	        j,                  ||        y# 1 sw Y    xY w# t.        $ r$ 	 |j1                  d        # t.        $ r Y  w xY ww xY w)a  Write a dict as JSON with restricted permissions (0o600).

    Uses ``os.open`` with ``O_EXCL`` and an explicit mode so the file is
    created atomically at 0o600. The previous ``write_text`` + post-write
    ``chmod`` opened a TOCTOU window where the temp file briefly inherited
    the process umask (commonly 0o644 = world-readable), exposing OAuth
    tokens to other local users between create and chmod. Mirrors the fix
    in ``agent/google_oauth.py`` (#19673).
    Tparentsexist_okz.tmp..   wr   r      )indentr   N
missing_ok)parentmkdirr
   with_suffixr-   getpidsecrets	token_hexopenr0   O_WRONLYO_CREATO_EXCLstatS_IRUSRS_IWUSRfdopenr   dumpflushfsyncfilenoreplacerI   unlink)ra   rd   tmpfdfhs        r   _write_jsonr   S  s>    	KKdT2 d 

U299;-q1B1B11E0FG
HCWWHKK"**$ryy0LL4<<'

 YYr31 	"RIIdBq#6HHJHHRYY[!	" 	

3		" 	"
  	JJ$J' 	  		sJ   &BE) -AE>E) E&"E) )	F3FF	FFFFc                       e Zd ZdZdddedeez  dz  fdZdefdZdefd	Zdefd
Z	ddZ
ddZddZddZddZddZd dZdeeef   fdZdddeeef   deddfdZdefdZdefdZy)!HermesTokenStoragea6  Persist OAuth tokens and client registration to JSON files.

    File layout::

        HERMES_HOME/mcp-tokens/<server_name>.json         -- tokens
        HERMES_HOME/mcp-tokens/<server_name>.client.json   -- client info
        HERMES_HOME/mcp-tokens/<server_name>.meta.json     -- oauth server metadata
    Nr&   server_namer&   c                X    t        |      | _        |t        |      | _        y d | _        y N)r;   _server_namer   _hermes_home)selfr   r&   s      r   __init__zHermesTokenStorage.__init__  s(    *;71<1HD-dr   r'   c                 L    t        | j                        | j                   dz  S )Nz.jsonr3   r   r   r   s    r   _tokens_pathzHermesTokenStorage._tokens_path  s&    d//0d6G6G5H3NNNr   c                 L    t        | j                        | j                   dz  S )Nz.client.jsonr   r   s    r   r[   z$HermesTokenStorage._client_info_path  s&    d//0d6G6G5H3UUUr   c                 L    t        | j                        | j                   dz  S )Nz
.meta.jsonr   r   s    r   
_meta_pathzHermesTokenStorage._meta_path  s&    d//0d6G6G5H
3SSSr   c                   K   t        | j                               }|y |j                  dd       }|.t        t	        |t        j
                         z
  d            |d<   n{|j                  d      j	 | j                         j                         j                  }|?	 |t        |d         z   }t        t	        |t        j
                         z
  d            |d<   	 t        j                  |      S # t        $ r d }Y dw xY w# t        t        f$ r Y 8w xY w# t        t        t        f$ r/}t        j!                  d| j                         |       Y d }~y d }~ww xY ww)N
expires_atr   
expires_inz$Corrupt tokens at %s -- ignoring: %s)rZ   r   rP   rb   maxtimer/   r   st_mtimerI   r]   r^   r   model_validateKeyErrorr   r   )r   rd   absolute_expiry
file_mtimeimplied_expiryr   s         r   
get_tokenszHermesTokenStorage.get_tokens  sF    $++-.<  ((<6&!$S499;)F%J!KDXXl#/"!..0557@@
 %%/#d<6H2I%IN),S$))+1Mq-Q)RD&	,,T22  "!
" ":.  Ix0 	NNA4CTCTCVX[\	sr   A0E3(C2 E>D D 1E2D =E?D  EDEDEE,%EEEEc                 >  K   |j                  dd      }|j                  d      }|$	 t        j                         t        |      z   |d<   t        | j                         |       t        j                  d| j                         y # t        t
        f$ r Y Lw xY ww)Nr   Tmodeexclude_noner   r   zOAuth tokens saved for %s)
model_dumpr/   r   rb   r]   r^   r   r   r   debugr   )r   tokenspayloadr   s       r   
set_tokenszHermesTokenStorage.set_tokens  s     ##d#C [[.
!(,		c*o(E%
 	D%%'10$2C2CD z*  s(   'B#B ;BBBBBc                    K   t        | j                               }|y 	 t        j                  |      S # t        t
        t        f$ r/}t        j                  d| j                         |       Y d }~y d }~ww xY ww)Nz)Corrupt client info at %s -- ignoring: %s)	rZ   r[   r   r   r^   r]   r   r   r   r   rd   r   s      r   get_client_infoz"HermesTokenStorage.get_client_info  sk     $0023<	-<<TBBIx0 	NNFH^H^H`bef	s+   A;5 A;A8	%A3.A;3A88A;c                    K   t        | j                         |j                  dd             t        j	                  d| j
                         y w)Nr   Tr   zOAuth client info saved for %s)r   r[   r   r   r   r   )r   client_infos     r   set_client_infoz"HermesTokenStorage.set_client_info  s?     D**,k.D.D&_c.D.de5t7H7HIs   AAc                     t        | j                         |j                  dd             t        j	                  d| j
                         y )NTr   )r   r   zOAuth metadata saved for %s)r   r   r   r   r   r   )r   metadatas     r   save_oauth_metadataz&HermesTokenStorage.save_oauth_metadata  s9    DOO%x':':SY':'Z[2D4E4EFr   c                     t        | j                               }|y 	 t        j                  |      S # t        t
        t        f$ r/}t        j                  d| j                         |       Y d }~y d }~ww xY w)Nz,Corrupt OAuth metadata at %s -- ignoring: %s)	rZ   r   r   r   r^   r]   r   r   r   r   s      r   load_oauth_metadataz&HermesTokenStorage.load_oauth_metadata  sd    $//+,<	 //55Ix0 	NNI4??K\^ab	s   3 A6%A11A6c                     | j                         | j                         | j                         fD ]  }|j                  d        y)z.Delete all stored OAuth state for this server.Tr   N)r   r[   r   r   )r   ps     r   removezHermesTokenStorage.remove  s?    ##%t'='='?ARS 	&AHHH%	&r   c                     i }| j                         | j                         | j                         fD ]   }	 |j                         ||j                  <   " |S # t
        $ r Y 0w xY w)a5  Capture on-disk OAuth state so a failed re-auth can restore it.

        Maps filename -> bytes for whichever of the three state files exist.
        Feed back to ``restore()`` to undo an intervening ``remove()`` when a
        re-authentication attempt fails, so a still-valid token isn't destroyed.
        )r   r[   r   
read_bytesr4   rI   )r   snapr   s      r   snapshotzHermesTokenStorage.snapshot  sk     "$##%t'='='?ARS 	A ||~QVV	
   s   A	A#"A#F)only_if_absentr   r   c                *   |r`t        d | j                         | j                         | j                         fD              r!t        j                  d| j                         y| j                          |syt        | j                        }|j                  dd       |j                         D ]  \  }}||z  }	 t        j                  t        |      t        j                  t        j                   z  t        j"                  z  t$        j&                  t$        j(                  z        }t        j*                  |d      5 }|j-                  |       ddd        y# 1 sw Y   xY w# t.        $ r!}	t        j1                  d||	       Y d}	~	d}	~	ww xY w)zGRevert to a snapshot without overwriting a concurrent successful write.c              3   <   K   | ]  }|j                           y wr   )r   ).0ra   s     r   	<genexpr>z-HermesTokenStorage.restore.<locals>.<genexpr>  s      "
 KKM"
s   z9Skipping OAuth rollback for %s because newer state existsNTr   wbz$Failed to restore OAuth state %s: %s)anyr   r[   r   r   infor   r   r3   r   r   rO   r-   r   r0   r   r   O_TRUNCr   r   r   r   writerI   r   )
r   r   r   	token_dirfnamerd   ra   r   r   r   s
             r   restorezHermesTokenStorage.restore  sI   c "
**,d.D.D.FHYZ"
 
 KKK!! "4#4#45	t4#>>+ 	SKE4u$D	SWWIKK"**,rzz9LL4<</
 YYr4( #BHHTN#	S# # SEucRRSs1   :BE(?EE(E%	!E((	F1FFc                    | j                         }|j                         sy|j                  |j                  dz         }	 |j	                  |j                                |j                  d       | j                         j                  d       t        j                  d| j                  |j                         y# t        $ r!}t        j                  d||       Y d}~d}~ww xY w)u  Discard a dead dynamically-registered client so it gets re-created.

        Called when the IdP rejects our cached ``client_id`` with
        ``invalid_client`` on the token endpoint — proof the server-side
        registration is gone (IdP redeploy / DB wipe / rebrand). Deleting
        ``client.json`` makes the MCP SDK's ``async_auth_flow`` take the
        ``if not client_info`` branch and re-run RFC 7591 dynamic client
        registration on the next flow. The stale ``meta.json`` is dropped
        too so discovery re-runs against a freshly fetched document.

        Tokens are intentionally left in place — the subsequent
        re-authorization overwrites them, and keeping them avoids losing a
        still-valid refresh token if the re-registration never completes.

        A single ``.bak`` copy of the client file is kept for recovery.
        Returns True if a client file was present and removed.
        Fz.bakz'Could not back up client info at %s: %sNTr   zMCP OAuth '%s': cached client registration rejected as invalid_client; removed client.json + meta.json (backup at %s) to force re-registration)r[   r   	with_namer4   write_bytesr   rI   r   r   r   r   r   )r   client_pathbackupr   s       r   poison_client_registrationz-HermesTokenStorage.poison_client_registration*  s    $ ,,.!!#&&{'7'7&'@A	X{5578 	d+  D 1Vv{{	

   	XNNDkSVWW	Xs   B> >	C(C##C(c                 >    | j                         j                         S )z7Return True if we have tokens on disk (may be expired).)r   r   r   s    r   has_cached_tokensz$HermesTokenStorage.has_cached_tokensM  s      "))++r   )r'   zOAuthToken | None)r   r   r'   N)r'   z!OAuthClientInformationFull | None)r   r   r'   N)r   r   r'   N)r'   zOAuthMetadata | Noner'   N)r   r   r   r   r0   r   r   r   r[   r   r   r   r   r   r   r   r   dictbytesr   boolr   r   r  r   r   r   r   r   }  s     NR SC Std9J SOd OV4 VTD T
%NE,JG&
$sEz*  MR SS%Z 0 ST SVZ S:!D !F,4 ,r   r   c                  :    dddd G fddt               } | fS )aT  Create a per-flow callback HTTP handler class with its own result dict.

    Returns ``(HandlerClass, result_dict)`` where *result_dict* is a mutable
    dict that the handler writes ``auth_code`` and ``state`` into when the
    OAuth redirect arrives.  Each call returns a fresh pair so concurrent
    flows don't stomp on each other.
    N)	auth_codestateerrorc                   .    e Zd Zd fdZdededdfdZy)(_make_callback_handler.<locals>._Handlerr'   Nc                    t        t        | j                        j                        }|j	                  dd g      d   }|j	                  dd g      d   }|j	                  dd g      d   }|d<   |d<   |d<   |rdn	d|xs d d	}| j                  d
       | j                  dd       | j                          | j                  j                  |j                                y )Ncoder   r	  r
  r  zn<html><body><h2>Authorization Successful</h2><p>You can close this tab and return to Hermes.</p></body></html>z3<html><body><h2>Authorization Failed</h2><p>Error: unknownz</p></body></html>   zContent-Typeztext/html; charset=utf-8)r   r	   ra   queryr/   send_responsesend_headerend_headerswfiler   encode)r   paramsr  r	  r
  bodyresults         r   do_GETz/_make_callback_handler.<locals>._Handler.do_GETb  s    htyy1778F::ftf-a0DJJw/2EJJw/2E"&F;#F7O#F7O
 T"/i00BD	  s#^-GHJJT[[]+r   fmtargsc                 6    t         j                  d||z         y )NzOAuth callback: %s)r   r   )r   r  r  s      r   log_messagez4_make_callback_handler.<locals>._Handler.log_messagex  s    LL-sTz:r   r  )r   r   r   r  r0   r   r  )r  s   r   _Handlerr  a  s"    	,,	;3 	;s 	;t 	;r   r  )r   )r  r  s    @r   _make_callback_handlerr   W  s+     ,0$NF;) ;4 Vr   rQ   redirect_uric                 (     dt         ddf fd}|S )a  Return a redirect handler closure that closes over the given port.

    Using a closure instead of reading the module-level ``_oauth_port`` avoids
    cross-server state pollution when multiple MCP servers run OAuth
    concurrently (fixes #44588).

    ``redirect_uri`` is the configured proxy callback (e.g. a Tailscale Funnel
    URL), or ``None`` for the loopback default. It tailors the remote-session
    hint: a proxied callback reaches this machine on its own, so the loopback
    SSH-tunnel guidance would be misleading.
    authorization_urlr'   Nc           	        K   ddl m}  |       }||j                  |        d{    yt        d       d|  d}t	        |t
        j                         t        t        j                  d      xs t        j                  d	            }|r"r t	        d
 dt
        j                         n)|r'r%t	        d d d dt
        j                         t               rQ	 t        j                  |       }|rt	        dt
        j                         nt	        dt
        j                         yyt	        dt
        j                         y7 +# t        $ r t	        dt
        j                         Y yw xY ww)zShow the authorization URL to the user.

        Opens the browser automatically when possible; always prints the URL
        as a fallback for headless/SSH/gateway environments.
        r   get_dashboard_oauth_flowNzvMCP OAuth requires browser authorization but no interactive session is available (non-interactive/background context).zL
  MCP OAuth: authorization required.
  Open this URL in your browser:

    
filer~   r   zN  Remote session detected. After you authorize, the provider redirects to
    uU   
  which forwards to the callback listener on this machine — no SSH tunnel needed.
z_  Remote session detected. After you authorize, the provider redirects to
    http://127.0.0.1:u  /callback
  which only the listener on THIS machine can receive. Two options:

    1. Easiest — when your browser shows a connection error after
       authorizing, copy the full URL from the address bar and paste
       it at the prompt below. The pasted ``code=...&state=...`` is
       enough to complete the flow.

    2. Or forward the port first in a separate terminal:
         ssh -N -L z:127.0.0.1:z <user>@<this-host>
       then open the URL above and let it redirect normally.

  See: https://hermes-agent.nousresearch.com/docs/guides/oauth-over-ssh
z"  (Browser opened automatically.)
u=     (Could not open browser — please open the URL manually.)
u=     (Headless environment detected — open the URL manually.)
)tools.mcp_dashboard_oauthr&  publish_authorization_urlrs   printrm   stderrr  r-   getenvr   
webbrowserr   	Exception)r#  r&  dashboard_flowmsgon_sshopenedrQ   r!  s         r   _redirect_handlerz1_make_redirect_handler.<locals>._redirect_handler  s     	G13% ::;LMMM 	"I	
$%R) 	
 	c

#bii-E91EFl #n %hi ZZ	  ((,v 	.& '+V;tf =\] ZZ$ i#):;?cjjQZadakakl R RY\YcYcdM NF  iV]`]g]ghis5   $E?EB=E?&AE 4E?$E<9E?;E<<E?)r0   )rQ   r!  r5  s   `` r   _make_redirect_handlerr6    s%    Pe3 Pe4 Ped r   c                  h   K   t         t        d       t        t                       d{   S 7 w)u9  Wait for the OAuth callback on the legacy module-level port.

    Kept for backwards compatibility with callers that never went through
    :func:`build_oauth_auth`'s per-flow wiring. New code paths receive a
    per-flow waiter from :func:`_make_callback_waiter` so concurrent OAuth
    flows cannot cross ports (#34260).

    Raises:
        RuntimeError: If ``_oauth_port`` has not been set, which would indicate
            that ``build_oauth_auth`` was skipped — the asserting form below
            was a silent bug when running Python with ``-O``/``-OO``.
    Nu_   OAuth callback port not set — build_oauth_auth must be called before _wait_for_oauth_callback)r   RuntimeError_make_callback_waiterr   r   r   _wait_for_callbackr:    s9      .
 	
 4&{35555s   )202c                 @     dt         t        t        dz  f   f fd}|S )az  Return a callback waiter bound to a single OAuth flow's port.

    Closing over the port (instead of reading the module-level
    ``_oauth_port``) keeps concurrent OAuth flows isolated: flow A's waiter
    listens on flow A's port even when flow B's ``_configure_callback_port``
    overwrites the legacy global afterwards (#34260, the callback-side
    sibling of the #44588 redirect-handler fix).

    The waiter polls for the redirect without blocking the event loop. On an
    interactive TTY it races the HTTP listener against a stdin paste fallback
    so users without an SSH tunnel can paste the redirect URL (or just the
    ``code=...&state=...`` query string) from a browser on another machine.

    Raises (when awaited):
        OAuthNonInteractiveError: If the callback times out (no user present
            to complete the browser auth), or in non-interactive contexts.
    r'   Nc                  l  K   ddl m}   |        }||j                          d {   S t        d       t	               \  }}	 t        df|d      }t        j                  d       }|G|j                  j                          ||_        |j                         |_        |j                          n'd|_        |j                          |j                          t#        j$                  |j&                  d      }|j)                          d }t+               rIt-        dt.        j0                  d       t#        j$                  t2        |fd      }|j)                          d}	d}
d}	 ||	k  r3|d   |d   n(t5        j6                  |
       d {    ||
z  }||	k  r3|j9                          |d   t:        k(  rt!        d      |d   rt=        d|d          |d   t!        d      |d   |d   fS 7 # t        $ r}t!        d d	| d
      |d }~ww xY w7 # |j9                          w xY ww)Nr   r%  zOAuth callback requires an interactive session but none is available (non-interactive/background context); skipping browser authorization without binding a callback listener.r>   F)bind_and_activateTzOAuth callback port z is already in use (zk). Close any other in-progress login, or set a free `oauth.redirect_port` in the server config, then retry.)targetdaemonz
  Or paste the redirect URL here (or the ``?code=...&state=...`` portion) and press Enter. Type ``skip`` + Enter to continue without this server:)r)  r   )r>  r  r?  g     r@g      ?g        r  r
  user_skippedzOAuth authorization failed: uq   OAuth callback timed out — no authorization code received. Ensure you completed the browser authorization flow.r	  )r*  r&  wait_for_callbackrs   r   r   rF   rP   r@   rJ   rD   server_addressserver_activateallow_reuse_addressserver_bindrI   r   	threadingThreadhandle_requeststartrp   r,  rm   r-  _paste_callback_readerasynciosleepserver_close_USER_SKIPPED_SENTINELr8  )r&  r1  handler_clsr  serverreservedr   server_threadpaste_threadtimeoutpoll_intervalelapsedrQ   s               r   _waitz$_make_callback_waiter.<locals>._wait  st    F13%'99;;; 	"A	
 56V	d#[EF ),,T48H###% ((0(<(<(>%&&(-1*""$&&( "((0E0EdS 15' ZZ %++-VIdL  	"G#+&2fWo6QmmM222=(	 G# !'?44*>::'?!=fWo=NOPP+&*G 
 k"F7O33G <X  
	 +&tf,@ F4 4 	
	P 3 !s`   #H4G6H4BG9 BH4'(H HH AH49	HHHH4H H11H4)tupler0   )rQ   rW  s   ` r   r9  r9    s&    &h4sC$J/ h4T Lr   r  c                    	 t         j                  j                         }|sy|j                         }|sy| j                  d      | j                  d      y|j                         t        v rH| j                  d      | j                  d      yt        | d<   t        dt         j                         y|}d|v r|j                  dd      d   }|j                  d      r|dd }	 t        |      }|j                  d	dg      d
   }|j                  ddg      d
   }|j                  ddg      d
   }|s|st        dt         j                         y| j                  d      | j                  d      y|| d<   || d<   || d<   |rt        dt         j                         yy# t        t        t
        f$ r Y yw xY w# t
        t         f$ r t        dt         j                         Y yw xY w)u3  Read one line from stdin, parse it as an OAuth redirect, write to result.

    Accepts any of:
      - Full redirect URL: ``http://127.0.0.1:37949/callback?code=...&state=...``
      - The provider's own callback URL: ``https://mcp.example.com/callback?code=...&state=...``
      - Just the query string: ``?code=...&state=...`` or ``code=...&state=...``
      - A skip token (``skip``, ``cancel``, ``s``, ``n``, ``no``, ``q``, ``quit``)
        — exits the OAuth flow cleanly without auth. Caller raises
        :class:`OAuthNonInteractiveError` so MCP connection setup treats this
        as a non-fatal "user opted out" and continues without that server.

    Failures to parse, EOF, or interrupts are swallowed — this is best-effort
    fallback alongside the HTTP listener, which remains the primary path.
    Nr  r
  z  OAuth skipped. Run `hermes mcp login <server>` later to authenticate, or set ``enabled: false`` on that server in config.yaml to disable persistently.r(  ?r?   uA     Could not parse pasted input as an OAuth redirect — ignoring.r  r   r	  uD     Pasted input did not contain ``code=`` or ``error=`` — ignoring.u8     Got authorization code from paste — completing flow.)rm   rn   readlineKeyboardInterruptrI   r^   r:   r/   lower_SKIP_TOKENSrN  r,  r-  split
startswithr   r]   )r  liner  r  r  r	  r
  s          r   rJ  rJ  y  s   yy!!# ::<D zz+*fjj.A.M zz||#::k".&**W2E2Q0w3 		
 	 E
d{

3"1%ab	% ::ftf%a(DJJw'*EJJw'*ER	
 	 zz+*fjj.A.MF;F7OF7OHszzZ  w
3 L 	" O	
 	s#   F' )G 'F>=F>*G.-G.r   r   c                j    t        | |      }|j                          t        j                  d|        y)z8Delete stored OAuth tokens and client info for a server.r   zOAuth tokens removed for '%s'N)r   r   r   r   )r   r&   rT   s      r   remove_oauth_tokensrc    s)     !+FGNN
KK/=r   cfgc                 @   ddl m}  |       }|(d| d<   | j                  d      xs |j                  | d<   yt	        |      }| j                  d      s|r|| d<   d| d<   yt        | j                  dd            }|xs t        |      xs
 t               }|| d<   |a|S )a  Pick or validate the OAuth callback port.

    Stores the resolved port into ``cfg['_resolved_port']`` so sibling
    helpers (and the manager) can read it from the same dict. Returns the
    resolved port.

    Port choice precedence:
    1. explicit ``oauth.redirect_port`` config
    2. cached client registration redirect URI port
    3. newly allocated free port

    NOTE: also sets the legacy module-level ``_oauth_port`` so existing
    calls to ``_wait_for_callback`` keep working. The legacy global is
    the root cause of issue #5344 (port collision on concurrent OAuth
    flows); replacing it with a ContextVar is out of scope for this
    consolidation PR.
    r   r%  _resolved_portr!  redirect_port)	r*  r&  r/   r!  rk   rb   rg   rS   r   )rd  rT   r&  r1  cached_redirect_uri	requestedrQ   s          r   _configure_callback_portrj    s    , C-/N! !!ggn5T9T9TN.w777>"':1N !CGGOQ/0I R-g6R:P:RD CKKr   c                 h    | j                  d      }|r|S | j                  d      xs d}d| d| dS )u  Resolve the OAuth callback URL: configured ``redirect_uri`` or loopback.

    A configured ``redirect_uri`` lets the callback go through a proxy (e.g. a
    Tailscale Funnel exposing a public HTTPS URL that forwards to localhost);
    otherwise we default to ``http://<redirect_host>:<port>/callback``. An empty
    value is treated as unset. Both the client metadata and any pre-registered
    client info must derive the redirect_uri here so they stay identical — a
    mismatch makes the authorization server reject the callback.

    ``redirect_host`` (default ``127.0.0.1``) tweaks only the hostname of the
    loopback callback. Some providers' WAFs (e.g. Reclaim.ai's AWS API Gateway)
    reject any authorize request whose query string contains a literal
    ``127.0.0.1``, returning ``{"message":"Forbidden"}``; ``redirect_host:
    localhost`` works around that. The callback listener still binds
    ``127.0.0.1`` either way.
    r!  redirect_hostr>   zhttp://:rY   )r/   )rd  rQ   
configuredhosts       r   _resolve_redirect_urirp    sC    " (J77?#2{DTF!D6++r   r   c                 *   | j                  d      }|t        d      | j                  dd      }| j                  d      }t        | |      }|t        |      gddgdgd	d
}|r||d<   | j                  d      rd|d<   t	        j
                  |      S )zBuild OAuthClientMetadata from the oauth config dict.

    Requires ``cfg['_resolved_port']`` to have been populated by
    :func:`_configure_callback_port` first.
    rf  zI_configure_callback_port() must be called before _build_client_metadata()client_namezHermes Agentscopeauthorization_coderefresh_tokenr  none)rr  rV   grant_typesresponse_typestoken_endpoint_auth_methodclient_secretclient_secret_postry  )r/   r^   rp  r   r   r   )rd  rQ   rr  rs  r!  metadata_kwargss         r   _build_client_metadatar}  /  s     77#$D|W
 	
 ''-8KGGGE(d3L # ./,o>!(&,'O #( 
ww8L45--o>>r   client_metadatac                    |j                  d      }|sy|d   }t        ||      }||g|j                  |j                  |j                  d}|j                  d      r|d   |d<   |j                  d      r|d   |d<   |j                  d      r|d   |d<   t        j                  |      }t        | j                         |j                  dd	
             t        j                  d|| j                         y)z=If cfg has a pre-registered client_id, persist it to storage.	client_idNrf  )r  rV   rw  rx  ry  rz  rr  rs  r   Tr   z$Pre-registered client_id=%s for '%s')r/   rp  rw  rx  ry  r   r   r   r[   r   r   r   r   )rT   rd  r~  r  rQ   r!  	info_dictr   s           r   _maybe_preregister_clientr  M  s     $I D(d3L &&22)88&5&P&P!I ww%(%9	/"
ww}#&}#5	- 
www \	',;;IFK))+[-C-C^b-C-cd
LL7GDXDXYr   
server_urloauth_configzOAuthClientProvider | Nonec                    t         st        j                  d|        yt        |xs i       }t	        |       }t               s"|j                         st        d|  d|  d      t        ||       t        |      }t        |||       |j                  dt              }t        ||j                  d      xs d      }t        |      }t        |||||t!        |j                  d	d
                  S )aQ  Build an ``httpx.Auth``-compatible OAuth handler for an MCP server.

    Public API preserved for backwards compatibility. New code should use
    :func:`tools.mcp_oauth_manager.get_manager` so OAuth state is shared
    across config-time, runtime, and reconnect paths.

    Args:
        server_name: Server key in mcp_servers config (used for storage).
        server_url: MCP server endpoint URL.
        oauth_config: Optional dict from the ``oauth:`` block in config.yaml.

    Returns:
        An ``OAuthClientProvider`` instance, or None if the MCP SDK lacks
        OAuth support.
    zjMCP OAuth requested for '%s' but SDK auth types are not available. Install with: pip install 'mcp>=1.26.0'NzMCP OAuth for 'z': non-interactive environment and no cached tokens found. The OAuth flow requires browser authorization. Run `hermes mcp login z[` interactively first to complete initial authorization, then cached tokens will be reused.rf  r!  )r!  rT  i,  )r  r~  rT   redirect_handlercallback_handlerrT  )_OAUTH_AVAILABLEr   r   r  r   rp   r  r   rj  r}  r  r/   r   r6  r9  r   float)	r   r  r  rd  rT   r~  resolved_portr  r  s	            r   build_oauth_authr  l  s   ( 6	

 
|!r
"C -GW%>%>%@&} !!, .HH
 	
 S'*,S1OgsO< GG,k:M-CGGN$;$Ct -];'))cggi-. r   r   )Vr   rK  contextvarsr   loggingr-   r8   r   r@   r   rm   rF  r   r/  
contextlibr   http.serverr   r   pathlibr   typingr   urllib.parser   r	   r+   r
   	getLoggerr   r   r  mcp.client.authr   mcp.shared.authr   r   r   r   r,   r   pydanticr   r8  r   r   rb   __annotations__
ContextVarr   r   	frozensetr^  rN  r0   r3   r;   rE   rF   rL   rS   rg   rk   r  rp   rs   ry   r|   r   r  rZ   r   r   rX  typer   r6  r:  r9  rJ  rc  rj  rp  r}  r  r  r   r   r   <module>r     s  !F     	 	    
    % :   + .			8	$  M3  S| S S4Z  >T[=S=S $> :  =SK<R<R= 9  HI 3 d
T 1 T G G G
" " 13 - 2  6 #>  3:  F"= #* $	 	
C 
D 
 / / 0 04 &T dTk "d "$ "4 "TR, R,t$dDj 1 $X^ ^C$J ^B6%S4Z"8 6*} }@Q[4 Q[D Q[x &*>> td"> 
	>, ,0.	.(. 	.b,t ,3 ,3 ,0? ?)> ?<Z!Z	Z +Z 
	ZD !%::: +: "	:E!  M
LLKLM
  Fs$   5G* 
H *HHHH