
    PL
jc              	          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mZmZ ddlmZ ddlmZ ddlmZmZ  ej        e          ZdZ	 ddlmZ dd	lmZmZm Z m!Z! d
Zn # e"$ r e#                    d           Y nw xY w	 ddl$m%Z% n# e"$ r dZ%Y nw xY w G d de&          Z'da(e)dz  e*d<   defdZ+de,de,fdZ-de)fdZ.de/fdZ0de/fdZ1dede2dz  fdZ3dede2ddfdZ4 G d d          Z5de6e7e2f         fdZ8de,ddfdZ9de6e,e,dz  f         fd Z:d!e,ddfd"Z;d#e2de)fd$Z<d#e2dd%fd&Z=d'dd#e2d(d%ddfd)Z>	 d.d!e,d*e,d+e2dz  dd,fd-Z?dS )/aa  
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
          client_name: "My Custom Client"       # default: "Hermes Agent"
    N)BaseHTTPRequestHandler
HTTPServer)Path)Any)parse_qsurlparseF)OAuthClientProvider)OAuthClientInformationFullOAuthClientMetadataOAuthMetadata
OAuthTokenTz8MCP OAuth types not available -- OAuth MCP auth disabled)AnyUrlc                       e Zd ZdZdS )OAuthNonInteractiveErrorzHRaised when OAuth requires browser interaction in a non-interactive env.N)__name__
__module____qualname____doc__     3/home/kuhnn/.hermes/hermes-agent/tools/mcp_oauth.pyr   r   S   s        RRRRr   r   _oauth_portreturnc            
         	 ddl m}  t           |                       }n^# t          $ rQ t          t          j                            dt          t          j                    dz                                }Y nw xY w|dz  S )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   bases     r   _get_token_dirr%   e   s    Q444444OO%%&& Q Q QBJNN=#dikkI6M2N2NOOPPQ,s     AA;:A;namec                 h    t          j        dd|                               d          dd         pdS )zBSanitize a server name for use as a filename (no path separators).z[^\w\-]_N   default)resubstrip)r&   s    r   _safe_filenamer.   s   s2    6*c4((..s33DSD9FYFr   c                      t          j         t           j        t           j                  5 } |                     d           |                                 d         cddd           S # 1 swxY w Y   dS )z(Find an available TCP port on localhost.)	127.0.0.1r      N)socketAF_INETSOCK_STREAMbindgetsockname)ss    r   _find_free_portr8   x   s    	v~v'9	:	: "a	   }}q!" " " " " " " " " " " " " " " " " "s   /A&&A*-A*c                  p    	 t           j                                        S # t          t          f$ r Y dS w xY w)z@Return True if we can reasonably expect to interact with a user.F)sysstdinisattyAttributeError
ValueErrorr   r   r   _is_interactiver?      sB    y!!!J'   uus     55c                     t           j                            d          st           j                            d          rdS t           j        dk    rdS 	 t          j                    j        dk    rdS n# t          $ r Y nw xY wt           j                            d          st           j                            d          rdS dS )	z3Return True if opening a browser is likely to work.
SSH_CLIENTSSH_TTYFntTDarwinDISPLAYWAYLAND_DISPLAY)r   r    r!   r&   unamesysnamer=   r   r   r   _can_open_browserrI      s     
z~~l## rz~~i'@'@ u	w$t8::))4 *    
z~~i   BJNN3D$E$E t5s   A3 3
B ?B pathc                     |                                  sdS 	 t          j        |                     d                    S # t          j        t
          f$ r'}t                              d| |           Y d}~dS 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JSONDecodeErrorOSErrorloggerwarning)rJ   excs     r   
_read_jsonrX      s    ;;== tz$..'.::;;; '*   .c:::ttttts   'A   A=A88A=datac                 t   | j                             dd           	 t          j        | j         d           n# t          $ r Y nw xY w|                     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                     |                                 t          j        |                                           ddd           n# 1 swxY w Y   t          j        ||            dS # t          $ r* 	 |                    d           n# t          $ r Y n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_oki  z.tmp..   wrL   rM      )indentr*   N
missing_ok)parentmkdirr   chmodrT   with_suffixgetpidsecrets	token_hexopenr"   O_WRONLYO_CREATO_EXCLstatS_IRUSRS_IWUSRfdopenrP   dumpflushfsyncfilenoreplaceunlink)rJ   rY   tmpfdfhs        r   _write_jsonr|      s    	KdT222
e$$$$    

G29;;GG1B11E1EGG
H
HCWHHK"*$ry0L4<'
 

 Yr3111 	"RIdBq#6666HHJJJHRYY[[!!!	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	
3   	JJ$J'''' 	 	 	D	se   9 
AAA5F =AE!F !E%%F (E%)F 
F7F%$F7%
F2/F71F22F7c                       e Zd ZdZde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fdZdS )!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
    server_namec                 .    t          |          | _        d S N)r.   _server_name)selfr   s     r   __init__zHermesTokenStorage.__init__   s    *;77r   r   c                 4    t                      | j         dz  S )Nz.jsonr%   r   r   s    r   _tokens_pathzHermesTokenStorage._tokens_path   s    T%6"="="===r   c                 4    t                      | j         dz  S )Nz.client.jsonr   r   s    r   _client_info_pathz$HermesTokenStorage._client_info_path   s    T%6"D"D"DDDr   c                 4    t                      | j         dz  S )Nz
.meta.jsonr   r   s    r   
_meta_pathzHermesTokenStorage._meta_path   s    T%6"B"B"BBBr   OAuthToken | Nonec                 0  K   t          |                                           }|d S |                    dd           }|5t          t	          |t          j                    z
  d                    |d<   n|                    d          	 |                                                                 j        }n# t          $ r d }Y nw xY w|e	 |t          |d                   z   }t          t	          |t          j                    z
  d                    |d<   n# t          t          f$ r Y nw xY w	 t          j        |          S # t          t          t          f$ r9}t                              d|                                 |           Y d }~d S d }~ww xY w)N
expires_atr   
expires_inz$Corrupt tokens at %s -- ignoring: %s)rX   r   popintmaxtimer!   ro   st_mtimerT   	TypeErrorr>   r   model_validateKeyErrorrU   rV   )r   rY   absolute_expiry
file_mtimeimplied_expiryrW   s         r   
get_tokenszHermesTokenStorage.get_tokens   s     $++--..<4  ((<66&!$S49;;)F%J%J!K!KDXXl##/"!..005577@

 " " "!


"%%/#d<6H2I2I%IN),S$)++1Mq-Q-Q)R)RD&&!:.   D	,T222Ix0 	 	 	NNA4CTCTCVCVX[\\\44444	s=   +B7 7CCAD D-,D-1E F.FFtokensr   Nc                 h  K   |                     dd          }|                    d          }|?	 t          j                    t          |          z   |d<   n# t          t
          f$ r Y nw xY wt          |                                 |           t          	                    d| j
                   d S )NrP   Tmodeexclude_noner   r   zOAuth tokens saved for %s)
model_dumpr!   r   r   r   r>   r|   r   rU   debugr   )r   r   payloadr   s       r   
set_tokenszHermesTokenStorage.set_tokens  s      ##d#CC [[..
!(,	c*oo(E%%z*     	D%%''1110$2CDDDDDs   &A A-,A-!OAuthClientInformationFull | Nonec                    K   t          |                                           }|d S 	 t          j        |          S # t          t
          t          f$ r9}t                              d|                                 |           Y d }~d S d }~ww xY w)Nz)Corrupt client info at %s -- ignoring: %s)	rX   r   r
   r   r>   r   r   rU   rV   r   rY   rW   s      r   get_client_infoz"HermesTokenStorage.get_client_info&  s      $002233<4	-<TBBBIx0 	 	 	NNFH^H^H`H`befff44444	s   = B.BBclient_infor
   c                    K   t          |                                 |                    dd                     t                              d| j                   d S )NrP   Tr   zOAuth client info saved for %s)r|   r   r   rU   r   r   )r   r   s     r   set_client_infoz"HermesTokenStorage.set_client_info0  sT      D**,,k.D.D&_c.D.d.deee5t7HIIIIIr   metadatar   c                     t          |                                 |                    dd                     t                              d| j                   d S )NTrP   )r   r   zOAuth metadata saved for %s)r|   r   r   rU   r   r   )r   r   s     r   save_oauth_metadataz&HermesTokenStorage.save_oauth_metadata<  sN    DOO%%x':':SY':'Z'Z[[[2D4EFFFFFr   OAuthMetadata | Nonec                    t          |                                           }|d S 	 t          j        |          S # t          t
          t          f$ r9}t                              d|                                 |           Y d }~d S d }~ww xY w)Nz,Corrupt OAuth metadata at %s -- ignoring: %s)	rX   r   r   r   r>   r   r   rU   rV   r   s      r   load_oauth_metadataz&HermesTokenStorage.load_oauth_metadata@  s    $//++,,<4	 /555Ix0 	 	 	NNI4??K\K\^abbb44444	s   ; B.BBc                     |                                  |                                 |                                 fD ]}|                    d           dS )z.Delete all stored OAuth state for this server.Trb   N)r   r   r   rx   )r   ps     r   removezHermesTokenStorage.removeL  s[    ##%%t'='='?'?ARARS 	& 	&AHHH%%%%	& 	&r   c                 N    |                                                                  S )z7Return True if we have tokens on disk (may be expired).)r   rO   r   s    r   has_cached_tokensz$HermesTokenStorage.has_cached_tokensQ  s       ""))+++r   )r   r   )r   r   r   N)r   r   )r   r
   r   N)r   r   r   N)r   r   r   N)r   r   r   r   r"   r   r   r   r   r   r   r   r   r   r   r   r   boolr   r   r   r   r~   r~      sH        8C 8 8 8 8>d > > > >E4 E E E ECD C C C C
% % % %NE E E E,   J J J JG G G G   & & & &
,4 , , , , , ,r   r~   c                  B    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                   0    e Zd Zd fdZdededdfdZdS )(_make_callback_handler.<locals>._Handlerr   Nc                    t          t          | j                  j                  }|                    dd g          d         }|                    dd g          d         }|                    dd g          d         }|d<   |d<   |d<   |rdnd|pd d	}|                     d
           |                     dd           |                                  | j        	                    |
                                           d S )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   rJ   queryr!   send_responsesend_headerend_headerswfilewriteencode)r   paramsr   r   r   bodyresults         r   do_GETz/_make_callback_handler.<locals>._Handler.do_GETf  s%   hty11788F::ftf--a0DJJw//2EJJw//2E"&F;#F7O#F7O
 T TD"/iD D D	  s###^-GHHHJT[[]]+++++r   fmtargsc                 B    t                               d||z             d S )NzOAuth callback: %s)rU   r   )r   r   r   s      r   log_messagez4_make_callback_handler.<locals>._Handler.log_message|  s!    LL-sTz:::::r   r   )r   r   r   r   r"   r   r   )r   s   r   _Handlerr   e  s\        	, 	, 	, 	, 	, 	,,	;3 	;s 	;t 	; 	; 	; 	; 	; 	;r   r   )r   )r   r   s    @r   _make_callback_handlerr   [  sV     ,0$NNF; ; ; ; ; ; ;) ; ; ;4 Vr   authorization_urlc           	      `  K   d|  d}t          |t          j                   t          r\t	          j        d          st	          j        d          r4t          dt           dt           dt           d	t          j                   t                      r~	 t          j        |           }|rt          d
t          j                   nt          dt          j                   dS dS # t          $ r t          dt          j                   Y dS w xY wt          dt          j                   dS )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.
    zL
  MCP OAuth: authorization required.
  Open this URL in your browser:

    
)filerA   rB   za  Remote session detected. The OAuth provider will redirect your browser to
    http://127.0.0.1:z/callback
  which the callback listener on THIS machine is waiting on. If your browser
  is on a different machine, forward the port first in a separate terminal:

    ssh -N -L z:127.0.0.1:zv <user>@<this-host>

  Then open the URL above. 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.)
N)
printr:   stderrr   r   getenvrI   
webbrowserrk   	Exception)r   msgopeneds      r   _redirect_handlerr     s     	% 	% 	% 	% 
 
#CJ  
	,// 
29Y3G3G 
r$/r r
 )r r
 6Ar r r 
	
 
	
 
	
 
	
  
a	e_%677F i;#*MMMMMV]`]ghhhhhh NM  	e 	e 	eRY\Ycddddddd	e 	NUXU_``````s   AC' '%DDc                    K   t           t          d          t                      \  } }	 t          dt           f|           }n# t          $ r t          d          w xY wt          j        |j        d          }|	                                 d}d}d	}	 ||k     r6|d
         |d         n%t          j        |           d{V  ||z  }||k     6|                                 n# |                                 w xY w|d         rt          d|d                    |d
         t          d          |d
         |d         fS )u]  Wait for the OAuth callback to arrive on the local callback server.

    Uses the module-level ``_oauth_port`` which is set by ``build_oauth_auth``
    before this is ever called.  Polls for the result without blocking the
    event loop.

    Raises:
        OAuthNonInteractiveError: If the callback times out (no user present
            to complete the browser auth).
        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_callbackr0   uu   OAuth callback timed out — could not bind callback port. Complete the authorization in a browser first, then retry.T)targetdaemong     r@g      ?g        r   r   zOAuth authorization failed: uq   OAuth callback timed out — no authorization code received. Ensure you completed the browser authorization flow.r   )r   RuntimeErrorr   r   rT   r   	threadingThreadhandle_requeststartasynciosleepserver_close)handler_clsr   serverserver_threadtimeoutpoll_intervalelapseds          r   _wait_for_callbackr     s      .
 
 	
 122K
[+6DD 
 
 
 'I
 
 	

 $F,A$OOOMGMGk".&/2M-.........}$G	  	g MK&/KKLLLk"&C
 
 	

 +w//s   A A<C' 'C=r   c                     t          |           }|                                 t                              d|            dS )z8Delete stored OAuth tokens and client info for a server.zOAuth tokens removed for '%s'N)r~   r   rU   info)r   storages     r   remove_oauth_tokensr     s:     --GNN
KK/=====r   cfgc                     t          |                     dd                    }|dk    rt                      n|}|| 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.

    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.
    redirect_portr   _resolved_port)r   r!   r8   r   )r   	requestedports      r   _configure_callback_portr     sL     CGGOQ//00I )Q?ID CKKr   r   c                 L   |                      d          }|t          d          |                      dd          }|                      d          }d| d}|t          |          gd	d
gdgdd}|r||d<   |                      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.
    r   NzI_configure_callback_port() must be called before _build_client_metadata()client_namezHermes Agentscopehttp://127.0.0.1:	/callbackauthorization_coderefresh_tokenr   none)r   redirect_urisgrant_typesresponse_typestoken_endpoint_auth_methodclient_secretclient_secret_postr	  )r!   r>   r   r   r   )r   r   r   r   redirect_urimetadata_kwargss         r   _build_client_metadatar    s     77#$$D|W
 
 	
 ''-88KGGGE6t666L # ../,o>!(&,' 'O  )#( 
ww M8L45-o>>>r   r   client_metadatac                    |                     d          }|sdS |d         }d| d}||g|j        |j        |j        d}|                     d          r|d         |d<   |                     d          r|d         |d<   |                     d	          r|d	         |d	<   t	          j        |          }t          |                                 |                    d
d                     t          
                    d|| j                   dS )z=If cfg has a pre-registered client_id, persist it to storage.	client_idNr   r  r  )r  r  r  r  r	  r
  r   r   rP   Tr   z$Pre-registered client_id=%s for '%s')r!   r  r  r	  r
   r   r|   r   r   rU   r   r   )r   r   r  r  r   r  	info_dictr   s           r   _maybe_preregister_clientr  7  s/    $$I  D6t666L &&2)8&5&P! !I ww :%(%9	/"
ww} 6#&}#5	- 
www * \	',;IFFK))++[-C-C^b-C-c-cddd
LL7GDXYYYYYr   
server_urloauth_configzOAuthClientProvider | Nonec                    t           st                              d|            dS t          |pi           }t	          |           }t                      s/|                                st                              d|            t          |           t          |          }t          |||           t          |||t          t          t          |                    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'NzMCP OAuth for '%s': non-interactive environment and no cached tokens found. The OAuth flow requires browser authorization. Run interactively first to complete the initial authorization, then cached tokens will be reused.r   i,  )r  r  r   redirect_handlercallback_handlerr   )_OAUTH_AVAILABLErU   rV   dictr~   r?   r   r   r  r  r	   r   r   floatr!   )r   r  r  r   r   r  s         r   build_oauth_authr  V  s    (  6	
 	
 	

 t
|!r
"
"C --G 
W%>%>%@%@ 
, 	
 	
 	
 S!!!,S11OgsO<<<'*+cggi--..   r   r   )@r   r   rP   loggingr   r+   ri   r2   ro   r:   r   r   r   http.serverr   r   pathlibr   typingr   urllib.parser   r   	getLoggerr   rU   r  mcp.client.authr	   mcp.shared.authr
   r   r   r   r   r   pydanticr   r   r   r   r   __annotations__r%   r"   r.   r8   r   r?   rI   r  rX   r|   r~   tupletyper   r   r   r   r   r  r  r  r   r   r   <module>r)     s    B    				 				    



          : : : : : : : :             + + + + + + + +		8	$	$  M333333             M M M
LLKLLLLLM   FFFS S S S S| S S S S4Z       G G G G G G
" " " " "    4    &T dTk    $d $$ $4 $ $ $ $XB, B, B, B, B, B, B, B,T$dDj 1 $ $ $ $X(as (at (a (a (a (aV:0%S4Z"8 :0 :0 :0 :0D>S >T > > > > $ 3    *? ?)> ? ? ? ?<Z!Z	Z +Z 
	Z Z Z ZD !%3 333 +3 "	3 3 3 3 3 3s$   #A8 8BBB   B*)B*