
    `gj5                    ^   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  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%y)!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_LINEzOptional[threading.Thread]_PROBE_THREADg      $@F>   sshmodaldockerdaytonasingularitymanaged_modalc           
        	 t        j                         5 }t        j                         5 }	 t        j                  | |||dt        j                        }|j                  d       |j                  d       |j                         j                  dd      j                         }|j                         j                  dd      j                         }|j                  ||fcddd       cddd       S # t        j
                  $ r Y ddd       ddd       yw xY w# 1 sw Y   nxY wddd       y# 1 sw Y   yxY w# t        $ r Y y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stderrtimeoutcheckstdinN) r   r   zutf-8replace)r   r   z	not foundr   r   z	oserror: )tempfileTemporaryFile
subprocessrunDEVNULLTimeoutExpiredseekreaddecodestrip
returncodeFileNotFoundErrorOSError)cmdr   out_ferr_fresultouterrexcs           B/root/.hermes/venv/lib/python3.12/site-packages/tools/env_probe.py_runr,   O   sD   &)##% 	/0F0F0H 	/E
)#  #$,, JJqMJJqM**,%%gy9??AC**,%%gy9??AC$$c3.!	/ 	/ 	/ ,, )(	/ 	/ 	/)	/ 	/ 	/ 	/ 	/"  #" )23%((()s   D? D3D)C5BD"	D3+	D? 5DD	D3D? DDD'	#D3*D? 3D<8D? <D? ?	E$
E$EE$E$c                f    t        j                  |       syt        | ddg      \  }}}|dk(  r|r|S y)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,   )binaryrcr(   r)   s       r+   _python_version_ofr4   z   sC    <<  (J  K  LLBS	Qw3
    c                ^    t        j                  |       syt        | dddg      \  }}}|dk(  S )z/True if ``<binary> -m pip --version`` succeeds.Fz-mpip	--versionr   r/   )r2   r3   _out_errs       r+   _has_pip_moduler;      s3    <<64<=NBd7Nr5   c                    t        j                  |       syd}t        | d|g      \  }}}|dk(  xr |j                         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)r0   r1   r,   r    )r2   coder3   r(   r:   s        r+   _detect_pep668r?      sP     <<	; 	 &$-.MBT7+syy{e++r5   c                    t        j                  d      syt        ddg      \  } }}| dk7  s|syd|v r:|j                  d      r)	 |j	                  dd      d   }|dd j                         S y# t        t        f$ r Y yw xY w)	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   )r0   r1   r,   endswithrsplitr    
IndexErrorAttributeError)r3   r(   r:   tails       r+   _pip_python_versionrH      s     <<%-.MBT	QwcSS\\#.	::j!,Q/D9??$$  N+ 		s   'A/ /B Bc                 r   t        j                  d      xs dj                         j                         } | t        v ryt        d      }t        d      }|rt        d      nd}t               }|rt        d      nd}t        j                  d      du}t        |xr |xr |j                  |             }|duxr |xr | xr | xs |}|ryg }	|rd	| }
|s|
d
z  }
|	j                  |
       n|	j                  d       |r||k7  r|	j                  d|        n|s|r|	j                  d       |r/|r|	j                  d| d       n+|s)|	j                  d|        n|rn|	j                  d       |r|	j                  d       |r|	j                  d       |	syddj                  |	      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_BACKENDSr4   r;   rH   r?   r0   r1   bool
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_linerc      s    yy(3G::<BBDG"" +G)F07/),UK&(L.5	*5J\\$t+F LUWUW5G5G5U1UVHt 	'	'L	' ^%v	   DWI&))GG%&&G#gfX&' 	23KK,|nK@A KK,|n56	M"23N#$))D/1C77r5   )force_refreshc                t   | r2t         5  dat        j                          dat
        dz  adaddd       t        j                         r
t        xs dS t                t        rdnt        }t        j                  |      s#t        sdat        j                  dt               yt        xs dS # 1 sw Y   xY w)	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.
    NrB   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)rd   wait_timeouts     r+   get_environment_probe_linerr     s    "  	,L M!OJ&+#	, !r!248KLL1 '&*#NN;#
 21	, 	,s   $B..B7c                    	 t               }t        5  | t
        k7  r
	 ddd       y|at        j                          ddd       y# t        $ r"}t        j                  d|       d}Y d}~_d}~ww xY w# 1 sw Y   yxY w)uD   Body of the single probe thread — computes and publishes the line.zenv_probe failed: %sr   N)	rc   	Exceptionro   debugrf   ri   r   rg   set)genliner*   s      r+   _probe_workerry   /  sz     " 
 *  	   +S1 s'   
A A3A3	A0A++A03A<c                 *   t         5  t        j                         r
	 ddd       yt        t        j	                         r
	 ddd       yt        j                  t        t        fdd      at        j                          ddd       y# 1 sw Y   yxY w)z?Start the probe worker if it isn't running and hasn't finished.Nz	env-probeT)targetargsnamedaemon)
rf   rg   rk   r   is_alive	threadingThreadry   ri   start r5   r+   rl   rl   >  s     
   $)?)?)A	 
 "(( 	
 	  s   B	B	
6B		Bc                     t                y)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)rl   r   r5   r+   warm_environment_probe_asyncr   O  s
     r5   c                     t         5  dat        j                          dat
        dz  adaddd       y# 1 sw Y   yxY w)u8   Test helper — clear the cache between probe scenarios.NrB   F)rf   r   rg   rh   r   ri   rj   r   r5   r+   _reset_cache_for_testsr   ]  sA     
 (a
"'( ( (s   $4=)g      @)r$   z	list[str]r   floatreturnztuple[int, str, str])r2   strr   r   )r2   r   r   rT   )r   r   )r   r   )rd   rT   r   r   )rw   intr   None)r   r   )&__doc__
__future__r   loggingrP   r0   r   sysr   r   typingr   	getLogger__name__ro   Lockrf   r   __annotations__Eventrg   r   ri   rm   rj   	frozensetrS   r,   r4   r;   r?   rH   rc   rr   ry   rl   r   r   r   r5   r+   <module>r      s   : #  	   
   			8	$ inn"m "ioo,0) 0 

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