§
    øžyj<9  ã                  ó^  — U d Z ddlm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  ej        e¦  «        Z ej        ¦   «         Zdaded<    ej        ¦   «         Zdaded	<   dad
Zda eh d£¦  «        Zd'd(d„Zd)d„Zd*d„Zd*d„Zd+d„Z d,d„Z!ddœd-d„Z"d.d#„Z#d/d$„Z$d/d%„Z%d/d&„Z&dS )0uW  Local-environment toolchain probe for the system prompt.

When the terminal backend is local (the agent's tools run on the same
machine as Hermes itself), we surface a single deterministic line about
Python tooling state so models don't have to discover it by hitting
walls.  Common failure modes this addresses:

* Hermes ships under one Python (e.g. 3.11 in a bundled venv) while the
  user's login shell has a different one (e.g. 3.12 system).  ``pip``
  resolved from PATH may not match ``python3 -m pip``.
* The bundled-venv Python has no pip module installed â†’ ``python3 -m
  pip`` returns ``No module named pip``.
* The system Python is PEP-668 externally-managed â†’ naive
  ``pip install`` fails with ``error: externally-managed-environment``.

The probe is cheap (a handful of subprocess calls, ~50ms total),
cached for the lifetime of the process, and emits **at most one
short line** when something non-default is detected.  When the
environment looks normal (python3+pip both present and matched, no
PEP 668), it emits nothing â€” no token cost.

Remote terminal backends (docker, modal, ssh, â€¦) are skipped: the
host's Python state is irrelevant when tools run inside a sandbox.
The sandbox has its own existing probe (``_probe_remote_backend``)
in ``agent/prompt_builder.py``.

Toggle via ``agent.environment_probe`` in config.yaml (default True).
é    )ÚannotationsN)ÚOptional)Úwindows_hide_flagsúOptional[str]Ú_CACHED_LINEzOptional[threading.Thread]Ú_PROBE_THREADg      $@F>   ÚsshÚmodalÚdockerÚdaytonaÚsingularityÚmanaged_modalÚvercel_sandboxç      @Úcmdú	list[str]ÚtimeoutÚfloatÚreturnútuple[int, str, str]c                ó2  — 	 t          j        ¦   «         5 }t          j        ¦   «         5 }	 t          j        | |||dt          j        t          ¦   «         ¬¦  «        }n,# t          j        $ r Y ddd¦  «         ddd¦  «         dS w xY w|                     d¦  «         |                     d¦  «         |                     ¦   «          	                    dd¦  «         
                    ¦   «         }|                     ¦   «          	                    dd¦  «         
                    ¦   «         }|j        ||fcddd¦  «         cddd¦  «         S # 1 swxY w Y   ddd¦  «         dS # 1 swxY w Y   dS # t          $ r Y dS t          $ r}d	d
d|› �fcY d}~S d}~ww xY w)u  Run a short subprocess.  Returns (returncode, stdout, stderr).

    Failures (binary missing, timeout, OSError) return (-1, "", "<reason>").

    Output is captured through temporary files rather than ``capture_output``
    pipes so ``timeout`` bounds the *whole* call â€” even on native Windows.  A
    console-script launcher (e.g. ``pip.exe``) can spawn a descendant that
    inherits the captured stdout/stderr handles and outlives its parent.  With
    OS pipes, the reader threads inside ``subprocess.communicate()`` then block
    until that descendant closes the write end â€” which the timeout does *not*
    cover, because killing the direct child leaves the grandchild holding the
    pipe.  A whole warm probe could hang for ~28 min this way while holding
    ``_CACHE_LOCK``, wedging every new session's system-prompt build.

    Temp files have no reader threads, so ``wait()`` only ever waits on the
    direct child; a lingering grandchild holding the handle can't block us, and
    the probe genuinely fails open on timeout.
    F)ÚstdoutÚstderrr   ÚcheckÚstdinÚcreationflagsN)éÿÿÿÿÚ r   r   zutf-8Úreplace)r   r   z	not foundr   r   z	oserror: )ÚtempfileÚTemporaryFileÚ
subprocessÚrunÚDEVNULLr   ÚTimeoutExpiredÚseekÚreadÚdecodeÚstripÚ
returncodeÚFileNotFoundErrorÚOSError)r   r   Úout_fÚerr_fÚresultÚoutÚerrÚexcs           ú5/home/ragecks/.hermes/hermes-agent/tools/env_probe.pyÚ_runr4   Q   sƒ  € ð&)ÝÔ#Ñ%Ô%ð 	/¨µÔ0FÑ0HÔ0Hð 	/ÈEð)Ý#œØØ Ø Ø#ØÝ$Ô,õ
 #5Ñ"6Ô"6ðñ ô ��øõ Ô,ð )ð )ð )Ø(ð!	/ð 	/ð 	/ñ 	/ô 	/ð 	/ð 	/ð 	/ð 	/ñ 	/ô 	/ð 	/ð 	/ð 	/ð)øøøà�JŠJ�q‰MŒMˆMØ�JŠJ�q‰MŒMˆMØ—*’*‘,”,×%Ò% g¨yÑ9Ô9×?Ò?ÑAÔAˆCØ—*’*‘,”,×%Ò% g¨yÑ9Ô9×?Ò?ÑAÔAˆCØÔ$ c¨3Ð.ð+	/ð 	/ð 	/ð 	/ñ 	/ô 	/ð 	/ð 	/ð 	/ð 	/ð 	/ñ 	/ô 	/ð 	/ð 	/ð 	/ð 	/ð 	/ð 	/øøøð 	/ð 	/ð 	/ð 	/ð 	/ð 	/ð 	/ñ 	/ô 	/ð 	/ð 	/ð 	/ð 	/ð 	/ð 	/ð 	/øøøð 	/ð 	/ð 	/ð 	/ð 	/ð 	/øõ, ð #ð #ð #Ø"Ð"Ð"Ýð )ð )ð )Ø�2Ð( 3Ð(Ð(Ð(Ð(Ð(Ð(Ð(Ð(Ð(øøøøð)øøøsŸ   ‚E. •E!©E	«1AÁE	ÁBÁ,E	Á-E!Á8E. ÂBÂB*E	Ä0E!Ä<E. Å	E	ÅE!ÅE	ÅE!ÅE. Å!E%Å%E. Å(E%Å)E. Å.
FÅ;	FÆFÆFÆFÚbinaryÚstrc                ór   — t          j        | ¦  «        sdS t          | ddg¦  «        \  }}}|dk    r|r|S dS )zFReturn a short version string like ``3.12.4`` for ``binary``, or None.Nú-cz`import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')r   ©ÚshutilÚwhichr4   )r5   Úrcr0   r1   s       r3   Ú_python_version_ofr=   �   sW   € åŒ<˜ÑÔð ØˆtÝ˜ ð  (Jð  Kñ  Lô  L�L€BˆˆSØ	ˆQ‚w€w�3€wØˆ
Øˆ4ó    Úboolc                óh   — t          j        | ¦  «        sdS t          | dddg¦  «        \  }}}|dk    S )z/True if ``<binary> -m pip --version`` succeeds.Fz-mÚpipú	--versionr   r9   )r5   r<   Ú_outÚ_errs       r3   Ú_has_pip_modulerE   ‹   s>   € åŒ<˜ÑÔð ØˆuÝ˜6 4¨°Ð<Ñ=Ô=�N€BˆˆdØ�Š7€Nr>   c                óš   — t          j        | ¦  «        sdS d}t          | d|g¦  «        \  }}}|dk    o|                     ¦   «         dk    S )zÑTrue when ``<binary>``'s install location is PEP-668 externally-managed.

    Looks for ``EXTERNALLY-MANAGED`` next to the stdlib (the marker file
    Debian/Ubuntu drop in to gate naive ``pip install``).
    Fz™import sys, os;stdlib = os.path.dirname(os.__file__);marker = os.path.join(stdlib, 'EXTERNALLY-MANAGED');print('yes' if os.path.exists(marker) else 'no')r8   r   Úyes)r:   r;   r4   r)   )r5   Úcoder<   r0   rD   s        r3   Ú_detect_pep668rI   “   s]   € õ Œ<˜ÑÔð Øˆuð	;ð 	õ ˜& $¨Ð-Ñ.Ô.�M€BˆˆTØ�Š7Ð+�s—y’y‘{”{ eÒ+Ð+r>   c                 óD  — t          j        d¦  «        sdS t          ddg¦  «        \  } }}| dk    s|sdS d|v rf|                     d¦  «        rQ	 |                     dd¦  «        d         }|dd…                              ¦   «         S # t          t          f$ r Y dS w xY wdS )	zùIf ``pip`` is on PATH, return the Python version it's bound to.

    ``pip --version`` output looks like::

        pip 24.0 from /usr/lib/python3/dist-packages/pip (python 3.12)

    Returns the parenthesised version (e.g. ``"3.12"``) or None.
    rA   NrB   r   z(python ú)é   r   )r:   r;   r4   ÚendswithÚrsplitr)   Ú
IndexErrorÚAttributeError)r<   r0   rD   Útails       r3   Ú_pip_python_versionrR   ¥   sÂ   € õ Œ<˜ÑÔð ØˆtÝ˜% Ð-Ñ.Ô.�M€BˆˆTØ	ˆQ‚w€w�c€wØˆtà�SÐÐ˜SŸ\š\¨#Ñ.Ô.Ðð	Ø—:’:˜j¨!Ñ,Ô,¨QÔ/ˆDØ˜˜˜”9—?’?Ñ$Ô$Ð$øÝ�NÐ+ð 	ð 	ð 	Ø�4�4ð	øøøàˆ4s   Á7B ÂBÂBc                 óô  — t          j        d¦  «        pd                     ¦   «                              ¦   «         } | t          v rdS t          d¦  «        }t          d¦  «        }|rt          d¦  «        nd}t          ¦   «         }|rt          d¦  «        nd}t          j
        d¦  «        du}t          |o|o|                     |¦  «         ¦  «        }|duo	|o| o| p|}|rdS g }	|r"d	|› �}
|s|
d
z  }
|	                     |
¦  «         n|	                     d¦  «         |r||k    r|	                     d|› �¦  «         n|s|r|	                     d¦  «         |r7|r|	                     d|› d�¦  «         n3|s|	                     d|› �¦  «         n|rn|	                     d¦  «         |r|	                     d¦  «         |r|	                     d¦  «         |	sdS dd                     |	¦  «        z   dz   S )uØ   Build the one-liner.  Returns "" when nothing notable is detected.

    Emit only when SOMETHING is off â€” the goal is to save the model from
    hitting an avoidable wall, not to narrate a healthy environment.
    ÚTERMINAL_ENVÚlocalr   Úpython3ÚpythonFÚuvNzpython3=z (no pip module)zpython3=missingzpython=zpython=missing (use python3)u   pipâ†’pythonz (mismatch)zpip=missingzPEP 668=yes (use venv or uv)zuv=installedzPython toolchain: z, ú.)ÚosÚgetenvr)   ÚlowerÚ_REMOTE_BACKENDSr=   rE   rR   rI   r:   r;   r?   Ú
startswithÚappendÚjoin)ÚbackendÚpy3_verÚpy_verÚpy3_has_pipÚpip_bound_toÚ
py3_pep668Úhas_uvÚmismatchÚsilent_conditionsÚbitsÚpy3_bits              r3   Ú_build_probe_linerl   ½   sŽ  € õ Œy˜Ñ(Ô(Ð3¨G×:Ò:Ñ<Ô<×BÒBÑDÔD€GØÕ"Ð"Ð"Øˆrå  Ñ+Ô+€GÝ Ñ)Ô)€FØ07ÐB•/ )Ñ,Ô,Ð,¸U€KÝ&Ñ(Ô(€LØ.5Ð@• 	Ñ*Ô*Ð*¸5€Jõ Œ\˜$ÑÔ tÐ+€Fõ �LÐU WÐU°W×5GÒ5GÈÑ5UÔ5UÐ1UÑVÔV€Hà�tÐð 	'Øð	'àˆLð	'ð ˆ^Ð%˜vð	 ð ð Øˆrð €DØð 'Ø&˜WÐ&Ð&ˆØð 	*ØÐ)Ñ)ˆGØ�Š�GÑÔÐÐà�ŠÐ%Ñ&Ô&Ð&àð 4�&˜GÒ#Ð#Ø�ŠÐ&˜fÐ&Ð&Ñ'Ô'Ð'Ð'Øð 4˜ð 4ð 	�ŠÐ2Ñ3Ô3Ð3àð #Øð 	7Ø�KŠKÐ@ |Ð@Ð@Ð@ÑAÔAÐAÐAØð 	7ð �KŠKÐ5 |Ð5Ð5Ñ6Ô6Ð6øØ	ð #àà�Š�MÑ"Ô"Ð"àð 4Ø�ŠÐ2Ñ3Ô3Ð3àð $Ø�Š�NÑ#Ô#Ð#àð Øˆrà $§)¢)¨D¡/¤/Ñ1°CÑ7Ð7r>   )Úforce_refreshrm   c                ó´  — | rHt           5  dat                               ¦   «          dat
          dz  adaddd¦  «         n# 1 swxY w Y   t                               ¦   «         r	t          pdS t          ¦   «          t          rdnt          }t           
                    |¬¦  «        s+t          s"dat                               dt          ¦  «         dS t          pdS )	u¥  Return the cached probe line (building it on first call).

    Returns "" when the environment is clean â€” the system prompt
    assembler should drop the section in that case rather than
    emit an empty heading.

    The probe itself always runs in a single background worker thread;
    this function waits on its completion event for at most
    ``_PROBE_WAIT_TIMEOUT`` seconds and then fails open with "".  A
    wedged probe subprocess (#67964) therefore can never block
    system-prompt construction â€” at worst the toolchain line is absent
    from prompts built while the probe is stuck.

    ``force_refresh`` is for tests; real callers should never need it.
    NrL   Fr   gš™™™™™©?)r   Tzcenv_probe did not finish within %.0fs; building the system prompt without the Python toolchain line)Ú_CACHE_LOCKr   Ú_PROBE_DONEÚclearr   Ú
_PROBE_GENÚ_WAIT_ALREADY_TIMED_OUTÚis_setÚ_ensure_probe_startedÚ_PROBE_WAIT_TIMEOUTÚwaitÚloggerÚwarning)rm   Úwait_timeouts     r3   Úget_environment_probe_liner{     s'  € ð" ð ,Ýð 	,ð 	,ØˆLÝ×ÒÑÔÐØ ˆMÝ˜!‰OˆJØ&+Ð#ð	,ð 	,ð 	,ñ 	,ô 	,ð 	,ð 	,ð 	,ð 	,ð 	,ð 	,øøøð 	,ð 	,ð 	,ð 	,õ ×ÒÑÔð "ÝÐ!˜rÐ!åÑÔÐÝ2ÐK�4�4Õ8K€LÝ×Ò LÐÑ1Ô1ð õ 'ð 	Ø&*Ð#Ý�NŠNð;å#ñô ð ð
 ˆrÝÐ˜2Ðs   Š*A Á AÁAÚgenÚintÚNonec                ó6  — 	 t          ¦   «         }n4# t          $ r'}t                               d|¦  «         d}Y d}~nd}~ww xY wt          5  | t
          k    r	 ddd¦  «         dS |at                               ¦   «          ddd¦  «         dS # 1 swxY w Y   dS )uD   Body of the single probe thread â€” computes and publishes the line.zenv_probe failed: %sr   N)	rl   Ú	Exceptionrx   Údebugro   rr   r   rp   Úset)r|   Úliner2   s      r3   Ú_probe_workerr„   <  s  € ðÝ Ñ"Ô"ˆˆøÝð ð ð Ý�ŠÐ+¨SÑ1Ô1Ð1Øˆˆˆˆˆˆøøøøðøøøõ 
ð ð Ø•*ÒÐØðð ð ñ ô ð ð ð ð ˆÝ�ŠÑÔÐð	ð ð ñ ô ð ð ð ð ð ð ð øøøð ð ð ð ð ð s*   ‚ ‘
A›=½AÁBÁ&BÂBÂBc                 óh  — t           5  t                               ¦   «         r	 ddd¦  «         dS t          �'t                               ¦   «         r	 ddd¦  «         dS t          j        t          t          fdd¬¦  «        at           	                    ¦   «          ddd¦  «         dS # 1 swxY w Y   dS )z?Start the probe worker if it isn't running and hasn't finished.Nz	env-probeT)ÚtargetÚargsÚnameÚdaemon)
ro   rp   rt   r   Úis_aliveÚ	threadingÚThreadr„   rr   Ústart© r>   r3   ru   ru   K  s,  € õ 
ð ð Ý×ÒÑÔð 	Øðð ð ñ ô ð ð ð õ Ð$­×)?Ò)?Ñ)AÔ)AÐ$Øð	ð ð ñ ô ð ð ð õ
 "Ô(Ý Ý�ØØð	
ñ 
ô 
ˆõ 	×ÒÑÔÐðð ð ñ ô ð ð ð ð ð ð ð øøøð ð ð ð ð ð s   ˆB'°!B'Á<B'Â'B+Â.B+c                 ó"   — t          ¦   «          dS )aË  Kick off the probe in a background thread so the first
    system-prompt build doesn't pay the ~0.5s of subprocess calls
    (python3/pip/PEP-668 version checks) on the time-to-first-token
    critical path.

    Idempotent and fail-safe.  The prompt-build call to
    ``get_environment_probe_line`` waits (bounded) on the same worker's
    completion event instead of recomputing.  Called from agent init
    (all platforms); safe to call from anywhere.
    N)ru   rŽ   r>   r3   Úwarm_environment_probe_asyncr�   \  s   € õ ÑÔÐÐÐr>   c                 ó˜   — t           5  dat                               ¦   «          dat
          dz  adaddd¦  «         dS # 1 swxY w Y   dS )u8   Test helper â€” clear the cache between probe scenarios.NrL   F)ro   r   rp   rq   r   rr   rs   rŽ   r>   r3   Ú_reset_cache_for_testsr’   j  s™   € õ 
ð (ð (ØˆÝ×ÒÑÔÐØˆÝ�a‰ˆ
Ø"'Ðð(ð (ð (ñ (ô (ð (ð (ð (ð (ð (ð (ð (øøøð (ð (ð (ð (ð (ð (s   ˆ*?¿AÁA)r   )r   r   r   r   r   r   )r5   r6   r   r   )r5   r6   r   r?   )r   r   )r   r6   )rm   r?   r   r6   )r|   r}   r   r~   )r   r~   )'Ú__doc__Ú
__future__r   ÚloggingrZ   r:   r"   r    r‹   Útypingr   Úhermes_cli._subprocess_compatr   Ú	getLoggerÚ__name__rx   ÚLockro   r   Ú__annotations__ÚEventrp   r   rr   rv   rs   Ú	frozensetr]   r4   r=   rE   rI   rR   rl   r{   r„   ru   r�   r’   rŽ   r>   r3   ú<module>rž      s  ððð ð ð: #Ð "Ð "Ð "Ð "Ð "à €€€Ø 	€	€	€	Ø €€€Ø Ð Ð Ð Ø €€€Ø Ð Ð Ð Ø Ð Ð Ð Ð Ð à <Ð <Ð <Ð <Ð <Ð <à	ˆÔ	˜8Ñ	$Ô	$€ð ˆiŒnÑÔ€Ø"€Ð "Ð "Ð "Ñ "ØˆiŒoÑÔ€Ø,0€Ð 0Ð 0Ð 0Ñ 0ð €
ð
 Ð ð  Ð ð
 �9ð ð ð ñ ô Ð ð-)ð -)ð -)ð -)ð -)ð`ð ð ð ðð ð ð ð,ð ,ð ,ð ,ð$ð ð ð ð0O8ð O8ð O8ð O8ðd 9>ð *ð *ð *ð *ð *ð *ðZð ð ð ðð ð ð ð"ð ð ð ð(ð (ð (ð (ð (ð (r>   