
    j=                     h   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d3ded	ed
efdZd4ded	ed
efdZde	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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d5d#ed	ed
efd$Z d6d%ed	ed
efd&Z!d7d%ed	e"d
e"fd'Z#d3d%ed	ed
efd(Z$d)Z%d*edz  d
edz  fd+Z&d8d,Z'd-ed
efd.Z(d/ed
efd0Z)d-ed1ed
efd2Z*y)9z*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     /home/cube/projects/richard/traning coach/.omo/evidence/nutricoach-v150-combined/st_01a0560c-r63-installed-wheel-first-claim-qa/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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   r'   s     r   _restore_file_moder*   .   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)r+   r,   
target_str	real_pathtmp_strexcfs          r   atomic_replacerE   ?   s@   ( 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$   )indentr'   datarG   dump_kwargsc                *   t        |       } | j                  j                  dd       |dn
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        j$                  |	|       yt)        t        |	      |       y# 1 sw Y   ExY w# t&        $ r Y yw xY w# t*        $ r' 	 t        j,                  |        # 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)rG   ensure_ascii)r   parentmkdirr&   tempfilemkstempr   stemhasattrr   rU   fdopenjsondumpflushr=   r>   rE   r)   r%   r*   BaseExceptionr?   )
r   rH   rG   r'   rI   original_modefdr+   rD   rA   s
             r   atomic_json_writerh   o   sz   0 :DKKdT2 ,D2Ed2KM##499+QLB
 H 5 IIb$YYr31QII "	
  GGIHHQXXZ  2 #8T2	D) tI>% 21    	IIh 	  		sn   5A E" 5AEE" E 1E" EE" 	EE" EE" "	F,FF	FFFF)default_flow_style	sort_keysextra_contentri   rj   rk   c                   t        |       } | j                  j                  dd       t        |       }t	        j
                  t        | j                        d| j                   dd      \  }}	 t        j                  |dd	      5 }t        j                  ||||
       |r|j                  |       |j                          t        j                  |j                                ddd       t!        ||       }	t#        |	|       y# 1 sw Y   "x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).
    TrK   rN   rO   rP   rQ   rV   rW   rX   )ri   rj   N)r   r[   r\   r&   r]   r^   r   r_   r   ra   yamlrc   writerd   r=   r>   rE   r*   re   r?   r%   )
r   rH   ri   rj   rk   rf   rg   r+   rD   rA   s
             r   atomic_yaml_writero      s   , :DKKdT2'-M##499+QLB
YYr31QIIdA2DPYZ&GGIHHQXXZ  2 #8T2	9m4 21  	IIh 	  		sI   1D 	A D
) D 
DD 	E D65E6	E?EEEkey_pathc                 X   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%        j&                  t)        | j
                        d| j*                   dd      \  }}	 t-        j.                  |dd      5 }|j1                  ||       |j3                          t-        j4                  |j7                                ddd       t9        ||       }t;        ||       y# 1 sw Y   5xY w# 1 sw Y   /xY w# t<        $ r' 	 t-        j>                  |        # t@        $ 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)CommentedMapTrK   rt)typFrF      )mappingsequenceoffsetrrW   rX   NrN   rO   rP   rQ   rV   )!ruamel.yamlrr   ruamel.yaml.commentsrs   r   r[   r\   preserve_quotesallow_unicoderi   rG   r!   r<   loadr   splitr8   r&   r]   r^   r   r_   r   ra   rc   rd   r=   r>   rE   r*   re   r?   r%   )r   rp   r   rr   rs   yaml_rtrD   configcurrentkeyskey
next_valuerf   rg   r+   rA   s                   r   atomic_roundtrip_yaml_updater      s    !1:DKKdT2tnG"G G!&GNN1qN3{{}YYsWY-\\!_6F .- fl+f%G>>#DCRy[[%
*l3%J%GCL  GDH'-M##499+QLB
YYr31QLL#GGIHHQXXZ  2 #8T2	9m4= .-2 21  	IIh 	  		sU   
G !G9 9AG-? G9  G*-G62G9 9	H)HH)	H%"H)$H%%H)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.
    )rb   loadsJSONDecodeError	TypeError
ValueError)r   r   s     r   safe_json_loadsr   +  s7    zz$  )Z8 s     ::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   F  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   Q  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   n  s=    		#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//rN   )r   r   hostnamer   rstrip)r   r   parseds      r   base_url_hostnamer   z  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/   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   rN   )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)+__doc__r2   rb   loggingr   r9   r"   r]   pathlibr   typingr   r   urllib.parser   rm   	getLogger__name__r5   	frozensetr   r   r   r   r   r&   r*   rE   r   rh   ro   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  $ "-U39- -uS$Y7G -C -h B
T	
B
B 	B
 *B B 
BR  % $1
T	
1
1 	1
 1 :1 
1h=
T	
== = 
	=F
# 
 
s 
  s 3 3   @# @ @ @3: #* )7 7 7( c  d  FAC A A Ar   