
    Jj*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'dAd/ed	ed
efd0Z(dBd/ed	e)d
e)fd2Z*d>d/ed	ed
efd3Z+d4Z,d5edz  d
edz  fd6Z-dCd7Z.d8ed
efd9Z/d:ed
efd;Z0d8ed<ed
efd=Z1dS )Dz*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-|                                                                 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     +/home/rurouni/.hermes/hermes-agent/utils.pyis_truthy_valuer      s`    }% % 7{{}}""$$66;;     namec                 J    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!    29T733UCCCCr   pathz
int | Nonec                     	 |                                  r+t          j        |                                 j                  ndS # t          $ r Y dS 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'   &   sV    48KKMMKt|DIIKK/000tK   tts   A A 
AAztuple[int, int] | Nonec                     t           j        dk    rdS 	 |                                 }n# t          $ r Y dS w xY w|j        |j        fS )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-   .   sY    	w'tYY[[   tt9bis   ) 
77ownerc                     |t          t          d          sdS 	 t          j        | |d         |d                    dS # t          $ r Y dS 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   r0   r&   )r    r.   s     r   _restore_file_ownerr3   9   se     }GB00}
uQxq*****   s   "? 
AAmodec                 \    |dS 	 t          j        | |           dS # t          $ r Y dS 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    r4   s     r   _restore_file_moder7   J   sM     |
t   s    
++tmp_pathtargetc           
      L   t          |          }t          j                            |          rt          j                            |          n|}t          |           }	 t          j        ||           n-# t          $ r}|j        t          j        t          j	        fvr t                              d||t          j                            |j        |j                             t          j        ||           	 t          j        ||           n# t          $ r Y nw xY w	 t#          |d          5 }t          j        |                                           ddd           n# 1 swxY w Y   n# t          $ r Y nw xY wt          j        |           Y d}~nd}~ww xY w|S )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)r8   r9   
target_str	real_pathtmp_strexcfs          r   atomic_replacerR   [   s   ( VJ02z0J0JZ  ,,,PZI(mmG

7I&&&&   9U[%+666KO	3955		
 	
 	
 	+++	OGY//// 	 	 	D		i&& %!$$$% % % % % % % % % % % % % % % 	 	 	D	
	''( s    A7 7F!A:F=DF
D FD  F$E34'E'E3'E+	+E3.E+	/E32F3
F =F?F  FF!   )indentr4   datarT   dump_kwargsc                   t          |           } | 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| |	                                 t          j        |	                                           ddd           n# 1 swxY w Y   t%          ||           }
t          |
          }t'          ||           |)	 t          j        ||           dS # t*          $ r Y dS w xY wt-          ||           dS # t.          $ r( 	 t          j        |           n# t*          $ r Y n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)rT   ensure_ascii)r   parentmkdirr'   r-   tempfilemkstempr   stemr2   r   rb   fdopenjsondumpflushrJ   rK   rR   r3   r6   r&   r7   BaseExceptionrL   )r    rU   rT   r4   rV   original_modeoriginal_ownerfdr8   rQ   rN   real_path_objs               r   atomic_json_writerv      s\   0 ::DKdT222 ,DD2Ed2K2KM)$//N#49  LB
"H 5 5 Ib$Yr3111 		!QI "	 
    GGIIIHQXXZZ   		! 		! 		! 		! 		! 		! 		! 		! 		! 		! 		! 		! 		! 		! 		! #8T22	YM>:::-----    }m<<<<<   	Ih 	 	 	D	st   AF AD$F $D((F +D(,4F !E8 8
FF FF 
G&F;:G;
GGGG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).
    Fc                 H    t                                          |d          S )NF)superincrease_indent)selfflow
indentless	__class__s      r   r{   zIndentDumper.increase_indent   s    ww&&tU333r   )FF)__name__
__module____qualname____doc__r{   __classcell__)r   s   @r   rx   rx      sG        	 	4 4 4 4 4 4 4 4 4 4r   rx   )default_flow_style	sort_keysextra_contentr   r   r   c          	      *   t          |           } | j                            dd           t          |           }t	          |           }t          j        t          | j                  d| j         dd          \  }}	 t          j
        |dd	          5 }	t          j        ||	t          ||d
           |r|	                    |           |	                                 t          j        |	                                           ddd           n# 1 swxY w Y   t%          ||           }
t          |
          }t'          ||           t)          ||           dS # t*          $ r( 	 t          j        |           n# t.          $ r Y n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).
    TrX   r[   r\   r]   r^   rc   rd   re   )Dumperr   r   allow_unicodeN)r   rh   ri   r'   r-   rj   rk   r   rl   r   rm   yamlro   rx   writerp   rJ   rK   rR   r3   r7   rq   rL   r&   )r    rU   r   r   r   rr   rs   rt   r8   rQ   rN   ru   s               r   atomic_yaml_writer      s   , ::DKdT222'--M)$//N#49  LB
!Yr3111 	!Q I##5#"     '&&&GGIIIHQXXZZ   '	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	!* #8T22	YM>:::=-88888   	Ih 	 	 	D	sV    E  A1DE  DE  DAE   
F+F ?F 
F
FFFkey_pathc                 N   ddl m} ddlm} t	          |           } | j                            dd            |d          }d|_        d|_        d|_	        |
                    d	d
d	           |                                 rO|                     dd          5 }|                    |          p	 |            }ddd           n# 1 swxY w Y   n
 |            }t          ||          s ||          }|}|                    d          }	|	dd         D ]8}
|                    |
          }t          ||          s |            }|||
<   |}9|||	d         <   t#          |           }t%          |           }t'          j        t+          | j                  d| j         dd          \  }}	 t/          j        |dd          5 }|                    ||           |                                 t/          j        |                                           ddd           n# 1 swxY w Y   t;          ||           }t	          |          }t=          ||           t?          ||           dS # t@          $ r( 	 t/          j!        |           n# tD          $ r Y n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)CommentedMapTrX   rt)typFrS      )mappingsequenceoffsetrrd   re   Nr[   r\   r]   r^   rc   )#ruamel.yamlr   ruamel.yaml.commentsr   r   rh   ri   preserve_quotesr   r   rT   r"   rI   loadr   splitrE   r'   r-   rj   rk   r   rl   r   rm   ro   rp   rJ   rK   rR   r3   r7   rq   rL   r&   )r    r   r   r   r   yaml_rtrQ   configcurrentkeyskey
next_valuerr   rs   rt   r8   rN   ru   s                     r   atomic_roundtrip_yaml_updater   (  s[    !     111111::DKdT222dtnnnG"G G!&GNN1qN333{{}}  YYsWY-- 	7\\!__6F	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 fl++ &f%%G>>#DCRCy  [[%%
*l33 	&%J%GCLGDH'--M)$//N#49  LB
Yr3111 	!QLL###GGIIIHQXXZZ   	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! #8T22	YM>:::=-88888   	Ih 	 	 	D	sh    CCC2I2 	AH&I2 &H**I2 -H*.AI2 2
J$=JJ$
JJ$JJ$textc                 t    	 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.
    )rn   loadsJSONDecodeError	TypeError
ValueError)r   r   s     r   safe_json_loadsr   n  sE    z$ )Z8   s    77c                  b    t           "t          t          dd           pt          j        a t           S )NCSafeLoader)_fast_yaml_loadergetattrr   
SafeLoader r   r   _get_fast_yaml_loaderr     s'     #D->>Q$/r   streamc                 F    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      9V$9$;$;<<<<r   r   c                     t          j        | d                                          }|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   r   intr   r   r   r   raws      r   env_intr     se    
)C


"
"
$
$C 3xx	"      < AA        c                     t          j        | d                                          }|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     sf    
)C


"
"
$
$C Szz	"   r   c                 J    t          t          j        | d          |          S )z*Read an environment variable as a boolean.r   r   r   )r   r   s     r   env_boolr     s!    29S"--w????r   )HTTPS_PROXY
HTTP_PROXY	ALL_PROXYhttps_proxy
http_proxy	all_proxy	proxy_urlc                     t          | pd                                          }|sdS |                                                    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     ss     IO$$**,,I t##J// 989S__%5%56888r   c                      t           D ]=} t          j        | d          }t          |          }|r||k    r|t          j        | <   >dS )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     sW     ) )	#r""(//
 	)*--(BJsO	) )r   base_urlc                     | pd                                 }|sdS t          d|v r|nd|           }|j        pd                                                    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//r[   )r   r   hostnamer   rstrip)r   r   parseds      r   base_url_hostnamer     sl     >r
 
 
"
"C rUc\\cczCzz::FO!r((**11#666r   modelc                    | pd                                                                 }|sdS d|v r|                    dd          d         }|                    d          ph|                    d          pS|                    d          p>|                    d	          p)|                    d
          p|                    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/r1   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 u
axxHHS!R 	X 	<<	""	<<  	 <<	 <<		
 <<r   domainc                     t          |           }|sdS |pd                                                                                    d          }|sdS ||k    p|                    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   r[   )r   r   r   r   endswith)r   r   r   s      r   base_url_host_matchesr     s|     !**H ul!!##))++22377F uv@!2!23<!@!@@r   )F)r   )N)r   )r   )r   N)2r   r?   rn   loggingr   rF   r#   rj   pathlibr   typingr   r   urllib.parser   r   	getLoggerr   rB   	frozensetr   r   r   r   r   r'   r-   r3   r7   rR   r   rv   
SafeDumperrx   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   <module>r      s   0 0    				                  ! ! ! ! ! ! 		8	$	$ 55566 3  $    D D# D DT D D D D
d |     t  (@        d +C     "T  $    "-U39- -uS$Y7G -C - - - -h E E E
T	
E
E 	E
 *E E 
E E E EP4 4 4 4 44? 4 4 4(  % $B B B
T	
B
B 	B
 B :B 
B B B BJ@
T	
@@ @ 
	@ @ @ @L
 
# 
 
s 
 
 
 
(    =3 =3 = = = =  s 3     3      @ @# @ @ @ @ @ @3: #*    ) ) ) )7 7 7 7 7 7( c  d        FAC A A A A A A A Ar   