
    6j/                     b   d 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eddfdZd'd	efd
ZdedefdZdededefdZd ZdefdZd(d	ededdfdZd)d	ededdfdZdZd Z e            Zdddededz  defdZdddedz  fdZdedefd Zdefd!Zd"Zdefd#Z dedz  fd$Z!d%edefd&Z"dS )*z)
Hermes Web UI -- HTTP helper functions.
    N)Path)
IMAGE_EXTSMD_EXTSbodyreturnc                 r      fd|D             }|r%t          dd                    |                     dS )zHPhase D: Validate required fields. Raises ValueError with clean message.c                 p    g | ]2}                     |                               |          d k    0|3S )r   )get).0fr   s     !/root/hermes-webui/api/helpers.py
<listcomp>zrequire.<locals>.<listcomp>   s<    IIIQIq8H8Hq8H8H8H    zMissing required field(s): z, N)
ValueErrorjoin)r   fieldsmissings   `  r   requirer      sT    IIII&IIIG MKtyy7I7IKKLLLM Mr     statusc                 *    t          | d|i|          S )z#Return a clean JSON error response.error)r   )j)handlermsgr   s      r   badr      s    WwnV4444r   ec                 Z    ddl }t          |           }|                    dd|          }|S )zJStrip filesystem paths from exception messages before returning to client.r   Nz,(?:(?:/[a-zA-Z0-9_.-]+)+|(?:[A-Z]:\\[^\s]+))z<path>)restrsub)r   r   r   s      r   _sanitize_errorr"      s1    III
a&&C
&&@(C
P
PCJr   root	requestedc                     | |z                                   }|                    |                                             |S )zEResolve a relative path inside root, raising ValueError on traversal.)resolverelative_to)r#   r$   resolveds      r   safe_resolver)       s9    y ))++H(((Or   c                     |                      dd           |                      dd           |                      dd           |                      dd           |                      d	d
           dS )z'Add security headers to every response.zX-Content-Type-OptionsnosniffzX-Frame-OptionsDENYzReferrer-Policyzsame-originzContent-Security-Policya  default-src 'self' https://*.cloudflareaccess.com; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com; img-src 'self' data: https: blob:; font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com; connect-src 'self' https://cdn.jsdelivr.net; manifest-src 'self' https://*.cloudflareaccess.com; base-uri 'self'; form-action 'self'zPermissions-PolicyzDcamera=(), microphone=(self), geolocation=(), clipboard-write=(self)N)send_header)r   s    r   _security_headersr.   '   s    0)<<<)6222)=999!	.   N    r   c                 `    t          | dd          }|sdS |                    dd          }d|v S )z*Check if the client accepts gzip encoding.headersNFzAccept-Encoding gzip)getattrr
   )r   r0   aes      r   _accepts_gzipr5   ;   s>    gy$//G u	&	+	+BR<r      extra_headersc                    t          j        |dd                              d          }|                     |           |                     dd           t          |           rDt          |          dk    r1dd	l}|                    |d
          }|                     dd           |                     dt          t          |                               |                     dd           t          |            |r0|                                D ]\  }}|                     ||           |                                  | j                            |           d	S )zSend a JSON response.

    *extra_headers*: optional dict of additional headers to include
    (e.g., {'Set-Cookie': '...'}).  Headers are sent before end_headers().
    F   )ensure_asciiindentutf-8Content-Typezapplication/json; charset=utf-8i   r   N   )compresslevelzContent-Encodingr2   Content-LengthCache-Controlno-store)_jsondumpsencodesend_responser-   r5   lenr2   compressr    r.   itemsend_headerswfilewrite)r   payloadr   r7   r   r2   kvs           r   r   r   D   s_    ;wU1===DDWMMD&!!!(IJJJ
 W 8#d))d"2"2}}T}33.777(#c$ii..999444g &!'')) 	& 	&DAq1%%%%Mr   text/plain; charset=utf-8content_typec                    t          |t                    r|n!t          |                              d          }|                     |           |                     d|           |                     dt          t          |                               |                     dd           t          |            |                                  | j	        
                    |           dS )z#Send a plain text or HTML response.r<   r=   r@   rA   rB   N)
isinstancebytesr    rE   rF   r-   rG   r.   rJ   rK   rL   )r   rM   r   rQ   r   s        r   trU   `   s     %00R77c'll6I6I'6R6RD&!!!555(#c$ii..999444gMr   i  @c                  |   t          j        d          t          j        dt           j                  t          j        d          t          j        d          dt          dt          fddt          dt          ffd		 d
dlm n# t          $ r cY S w xY wdt          dt          ffd} | S )zFReturn a redactor backed by hermes-agent plus local fallback patterns.a  (?<![A-Za-z0-9_-])(sk-[A-Za-z0-9_-]{10,}|ghp_[A-Za-z0-9]{10,}|github_pat_[A-Za-z0-9_]{10,}|gho_[A-Za-z0-9]{10,}|ghu_[A-Za-z0-9]{10,}|ghs_[A-Za-z0-9]{10,}|ghr_[A-Za-z0-9]{10,}|AKIA[A-Z0-9]{16}|xox[baprs]-[A-Za-z0-9-]{10,}|hf_[A-Za-z0-9]{10,}|SG\.[A-Za-z0-9_-]{10,})(?![A-Za-z0-9_-])z!(Authorization:\s*Bearer\s+)(\S+)zo([A-Z0-9_]{0,50}(?:API_?KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)[A-Z0-9_]{0,50})\s*=\s*(['\"]?)(\S+)\2zH-----BEGIN[A-Z ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z ]*PRIVATE KEY-----tokenr   c                 Z    t          |           dk    r| d d          d| dd           ndS )N      z...z***)rG   )rW   s    r   _maskz_build_redact_fn.<locals>._mask   s;    03E

b0@0@%),,bcc
,,,eKr   textc                     t          | t                    r| s| S                     fd|           }                     fd|           }                     fd|           }                     d|           } | S )Nc                 @     |                      d                    S )N   groupmr\   s    r   <lambda>z<_build_redact_fn.<locals>._fallback_redact.<locals>.<lambda>   s    eeAGGAJJ&7&7 r   c                 l    |                      d           |                      d                    z   S )Nr`   r9   ra   rc   s    r   re   z<_build_redact_fn.<locals>._fallback_redact.<locals>.<lambda>   s*    !''!**uuQWWQZZ7H7H*H r   c                     |                      d           d|                      d            |                      d                     |                      d           S )Nr`   =r9      ra   rc   s    r   re   z<_build_redact_fn.<locals>._fallback_redact.<locals>.<lambda>   sQ    QQaggajjQ%%

2C2CQQWWQZZQQ r   z[REDACTED PRIVATE KEY])rS   r    r!   )r]   _AUTH_HDR_RE_CRED_RE_ENV_RE_PRIVKEY_REr\   s    r   _fallback_redactz*_build_redact_fn.<locals>._fallback_redact   s    $$$ 	D 	K||7777>> H H H H$OO{{QQQQSW
 
 7>>r   r   )redact_sensitive_textc                     t          | t                    r| s| S 	  | d          }n# t          $ r  |           }Y nw xY w |          S )NT)force)rS   r    	TypeError)r]   agent_redactedrn   ro   s     r   _combined_redactz*_build_redact_fn.<locals>._combined_redact   s    $$$ 	D 	K	9224tDDDNN 	9 	9 	922488NNN	9  ///s   * AA)_recompile
IGNORECASEr    agent.redactro   ImportError)rt   rj   rk   rl   rm   rn   r\   ro   s    @@@@@@@r   _build_redact_fnrz   q   sE   
 {	 H ;CS^TTLk	" G +S KLS LS L L L L	s 	s 	 	 	 	 	 	 	 	 	 	 6666666       0s 0s 0 0 0 0 0 0 0 s   B B$#B$_enabledr]   r|   c                    t          | t                    r| s| S |1ddlm} t	           |                                dd                    }|s| S t          |           S )u  Redact sensitive text from API responses. Respects api_redact_enabled setting.

    The ``_enabled`` parameter is an internal optimization for callers that
    redact many strings in a single response — `redact_session_data()` reads
    the setting once and threads it through ``_redact_value`` so we avoid
    re-loading settings.json from disk per string. (Opus pre-release perf fix.)
    Nr   load_settingsapi_redact_enabledT)rS   r    
api.configr   boolr
   _redact_fn_cached)r]   r|   r   s      r   _redact_textr      sy     dC    ,,,,,,++,@$GGHH T"""r   c                   t          | t                    rt          |           S t          | t                    r fd|                                 D             S t          | t
                    rfd| D             S | S )zRecursively redact credentials from strings, dicts, and lists.

    ``_enabled`` is threaded through so a single response-level redact pass
    only reads settings.json once. (Opus pre-release perf fix.)
    r{   c                 :    i | ]\  }}|t          |           S r{   _redact_value)r   rN   valr|   s      r   
<dictcomp>z!_redact_value.<locals>.<dictcomp>   s,    QQQVQ=x888QQQr   c                 2    g | ]}t          |           S r   r   )r   itemr|   s     r   r   z!_redact_value.<locals>.<listcomp>   s&    EEE4dX666EEEr   )rS   r    r   dictrI   list)rO   r|   s    `r   r   r      s     !S 2A1111!T RQQQQqwwyyQQQQ!T FEEEE1EEEEHr   session_dictc                    ddl m} t           |                                dd                    }t	          |           }t          |                    d          t                    rt          |d         |          |d<   d|v rt          |d         |          |d<   d|v rt          |d         |          |d<   |S )	a4  Redact credentials from message content and tool_call data before API response.

    Applies to: messages[], tool_calls[], and title.
    The underlying session file is not modified; redaction is response-layer only.

    Reads the ``api_redact_enabled`` setting ONCE for the entire response and
    threads it through to avoid hundreds of settings.json reads per session
    payload (a 50-message session has hundreds of nested strings). When the
    setting is disabled this is also a fast path: the recursion still walks
    but every string returns early.
    r   r~   r   Ttitler{   messages
tool_calls)	r   r   r   r
   r   rS   r    r   r   )r   r   r|   results       r   redact_session_datar      s     )(((((MMOO''(<dCCDDH,F&**W%%s++ K&vgJJJwV*6*+=QQQzv,VL-AHUUU|Mr   c                 (   t          | j                            dd                    }|t          k    rt	          d| dt           d          |r| j                            |          nd}	 t          j        |          S # t          $ r i cY S w xY w)z9Read and JSON-parse a POST request body (capped at 20MB).r@   r   zRequest body too large (z bytes, max )s   {})
intr0   r
   MAX_BODY_BYTESr   rfilereadrC   loads	Exception)r   lengthraws      r   	read_bodyr      s    $$%5q99::FYFYYYYYZZZ(.
9'-

V
$
$
$EC{3   			s   .B BBhermes_profilec                  6    t          j        dt                    S )z@Return the cookie name used to persist the active WebUI profile.WEBUI_PROFILE_COOKIE_NAME)osgetenvPROFILE_COOKIE_NAME r   r   get_profile_cookie_namer      s    902EFFFr   c                 t   | j                             dd          }|sdS ddlm} |                                }	 |                    |           n# |j        $ r Y dS w xY wt                      }|                    |          }|r1|j        r*ddl	m
} |j        }|dk    s|                    |          r|S dS )zBExtract the active-profile cookie value from the request, or None.Cookier1   Nr   )_PROFILE_ID_REdefault)r0   r
   http.cookiescookiesSimpleCookieloadCookieErrorr   valueapi.profilesr   	fullmatch)r   cookie_header_hccookiecookie_namemorselr   r   s           r   get_profile_cookier     s    O''"55M tFM""""?   tt)++KZZ$$F &, //////l)~77<<J4s   A 
AAnamec                     ddl m} |                                }t                      }| ||<   d||         d<   d||         d<   d||         d<   ||                                         S )	a  Build a Set-Cookie header value for the active-profile cookie.

    Always persist the selected profile in the cookie, including 'default'.
    Clearing the cookie causes the backend to fall back to process-global
    _active_profile, which can unexpectedly switch clients back to another
    profile.

    Set HttpOnly because the UI reads the active profile from
    /api/profile/active JSON and does not need to access this cookie via
    document.cookie.
    r   N/pathThttponlyLaxsamesite)r   r   r   r   OutputString)r   r   r   r   s       r   build_profile_cookier     s     F)++KF;"%F;&*F;
#&+F;
#+++---r   )r   )r6   N)r6   rP   )#__doc__jsonrC   r   r   ru   pathlibr   r   r   r   r   r   r   r   r   r    r"   r)   r.   r   r5   r   rU   r   rz   r   r   r   r   r   r   r   r   r   r   r   r   <module>r      s        				           * * * * * * * *M$ MD M M M M5 5c 5 5 5 5
y S    t        (d       d    8	 	 	s 	[_ 	 	 	 	 "
? ? ?D %$&&  8< # # #s # # # # # #$ 15   $+    d t    0	$ 	 	 	 	 ' G G G G G
3:    ,.s .s . . . . . .r   