
    Fj!                       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
  ej        e          Z ej                    Zdaded<    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S )&uW  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Optional[str]_CACHED_LINE>   sshmodaldockerdaytonasingularitymanaged_modal      @cmd	list[str]timeoutfloatreturntuple[int, str, str]c                &   	 t          j        | dd|d          }|j        |j        pd                                |j        pd                                fS # t          $ r Y dS t           j        $ r Y dS t          $ r}ddd| fcY d	}~S d	}~ww xY w)
zRun a short subprocess.  Returns (returncode, stdout, stderr).

    Failures (binary missing, timeout, OSError) return (-1, "", "<reason>").
    TF)capture_outputtextr   check )r   z	not found)r   r   r   r   z	oserror: N)	
subprocessrun
returncodestdoutstripstderrFileNotFoundErrorTimeoutExpiredOSError)r   r   resultexcs       ./usr/local/lib/hermes-agent/tools/env_probe.py_runr&   8   s    
)
 
 
  6=#6B"="="?"?&-BUSUA\A\A^A^^^ # # #"""$ ! ! !    ) ) )2(3((((((((()s*   AA 
B$B5	B>BBB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whichr&   )r'   rcouterrs       r%   _python_version_ofr1   N   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   r+   )r'   r.   _out_errs       r%   _has_pip_moduler9   X   s>    < u64<==NBd7Nr2   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')r*   r   yes)r,   r-   r&   r   )r'   coder.   r/   r8   s        r%   _detect_pep668r=   `   s]     < u	; 	 &$-..MBT7+syy{{e++r2   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.
    r5   Nr6   r   z(python )   r   )r,   r-   r&   endswithrsplitr   
IndexErrorAttributeError)r.   r/   r8   tails       r%   _pip_python_versionrF   r   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_BACKENDSr1   r9   rF   r=   r,   r-   r3   
startswithappendjoin)backendpy3_verpy_verpy3_has_pippip_bound_to
py3_pep668has_uvmismatchsilent_conditionsbitspy3_bits              r%   _build_probe_liner`      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7r2   F)force_refreshra   c                j   | r!t           5  daddd           n# 1 swxY w Y   t          t          S t           5  t          t          cddd           S 	 t                      }n4# t          $ r'}t                              d|           d}Y d}~nd}~ww xY w|a|cddd           S # 1 swxY w Y   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.

    ``force_refresh`` is for tests; real callers should never need it.
    Nzenv_probe failed: %sr   )_CACHE_LOCKr   r`   	Exceptionloggerdebug)ra   liner$   s      r%   get_environment_probe_linerh      st       	  	 L	  	  	  	  	  	  	  	  	  	  	  	  	  	  	  	 	 	#	 	 	 	 	 	 	 		$&&DD 	 	 	LL/555DDDDDD	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	sG   B(A$#B($
B.BB(BB((B,/B,Nonec                 J    t           5  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.N)rc   r    r2   r%   _reset_cache_for_testsrl      ss     
                   s   )r   )r   r   r   r   r   r   )r'   r(   r   r   )r'   r(   r   r3   )r   r   )r   r(   )ra   r3   r   r(   )r   ri   )__doc__
__future__r   loggingrN   r,   r   sys	threadingtypingr   	getLogger__name__re   Lockrc   r   __annotations__	frozensetrQ   r&   r1   r9   r=   rF   r`   rh   rl   rk   r2   r%   <module>rx      s    : # " " " " "  				      



          		8	$	$
 in" " " " "
 9      
) ) ) ) ),      , , , ,$   0I8 I8 I8 I8X 9>      :     r2   