
    epj<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/thesage/.hermes/hermes-agent/tools/env_probe.py_runr4   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whichr4   )r5   rcr0   r1   s       r3   _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   r9   )r5   r<   _out_errs       r3   _has_pip_modulerE      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')r8   r   yes)r:   r;   r4   r)   )r5   coder<   r0   rD   s        r3   _detect_pep668rI      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.
    rA   NrB   r   z(python )   r   )r:   r;   r4   endswithrsplitr)   
IndexErrorAttributeError)r<   r0   rD   tails       r3   _pip_python_versionrR      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=   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((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_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!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)	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	$	$ in" " " " "io,0 0 0 0 0 

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