
    `gj*N                        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m	Z	 ddl
mZmZ ddlmZ ddlZ ej                   e      Z eh d      Zd<ded	ed
efdZd=ded	ed
efdZde	d
dfdZde	d
dfdZde	ddd
dfdZde	ddd
dfdZdeee	f   deee	f   d
efdZddddeee	f   dedededz  ded
dfdZ G d  d!ej@                        Z!dddd"deee	f   ded#ed$ed%edz  d
dfd&Z"deee	f   d'eded
dfd(Z#d>d)ed	ed
efd*Z$da%d+ Z&d,ed
efd-Z'd?d.ed	ed
efd/Z(d@d.ed	e)d
e)fd0Z*d<d.ed	ed
efd1Z+d2Z,d3edz  d
edz  fd4Z-dAd5Z.d6ed
efd7Z/d8ed
efd9Z0d6ed:ed
efd;Z1y)Bz*Shared utility functions for hermes-agent.    N)Path)AnyUnion)urlparse>   1onyestrueFvaluedefaultreturnc                     | |S t        | t              r| S t        | t              r$| j                         j	                         t
        v S t        |       S )zDCoerce bool-ish values using the project's shared truthy string set.)
isinstanceboolstrstriplowerTRUTHY_STRINGS)r   r   s     8/root/.hermes/venv/lib/python3.12/site-packages/utils.pyis_truthy_valuer      sI    }%%{{}""$66;    namec                 D    t        t        j                  | |      d      S )zBReturn True when an environment variable is set to a truthy value.Fr   r   osgetenv)r   r   s     r   env_var_enabledr   !   s    299T73UCCr   pathz
int | Nonec                     	 | j                         r-t        j                  | j                         j                        S dS # t        $ r Y yw xY w)zBCapture the permission bits of *path* if it exists, else ``None``.N)existsstatS_IMODEst_modeOSError)r   s    r   _preserve_file_moder&   &   sA    48KKMt||DIIK//0KtK s   <A A 	AAztuple[int, int] | Nonec                     t         j                  dk7  ry	 | j                         }|j                  |j
                  fS # t        $ r Y yw xY w)zACapture the owning uid/gid of *path* if the platform supports it.posixN)r   r   r"   r%   st_uidst_gid)r   sts     r   _preserve_file_ownerr,   .   sJ    	ww'YY[ 99bii  s   > 	A
	A
ownerc                     |t        t        d      sy	 t        j                  | |d   |d          y# t        $ r Y yw xY w)a  Re-apply uid/gid after an atomic replace when permitted.

    Docker and NAS-backed installs often run some commands as root while the
    persistent volume is owned by the runtime user. ``os.replace`` swaps in the
    temp file's owner, so a root-run config write can leave ``config.yaml`` owned
    by root. Best-effort chown preserves the existing owner for privileged
    callers and is harmless for unprivileged callers that cannot chown.
    Nchownr      )hasattrr   r/   r%   )r   r-   s     r   _restore_file_ownerr2   9   sD     }GB0
uQxq* s   3 	??modec                 V    |y	 t        j                  | |       y# t        $ r Y yw xY w)a  Re-apply *mode* to *path* after an atomic replace.

    ``tempfile.mkstemp`` creates files with 0o600 (owner-only).  After
    ``os.replace`` swaps the temp file into place the target inherits
    those restrictive permissions, breaking Docker / NAS volume mounts
    that rely on broader permissions set by the user.  Calling this
    right after ``os.replace`` restores the original permissions.
    N)r   chmodr%   )r   r3   s     r   _restore_file_moder6   J   s1     |
t s    	((tmp_pathtargetc           
      N   t        |      }t        j                  j                  |      rt        j                  j	                  |      n|}t        |       }	 t        j
                  ||       |S # t        $ r*}|j                  t        j                  t        j                  fvr t        j                  d||t        j                  j                  |j                  |j                               t        j                  ||       	 t        j                   ||       n# t        $ r Y nw xY w	 t#        |d      5 }t        j$                  |j'                                ddd       n# 1 sw Y   nxY wn# t        $ r Y nw xY wt        j(                  |       Y d}~|S d}~ww xY w)u  Atomically move *tmp_path* onto *target*, preserving symlinks.

    ``os.replace(tmp, target)`` atomically swaps ``tmp`` into place at
    ``target``.  When ``target`` is a symlink, the symlink itself is
    replaced with a regular file — silently detaching managed deployments
    that symlink ``config.yaml`` / ``SOUL.md`` / ``auth.json`` etc. from
    ``~/.hermes/`` to a git-tracked profile package or dotfiles repo
    (GitHub #16743).

    This helper resolves the symlink first so ``os.replace`` writes to
    the real file in-place while the symlink survives.  For non-symlink
    and non-existent paths the behavior is identical to a plain
    ``os.replace`` call unless the rename fails with ``EXDEV`` or ``EBUSY``;
    those cases fall back to copy/fsync/unlink for cross-device, bind-mount,
    and busy-file deployments.

    Returns the resolved real path used for the replace, so callers that
    need to re-apply permissions can target it instead of the symlink.
    z=atomic_replace: %s -> %s failed with %s; falling back to copyrbN)r   r   r   islinkrealpathreplacer%   errnoEXDEVEBUSYloggerdebug	errorcodegetshutilcopyfilecopystatopenfsyncfilenounlink)r7   r8   
target_str	real_pathtmp_strexcfs          r   atomic_replacerQ   [   sI   ( VJ02z0J  ,PZI(mG


7I&* )  99U[[%++66KOO		3995		
 	+	OOGY/ 			i& %!$% % % 		
		')sy   A0 0
F$:BFDF	D+(F*D++F/E5;$E(	E5(E1	-E54F5	F>F FFF$   )indentr3   datarS   dump_kwargsc                \   t        |       } | j                  j                  dd       |dn
t        |       }t	        |       }t        j                  t        | j                        d| j                   dd      \  }}	 |&t        t        d      rt        j                  ||       t        j                  |d	d
      5 }	t        j                  ||	f|dd| |	j                          t        j                   |	j#                                ddd       t%        ||       }
t        |
      }t'        ||       |	 t        j(                  ||       yt-        ||       y# 1 sw Y   SxY w# t*        $ r Y yw xY w# t.        $ r' 	 t        j0                  |        # t*        $ r Y  w xY ww xY w)a  Write JSON data to a file atomically.

    Uses temp file + fsync + os.replace to ensure the target file is never
    left in a partially-written state. If the process crashes mid-write,
    the previous version of the file remains intact.

    Args:
        path: Target file path (will be created or overwritten).
        data: JSON-serializable data to write.
        indent: JSON indentation (default 2).
        mode: Optional final permission mode. When set, the temp file is
            created and replaced with this mode, avoiding chmod-after-write
            TOCTOU exposure for secret-bearing files.
        **dump_kwargs: Additional keyword args forwarded to json.dump(), such
            as default=str for non-native types.
    Tparentsexist_okN._.tmpdirprefixsuffixfchmodwutf-8encodingF)rS   ensure_ascii)r   parentmkdirr&   r,   tempfilemkstempr   stemr1   r   ra   fdopenjsondumpflushrI   rJ   rQ   r2   r5   r%   r6   BaseExceptionrK   )r   rT   rS   r3   rU   original_modeoriginal_ownerfdr7   rP   rM   real_path_objs               r   atomic_json_writeru      s   0 :DKKdT2 ,D2Ed2KM)$/N##499+QLB
"H 5 IIb$YYr31 		!QII "	
  GGIHHQXXZ 		! #8T2	YM>:- }m<)		! 		!"    	IIh 	  		sn    A E;  AE -E; <E, E;  E)%E; ,	E85E; 7E88E; ;	F+FF+	F'$F+&F''F+c                   $     e Zd ZdZd fd	Z xZS )IndentDumperuD  PyYAML dumper that indents list items under mapping keys (2-space).

    Default PyYAML emits "indentless" sequences — list items start at the
    same column as their parent mapping key.  ``ruamel.yaml`` (used by
    :func:`atomic_roundtrip_yaml_update`) emits 2-space-indented sequences.
    Mixing both styles in the same ``config.yaml`` produces a file that
    stricter parsers like ``js-yaml`` reject with ``bad indentation of a
    mapping entry``.  Forcing ``indentless=False`` aligns the two
    serializers so all write paths emit byte-identical layouts (#31999).
    c                 $    t         |   |d      S )NF)superincrease_indent)selfflow
indentless	__class__s      r   rz   zIndentDumper.increase_indent   s    w&tU33r   )FF)__name__
__module____qualname____doc__rz   __classcell__)r~   s   @r   rw   rw      s    	4 4r   rw   )default_flow_style	sort_keysextra_contentr   r   r   c          	         t        |       } | j                  j                  dd       t        |       }t	        |       }t        j                  t        | j                        d| j                   dd      \  }}	 t        j                  |dd	      5 }	t        j                  ||	t        ||d
       |r|	j                  |       |	j                          t        j                   |	j#                                ddd       t%        ||       }
t        |
      }t'        ||       t)        ||       y# 1 sw Y   9xY w# t*        $ r' 	 t        j,                  |        # t.        $ r Y  w xY ww xY w)an  Write YAML data to a file atomically.

    Uses temp file + fsync + os.replace to ensure the target file is never
    left in a partially-written state.  If the process crashes mid-write,
    the previous version of the file remains intact.

    Args:
        path: Target file path (will be created or overwritten).
        data: YAML-serializable data to write.
        default_flow_style: YAML flow style (default False).
        sort_keys: Whether to sort dict keys (default False).
        extra_content: Optional string to append after the YAML dump
            (e.g. commented-out sections for user reference).
    TrW   rZ   r[   r\   r]   rb   rc   rd   )Dumperr   r   allow_unicodeN)r   rg   rh   r&   r,   ri   rj   r   rk   r   rl   yamlrn   rw   writero   rI   rJ   rQ   r2   r6   rp   rK   r%   )r   rT   r   r   r   rq   rr   rs   r7   rP   rM   rt   s               r   atomic_yaml_writer      sK   , :DKKdT2'-M)$/N##499+QLB
!YYr31 	!Q II##5#" &GGIHHQXXZ '	!* #8T2	YM>:=-81	! 	!2  	IIh 	  		sI   <D> A&D2:7D> 2D;7D> >	E.EE.	E*'E.)E**E.key_pathc                    ddl m} ddlm} t	        |       } | j
                  j                  dd        |d      }d|_        d|_        d|_	        |j                  d	d
d	       | j                         r7| j                  dd      5 }|j                  |      xs  |       }ddd       n |       }t        |      s ||      }|}|j                  d      }	|	dd D ]-  }
|j!                  |
      }t        ||      s |       }|||
<   |}/ |||	d   <   t#        |       }t%        |       }t'        j(                  t+        | j
                        d| j,                   dd      \  }}	 t/        j0                  |dd      5 }|j3                  ||       |j5                          t/        j6                  |j9                                ddd       t;        ||       }t	        |      }t=        ||       t?        ||       y# 1 sw Y   WxY w# 1 sw Y   FxY w# t@        $ r' 	 t/        jB                  |        # tD        $ r Y  w xY ww xY w)a_  Update one dotted YAML key while preserving comments and readable text.

    This is intentionally narrower than :func:`atomic_yaml_write`: it is for
    user-edited config files where comments, ordering, quoting, and Unicode
    should survive a single setting mutation.  Writes still use the same temp
    file + fsync + atomic replace pattern.
    r   )YAML)CommentedMapTrW   rt)typFrR      )mappingsequenceoffsetrrc   rd   NrZ   r[   r\   r]   rb   )#ruamel.yamlr   ruamel.yaml.commentsr   r   rg   rh   preserve_quotesr   r   rS   r!   rH   loadr   splitrD   r&   r,   ri   rj   r   rk   r   rl   rn   ro   rI   rJ   rQ   r2   r6   rp   rK   r%   )r   r   r   r   r   yaml_rtrP   configcurrentkeyskey
next_valuerq   rr   rs   r7   rM   rt   s                     r   atomic_roundtrip_yaml_updater   (  s-    !1:DKKdT2tnG"G G!&GNN1qN3{{}YYsWY- 	7\\!_6F	7 	7 fl+f%G>>#DCRy [[%
*l3%J%GCL GDH'-M)$/N##499+QLB
YYr31 	!QLL#GGIHHQXXZ 	! #8T2	YM>:=-8C	7 	74	! 	!  	IIh 	  		sU   
H,H AH
7H HHH 	I%H;:I;	IIIItextc                 z    	 t        j                  |       S # t         j                  t        t        f$ r |cY S w xY w)zParse JSON, returning *default* on any parse error.

    Replaces the ``try: json.loads(x) except (JSONDecodeError, TypeError)``
    pattern duplicated across display.py, anthropic_adapter.py,
    auxiliary_client.py, and others.
    )rm   loadsJSONDecodeError	TypeError
ValueError)r   r   s     r   safe_json_loadsr   n  s7    zz$  )Z8 s     ::c                  `    t         #t        t        dd       xs t        j                  a t         S )NCSafeLoader)_fast_yaml_loadergetattrr   
SafeLoader r   r   _get_fast_yaml_loaderr     s&     #D->Q$//r   streamc                 @    t        j                  | t                     S )uy  ``yaml.safe_load`` using the libyaml C loader when available.

    Accepts the same inputs as ``yaml.safe_load`` (a ``str``/``bytes`` document
    or a readable file object) and returns the same parsed structure. Falls
    back to PyYAML's pure-Python ``SafeLoader`` when ``CSafeLoader`` isn't
    available, so behavior is identical everywhere — only the speed differs.
    )Loader)r   r   r   )r   s    r   fast_safe_loadr     s     99V$9$;<<r   r   c                     t        j                  | d      j                         }|s|S 	 t        |      S # t        t
        f$ r |cY S w xY w)z:Read an environment variable as an integer, with fallback. )r   r   r   intr   r   r   r   raws      r   env_intr     sJ    
))C

"
"
$C3x	"    
5 A	A	c                     t        j                  | d      j                         }|s|S 	 t        |      S # t        t
        f$ r |cY S w xY w)z7Read an environment variable as a float, with fallback.r   )r   r   r   floatr   r   r   s      r   	env_floatr     sK    
))C

"
"
$CSz	" r   c                 D    t        t        j                  | d      |      S )z*Read an environment variable as a boolean.r   r   r   )r   r   s     r   env_boolr     s    299S"-w??r   )HTTPS_PROXY
HTTP_PROXY	ALL_PROXYhttps_proxy
http_proxy	all_proxy	proxy_urlc                     t        | xs d      j                         }|sy|j                         j                  d      rd|t	        d      d  S |S )zNormalize proxy URLs for httpx/aiohttp compatibility.

    WSL/Clash-style environments often export SOCKS proxies as
    ``socks://127.0.0.1:PORT``. httpx rejects that alias and expects the
    explicit ``socks5://`` scheme instead.
    r   Nzsocks://z	socks5://)r   r   r   
startswithlen)r   	candidates     r   normalize_proxy_urlr     sV     IO$**,I##J/9S_%56788r   c                      t         D ]?  } t        j                  | d      }t        |      }|s'||k7  s-|t        j                  | <   A y)zARewrite supported proxy env vars to canonical URL forms in-place.r   N)_PROXY_ENV_KEYSr   r   r   environ)r   r   
normalizeds      r   normalize_proxy_env_varsr     sB     )		#r"(/
*-(BJJsO	)r   base_urlc                     | xs dj                         }|syt        d|v r|nd|       }|j                  xs dj                         j	                  d      S )a  Return the lowercased hostname for a base URL, or ``""`` if absent.

    Use exact-hostname comparisons against known provider hosts
    (``api.openai.com``, ``api.x.ai``, ``api.anthropic.com``) instead of
    substring matches on the raw URL. Substring checks treat attacker- or
    proxy-controlled paths/hosts like ``https://api.openai.com.example/v1``
    or ``https://proxy.test/api.openai.com/v1`` as native endpoints, which
    leads to wrong api_mode / auth routing.
    r   z://z//rZ   )r   r   hostnamer   rstrip)r   r   parseds      r   base_url_hostnamer     sW     >r
 
 
"CUc\cC5z:FOO!r((*11#66r   modelc                 ^   | xs dj                         j                         }|syd|v r|j                  dd      d   }|j                  d      xs] |j                  d      xsJ |j                  d      xs7 |j                  d	      xs$ |j                  d
      xs |j                  d      S )uA  Return True for model families that require ``max_completion_tokens``.

    OpenAI's newer families reject ``max_tokens`` on /v1/chat/completions with
    HTTP 400 ``unsupported_parameter`` — the caller must send
    ``max_completion_tokens`` instead. This covers:

    - ``gpt-4o`` / ``gpt-4o-mini`` / ``gpt-4o-*``
    - ``gpt-4.1`` / ``gpt-4.1-*``
    - ``gpt-5`` / ``gpt-5.x`` / ``gpt-5-*``
    - ``o1`` / ``o1-*``
    - ``o3`` / ``o3-*``
    - ``o4`` / ``o4-*``

    Handles vendor prefixes like ``openai/gpt-5.4`` by stripping to the tail.
    The URL-based check (``base_url_hostname == "api.openai.com"``) misses
    third-party OpenAI-compatible endpoints (custom OpenAI gateways,
    OpenRouter) that front these models and enforce the same parameter
    constraint, so name-based detection is required as a fallback.
    r   F/r0   r   zgpt-4ozgpt-4.1zgpt-5o1o3o4)r   r   rsplitr   )r   ms     r   "model_forces_max_completion_tokensr     s    ( 
"##%A
axHHS!R 	X 	<<	"	<< 	 <<	 <<		
 <<r   domainc                     t        |       }|sy|xs dj                         j                         j                  d      }|sy||k(  xs |j	                  d|z         S )ac  Return True when the base URL's hostname is ``domain`` or a subdomain.

    Safer counterpart to ``domain in base_url``, which is the substring
    false-positive class documented on ``base_url_hostname``. Accepts bare
    hosts, full URLs, and URLs with paths.

        base_url_host_matches("https://api.moonshot.ai/v1", "moonshot.ai") == True
        base_url_host_matches("https://moonshot.ai", "moonshot.ai")        == True
        base_url_host_matches("https://evil.com/moonshot.ai/v1", "moonshot.ai") == False
        base_url_host_matches("https://moonshot.ai.evil/v1", "moonshot.ai")     == False
    Fr   rZ   )r   r   r   r   endswith)r   r   r   s      r   base_url_host_matchesr     s_     !*Hl!!#))+2237Fv@!2!23<!@@r   )F)r   )N)r   )g        )r   N)2r   r>   rm   loggingr   rE   r"   ri   pathlibr   typingr   r   urllib.parser   r   	getLoggerr   rA   	frozensetr   r   r   r   r   r&   r,   r2   r6   rQ   r   ru   
SafeDumperrw   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   <module>r      s    0    	      ! 			8	$ 563  $ D# D DT D
d |  t  (@  d +C  "T  $ "-U39- -uS$Y7G -C -h E
T	
E
E 	E
 *E E 
EP44?? 4(  % $B
T	
B
B 	B
 B :B 
BJ@
T	
@@ @ 
	@L
# 
 
s 
(  =3 =3 = s 3 3   @# @ @ @3: #* )7 7 7( c  d  FAC A A Ar   