
    )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t           j                  }|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stdin )r   z	not found)r   r   r   r   z	oserror: N)

subprocessrunDEVNULL
returncodestdoutstripstderrFileNotFoundErrorTimeoutExpiredOSError)r   r   resultexcs       4/home/ubuntu/.hermes/hermes-agent/tools/env_probe.py_runr(   8   s    
)$
 
 
  6=#6B"="="?"?&-BUSUA\A\A^A^^^ # # #"""$ ! ! !    ) ) )2(3((((((((()s*   AA" "
B/B 	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_ofr3   O   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)   r0   _out_errs       r'   _has_pip_moduler;   Y   s>    < u64<==NBd7Nr4   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)   coder0   r1   r:   s        r'   _detect_pep668r?   a   s]     < u	; 	 &$-..MBT7+syy{{e++r4   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.
    r7   Nr8   r   z(python )   r   )r.   r/   r(   endswithrsplitr    
IndexErrorAttributeError)r0   r1   r:   tails       r'   _pip_python_versionrH   s   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_BACKENDSr3   r;   rH   r?   r.   r/   r5   
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_linerb      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7r4   F)force_refreshrc   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   rb   	Exceptionloggerdebug)rc   liner&   s      r'   get_environment_probe_linerj      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)re   r    r4   r'   _reset_cache_for_testsrn      ss     
                   s   )r   )r   r   r   r   r   r   )r)   r*   r   r   )r)   r*   r   r5   )r   r   )r   r*   )rc   r5   r   r*   )r   rk   )__doc__
__future__r   loggingrP   r.   r   sys	threadingtypingr   	getLogger__name__rg   Lockre   r   __annotations__	frozensetrS   r(   r3   r;   r?   rH   rb   rj   rn   rm   r4   r'   <module>rz      s    : # " " " " "  				      



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