
    .cj7                    f   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	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      @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           1/home/ice/.hermes/hermes-agent/tools/env_probe.py_runr3   Q   s   &)#%% 	/0F0H0H 	/E)#  #$,
 #5"6"6   , ) ) )(!	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/)JJqMMMJJqMMM**,,%%gy99??AAC**,,%%gy99??AAC$c3.+	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/,  # # #""" ) ) )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whichr3   )r4   rcr/   r0   s       r2   _python_version_ofr<      sW    < t  (J  K  L  LLBS	Qww3w
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   r8   )r4   r;   _out_errs       r2   _has_pip_modulerD      s>    < u64<==NBd7Nr=   c                    t          j        |           sdS d}t          | d|g          \  }}}|dk    o|                                dk    S )zTrue 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``).
    Fzimport sys, os;stdlib = os.path.dirname(os.__file__);marker = os.path.join(stdlib, 'EXTERNALLY-MANAGED');print('yes' if os.path.exists(marker) else 'no')r7   r   yes)r9   r:   r3   r(   )r4   coder;   r/   rC   s        r2   _detect_pep668rH      s]     < u	; 	 &$-..MBT7+syy{{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 )	zIf ``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.
    r@   NrA   r   z(python )   r   )r9   r:   r3   endswithrsplitr(   
IndexErrorAttributeError)r;   r/   rC   tails       r2   _pip_python_versionrQ      s     < t%-..MBT	QwwcwtSS\\#..	::j!,,Q/D9??$$$N+ 	 	 	44	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<   rD   rQ   rH   r9   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              r2   _build_probe_linerk      s    y((3G::<<BBDDG"""r ++G))F07B/),,,UK&((L.5@	***5J\$t+F LUWUW5G5G5U5U1UVVHt 	'	'L	' ^%v	   r D '&W&& 	*))GG%&&& 4&G##&f&&'''' 4 4 	2333 # 	7KK@|@@@AAAA 	7 KK5|55666	 #M""" 42333 $N### r$))D//1C77r=   )force_refreshrl   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.
    NrK   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)rl   wait_timeouts     r2   get_environment_probe_linerz   	  s'   "  , 	, 	,L M!OJ&+#	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	,  "!r!2K448KLL11  ' 	&*#NN;#  
 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)	rk   	Exceptionrw   debugrn   rq   r   ro   set)r{   liner1   s      r2   _probe_workerr   6  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)
rn   ro   rs   r   is_alive	threadingThreadr   rq   start r=   r2   rt   rt   E  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)rt   r   r=   r2   warm_environment_probe_asyncr   V  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.NrK   F)rn   r   ro   rp   r   rq   rr   r   r=   r2   _reset_cache_for_testsr   d  s     
 ( (a
"'( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (s   *?AA)r   )r   r   r   r   r   r   )r4   r5   r   r   )r4   r5   r   r>   )r   r   )r   r5   )rl   r>   r   r5   )r{   r|   r   r}   )r   r}   )(__doc__
__future__r   loggingrY   r9   r!   sysr   r   typingr   hermes_cli._subprocess_compatr   	getLogger__name__rw   Lockrn   r   __annotations__Eventro   r   rq   ru   rr   	frozensetr\   r3   r<   rD   rH   rQ   rk   rz   r   rt   r   r   r   r=   r2   <module>r      s     : # " " " " "  				      



            < < < < < <		8	$	$ in" " " " "io,0 0 0 0 0 

     
 9      
-) -) -) -) -)`      , , , ,$   0I8 I8 I8 I8X 9> * * * * * *Z      "   ( ( ( ( ( (r=   