
    sjr                    Z   d 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Zddl	Z	ddl
Z
ddlZddlmZmZ ddlmZ ddlmZ ddlmZmZmZmZ ddlmZ ddlmZ dd	lmZ  ej        e          Z  e! ej"        d
                    Z#e#re $                    ej%                    e	j&                    Z'dZ( G d de)          Z* G d d          Z+dee,gdf         dz  ddfdZ-dee,gdf         dz  fdZ.de/de,ddfdZ0defdZ1dej2        de,ddfdZ3	 d1de4e,         de,dz  dej2        fdZ5dede/fdZ6dede/ddfd Z7d!e,de8e9e:f         dz  fd"Z; G d# d$e          Z< G d% d&          Z=d'e,de,fd(Z>d)Z? ej@        d*          ZA	 d2d,e,d-ee,         de,fd.ZB G d/ d0e          ZCdS )3aC  Base class for all Hermes execution environment backends.

Unified spawn-per-call model: every command spawns a fresh ``bash -c`` process.
A session snapshot (env vars, functions, aliases) is captured once at init and
re-sourced before each command. CWD persists via in-band stdout markers (remote)
or a temp file (local).
    N)ABCabstractmethod)deque)Path)IOCallableIterableProtocol)get_hermes_home)windows_hide_flags)is_interruptedHERMES_DEBUG_INTERRUPTl    c                   2     e Zd ZdZdddedef fdZ xZS )EnvironmentConnectionErroru  Infrastructure/connection-class failure of a terminal backend.

    Raised when the backend itself is unreachable (SSH host down, Docker
    daemon not running, remote file sync failing on a dead link) — never
    for a command that merely exited nonzero.  Subclassing RuntimeError
    keeps every existing ``except RuntimeError`` catcher working.

    ``terminal_tool`` turns this into a structured ``status: "degraded"``
    tool result (config gate ``terminal.degraded_mode: warn|fail``) so the
    model gets an actionable reason + retry hint instead of a traceback.
    The failed backend is never cached, so a later call retries from
    scratch and simply works once the backend is reachable again.
     )
retry_hintreasonr   c                j    t                                          |           || _        |pd| _        d S )Nu   This is an infrastructure failure, not a command failure. Verify the backend is reachable (network, service running, credentials), then retry the same command — recovery is automatic once the backend is back.)super__init__r   r   )selfr   r   	__class__s      ;/home/agent/.hermes/hermes-agent/tools/environments/base.pyr   z#EnvironmentConnectionError.__init__F   s:       $ 
2 	    )__name__
__module____qualname____doc__strr   __classcell__)r   s   @r   r   r   7   sd          :< 
 
 
s 
3 
 
 
 
 
 
 
 
 
 
r   r   c                       e Zd ZdZdZddeddfdZded	dfd
ZddZ	e
d	efd            Ze
d	efd            Zded	dfdZddded	efdZdS )_BoundedOutputCollectoru_  Retain a bounded 40/60 head-tail window of streamed text.

    When ``spill_path`` is set, the collector also tees the FULL stream to
    that file once eviction begins (up to ``_SPILL_CAP_CHARS``), so a
    truncated foreground result is recoverable without re-running the
    command. Memory stays bounded either way — the spill is disk-only.
    i@KL N	max_chars
spill_pathzPath | Nonec                 p   t          dt          |                    | _        t          | j        dz            | _        | j        | j        z
  | _        g | _        t                      | _        d| _        d| _	        d| _
        t          j                    | _        || _        d | _        d| _        d| _        d S )N   皙?r   F)maxintr#   _head_limit_tail_limit_headr   _tail_head_chars_tail_chars_total_chars	threadingLock_lock_spill_path	_spill_fh_spill_chars_spill_capped)r   r#   r$   s      r   r   z _BoundedOutputCollector.__init__^   s    QI//t~344>D,<< "
!&
^%%
%)-"r   textreturnc                    | j         | j        rdS 	 | j        ddlm}m}  || j         j        d            || j         dd          | _        d                    | j                  d                    | j	                  z   }| j        
                    |           t          |          | _        | j        | j        z
  }|dk    st          |          |k    rR| j        
                    |dt          d|                              | j        
                    d	           d| _        n| j        
                    |           | xj        t          |          z  c_        dS # t          $ r d| _        Y dS w xY w)
zATee ``text`` to the spill file (opened lazily on first overflow).Nr   )ensure_spill_diropen_exclusiveT)privatereplace)r=   errorsr   z+
... [spill capped at 5,000,000 chars] ...
)r4   r7   r5   tools.spill_safetyr;   r<   parentjoinr,   r-   writelenr6   _SPILL_CAP_CHARSr(   OSError)r   r8   r;   r<   backlogbudgets         r   _maybe_spillz$_BoundedOutputCollector._maybe_spillm   s   #t'9#F	&~%OOOOOOOO
 ! !1!8$GGGG!/$d9" " "
 ''$*--
0C0CC$$W---$'LL!*T->>F{{c$ii&00$$T/3q&>>/%:;;;$$%TUUU%)""$$T***T* 	& 	& 	&!%D	&s   EE& &E;:E;
str | Nonec                     | j         5  | j        	 ddd           dS 	 | j                                         n# t          $ r Y nw xY wd| _        t	          | j                  cddd           S # 1 swxY w Y   dS )z8Close the spill file and return its path if it was used.N)r3   r5   closerF   r   r4   r   s    r   close_spillz#_BoundedOutputCollector.close_spill   s    Z 	) 	)~%	) 	) 	) 	) 	) 	) 	) 	)$$&&&&   !DNt'((	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	)s1   	A09A0
AA0AA00A47A4c                 b    | j         5  | j        | j        z   cd d d            S # 1 swxY w Y   d S N)r3   r.   r/   rM   s    r   buffered_charsz&_BoundedOutputCollector.buffered_chars   s|    Z 	7 	7#d&66	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7s   $((c                 R    | j         5  | j        cd d d            S # 1 swxY w Y   d S rP   )r3   r0   rM   s    r   total_charsz#_BoundedOutputCollector.total_chars   st    Z 	% 	%$	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	%s     c                    |sd S | j         5  t          |          }| j        /| j        | j        |z   | j        k    r|                     |           | xj        |z  c_        d}| j        | j        k     rSt          | j        | j        z
  |          }|r4| j
                            |d |                    | xj        |z  c_        |}||z
  }|dk    s| j        dk    r	 d d d            d S || j        k    r[| j                                         | j                            || j         d                     | j        | _        	 d d d            d S ||d          }| j                            |           | xj        t          |          z  c_        | j        | j        k    r| j        | j        z
  }| j        d         }t          |          |k    r7| j                                         | xj        t          |          z  c_        n"||d          | j        d<   | xj        |z  c_        | j        | j        k    d d d            d S # 1 swxY w Y   d S )Nr   )r3   rD   r4   r5   r0   r#   rI   r.   r*   minr,   appendr+   r-   clearr/   popleft)	r   r8   text_lenstarttake	remainingchunkexcessfirsts	            r   rV   z_BoundedOutputCollector.append   s    	FZ '	/ '	/4yyH +*$x/$.@@!!$''')E$"2224+d.>>II !J%%d5D5k222$$,$$ E 5(IA~~!1Q!6!6+'	/ '	/ '	/ '	/ '	/ '	/ '	/ '	/, D,,,
  """
!!$(8'8':':";<<<#'#3 5'	/ '	/ '	/ '	/ '	/ '	/ '	/ '	/8 LEJe$$$E

*"T%555)D,<<
1u::''J&&((($$E

2$$$$)&''NDJqM$$.$$ "T%555?'	/ '	/ '	/ '	/ '	/ '	/ '	/ '	/ '	/ '	/ '	/ '	/ '	/ '	/ '	/ '	/ '	/ '	/s    CI+AIC)IIIr   suffixra   c          	      B   | j         5  t          |          | j        k    r|| j         d         cddd           S d                    | j                  }d                    | j                  }| j        t          |          z
  }| j        |k    r||z   |z   cddd           S d}t          d          D ]n}t          d|t          |          z
            }t          |dz            }||z
  }	t          d| j        |z
  |	z
            }
d|
dd| j        dd	}||k    r n|}ot          d|t          |          z
            }t          |dz            }||z
  }	|	r||	 d         nd}|d|         |d|         z   |z   |z   cddd           S # 1 swxY w Y   dS )
zARender within ``max_chars``, preserving a required status suffix.Nr      r   r'   z

... [OUTPUT TRUNCATED - ,z chars omitted out of z total] ...

)
r3   rD   r#   rB   r,   r-   r0   ranger(   r)   )r   ra   headtail	availablenotice_content_budget
head_chars
tail_charsomittedupdatedrendered_tails                r   renderz_BoundedOutputCollector.render   s   Z 	S 	S6{{dn,,t~o//0	S 	S 	S 	S 	S 	S 	S 	S 774:&&D774:&&DV4I I--d{V+	S 	S 	S 	S 	S 	S 	S 	S F1XX ! !!$Q	CKK(?!@!@ #!566
+j8
a!2Z!?*!LMMC7M C C"/BC C C  f$$E  IF$;<<N^c122J'*4J2<DD*.."M$vjyj'99MIFR9	S 	S 	S 	S 	S 	S 	S 	S 	S 	S 	S 	S 	S 	S 	S 	S 	S 	Ss   (FAF'C FFFrP   )r9   rJ   )r   r   r   r   rE   r)   r   r   rI   rN   propertyrQ   rS   rV   rq    r   r   r"   r"   Q   s0         !# ## #= # # # #& & & & & &@
) 
) 
) 
) 7 7 7 7 X7 %S % % % X%*/3 */4 */ */ */ */X ') S S S SS S S S S S Sr   r"   cbr9   c                     | t           _        dS )z>Register a callback that _wait_for_process fires periodically.N)_activity_callback_localcallback)rt   s    r   set_activity_callbackrx      s    (*%%%r   c                  .    t          t          dd          S )uj  Return the thread-local activity callback (see ``set_activity_callback``).

    Public accessor for callers outside this module that need to capture the
    calling thread's callback before handing work to another thread (the
    callback is thread-local, so a freshly spawned thread cannot read it
    back) — e.g. the manual cron-run heartbeat (#76502).
    rw   N)getattrrv   rs   r   r   get_activity_callbackr{      s     +Z>>>r   statelabelc                    t          j                    }|                     dd          }|| d         z
  |k     rdS || d<   	 t                      }|r+t	          || d         z
            } || d| d           dS dS # t
          $ r Y dS w xY w)a_  Fire the activity callback at most once every ``state['interval']`` seconds.

    *state* must contain ``last_touch`` (monotonic timestamp) and ``start``
    (monotonic timestamp of the operation start).  An optional ``interval``
    key overrides the default 10 s cadence.

    Swallows all exceptions so callers don't need their own try/except.
    intervalg      $@
last_touchNrZ   z (z
s elapsed))time	monotonicgetr{   r)   	Exception)r|   r}   nowr   rt   elapseds         r   touch_activity_if_duer      s     .

CyyT**H
U<  8++E,"$$ 	0#g.//GB%..7.../////	0 	0    s   9A> >
BBc                      t          j        d          } | rt          |           }nt                      dz  }|                    dd           |S )zReturn the host-side root for all sandbox storage (Docker workspaces,
    Singularity overlays/SIF cache, etc.).

    Configurable via TERMINAL_SANDBOX_DIR. Defaults to {HERMES_HOME}/sandboxes/.
    TERMINAL_SANDBOX_DIR	sandboxesTparentsexist_ok)osgetenvr   r   mkdir)customps     r   get_sandbox_dirr     sT     Y-..F ,LL+GGD4G(((Hr   procdatac                      g  _          fd}t          j        |d          }| _        |                                 dS )u  Write *data* to proc.stdin on a daemon thread to avoid pipe-buffer deadlocks.

    On Windows, text-mode stdin (``text=True`` / ``encoding="utf-8"``)
    translates ``\n`` → ``\r\n`` as the data flows through the pipe —
    which corrupts every write_file / patch call because the bytes that
    land on disk include injected carriage returns.  The file IS created,
    but every subsequent byte-count / content compare against the
    caller's ``\n``-only string fails.

    Workaround: write through ``proc.stdin.buffer`` (the underlying byte
    buffer), encoding to UTF-8 ourselves.  That bypasses Python's
    newline translation entirely on every platform.  No behaviour change
    on POSIX — the byte sequence is identical to what text-mode would
    produce there.

    Encoding uses ``errors="surrogateescape"`` — the exact inverse of the
    surrogateescape decode, so original bytes are restored.  For
    surrogate-free strings it is byte-identical to strict UTF-8.
    Surrogates outside the round-trip range U+DC80–U+DCFF raise and are
    recorded on ``proc._hermes_stdin_errors`` while stdin is still closed
    in ``finally`` so the child sees EOF instead of hanging;
    ``_wait_for_process`` reads the recorded error and surfaces it as
    ``stdin_error`` on the result.
    c                     j         $                    t          d                     d S t          j         dj                   } 	 t	          t
                    r                    dd          n}|                     |          }|t          |          k    r#t          d| dt          |           d          n># t          t          f$ r Y n+t          $ r}                    |           Y d }~nd }~ww xY w	 |                                  d S # t          $ r Y d S w xY w# 	 |                                  w # t          $ r Y w w xY wxY w)Nzprocess stdin unavailablebufferutf-8surrogateescapezshort stdin write: z of z bytes)stdinrV   RuntimeErrorrz   
isinstancer   encoderC   rD   BrokenPipeErrorrF   r   rL   )targetrawwrittenexcr   r?   r   s       r   _writez_pipe_stdin.<locals>._writeK  s   :MM,'BCCDDDF Xtz::	=Gc=R=R\$++g'8999X\Cll3''G#c(("" ##V#V#Vc#hh#V#V#VWWW #  ) 	 	 	D 	 	 	
 MM#	      ss   	A8C D( C=D( 	C=C83D( 8C==D( D 
D%$D%(E*D?>E?
E	EEETr   daemonN)_hermes_stdin_errorsr1   Thread_hermes_stdin_threadrZ   )r   r   r   threadr?   s   ``  @r   _pipe_stdinr   .  sg    4 #%F &D      8 VD999F &D
LLNNNNNr   cmd
stdin_datac           
          |                     dt                                 t          j        | ft          j        t          j        |t          j        nt          j        dddd|}|t          ||           |S )a  Spawn a subprocess with standard stdout/stderr/stdin setup.

    If *stdin_data* is provided, writes it asynchronously via :func:`_pipe_stdin`.
    Backends with special Popen needs (e.g. local's ``preexec_fn``) can bypass
    this and call :func:`_pipe_stdin` directly.
    creationflagsNTr   r>   )stdoutstderrr   r8   encodingr?   )
setdefaultr   
subprocessPopenPIPESTDOUTDEVNULLr   )r   r   kwargsr   s       r   _popen_bashr   l  s     o'9';';<<< !+!7jooZ=OGI   D D*%%%Kr   pathc                     |                                  r9	 t          j        |                     d                    S # t          $ r Y nw xY wi S )z:Load a JSON file as a dict, returning ``{}`` on any error.r   r   )existsjsonloads	read_textr   )r   s    r   _load_json_storer     sZ    {{}} 	:dnngn>>??? 	 	 	D	Is   '> 
A
Ac                     | j                             dd           |                     t          j        |d          d           dS )z.Write *data* as pretty-printed JSON to *path*.Tr      )indentr   r   N)rA   r   
write_textr   dumps)r   r   s     r   _save_json_storer     sH    KdT222OODJtA...OAAAAAr   	host_pathc                     	 t          |                                           }|j        |j        fS # t          $ r Y dS w xY w)zIReturn ``(mtime, size)`` for cache comparison, or ``None`` if unreadable.N)r   statst_mtimest_sizerF   )r   sts     r   _file_mtime_keyr     sO    )__!!##RZ((   tts   .1 
??c                       e Zd ZdZdedz  fdZd
dZddedz  defdZe	de
e         dz  fd            Ze	dedz  fd	            ZdS )ProcessHandlezDuck type that every backend's _run_bash() must return.

    subprocess.Popen satisfies this natively.  SDK backends (Modal, Daytona)
    return _ThreadedProcessHandle which adapts their blocking calls.
    r9   Nc                     d S rP   rs   rM   s    r   pollzProcessHandle.poll        r   c                     d S rP   rs   rM   s    r   killzProcessHandle.kill  r   r   timeoutc                     d S rP   rs   r   r   s     r   waitzProcessHandle.wait  r   r   c                     d S rP   rs   rM   s    r   r   zProcessHandle.stdout      (+r   c                     d S rP   rs   rM   s    r   
returncodezProcessHandle.returncode  r   r   r9   NrP   )r   r   r   r   r)   r   r   floatr   rr   r   r   r   r   rs   r   r   r   r     s          &cDj%%%%<<EDL<C<<<<+3$+++ X++C$J+++ X+++r   r   c                       e Zd ZdZ	 ddeg eeef         f         deg df         dz  fdZe	d             Z
e	dedz  fd            Zdedz  fd	Zd
 Zddedz  defdZdS )_ThreadedProcessHandleal  Adapter for SDK backends (Modal, Daytona) that have no real subprocess.

    Wraps a blocking ``exec_fn() -> (output_str, exit_code)`` in a background
    thread and exposes a ProcessHandle-compatible interface.  An optional
    ``cancel_fn`` is invoked on ``kill()`` for backend-specific cancellation
    (e.g. Modal sandbox.terminate, Daytona sandbox.stop).
    Nexec_fn	cancel_fnc                 8    | _         t          j                     _        d  _        d  _        t          j                    \  }}t          j        |ddd           _	        | _
         fd}t          j        |d          }|                                 d S )Nrr   r>   )r   r?   c                     	              \  } }|_         	 t          j        j        |                     dd                     n# t
          $ r Y nw xY wn%# t          $ r}|_        d_         Y d }~nd }~ww xY w	 t          j        j                   n# t
          $ r Y nw xY wj	        
                                 d S # 	 t          j        j                   n# t
          $ r Y nw xY wj	        
                                 w xY w)Nr   r>   r?   r&   )_returncoder   rC   	_write_fdr   rF   r   _errorrL   _doneset)output	exit_coder   r   r   s      r   _workerz0_ThreadedProcessHandle.__init__.<locals>._worker  sO   !$+GII!	#, HT^V]]79]-U-UVVVV   D % % %!#$      %HT^,,,,   D
     	HT^,,,,   D
    s   A /A A 
AA AA C 
A;#A61C 6A;;C ?B 
B&%B&D
C D
 
C-*D
,C--D
Tr   )
_cancel_fnr1   Eventr   r   r   r   pipefdopen_stdoutr   r   rZ   )r   r   r   read_fdwrite_fdr   ts   ``     r   r   z_ThreadedProcessHandle.__init__  s    
 $_&&
'+(, GIIy#	RRR!	! 	! 	! 	! 	! 	!& GD999						r   c                     | j         S rP   )r   rM   s    r   r   z_ThreadedProcessHandle.stdout  s
    |r   r9   c                     | j         S rP   )r   rM   s    r   r   z!_ThreadedProcessHandle.returncode  s    r   c                 F    | j                                         r| j        nd S rP   )r   is_setr   rM   s    r   r   z_ThreadedProcessHandle.poll  s#    #':#4#4#6#6@tD@r   c                 d    | j         r(	 |                                   d S # t          $ r Y d S w xY wd S rP   )r   r   rM   s    r   r   z_ThreadedProcessHandle.kill  sS    ? 	!!!!!   	 	s    
--r   c                 F    | j                             |           | j        S )Nr   )r   r   r   r   s     r   r   z_ThreadedProcessHandle.wait  s!    
(((r   rP   )r   r   r   r   r   tupler   r)   r   rr   r   r   r   r   r   r   rs   r   r   r   r     s         04# #"eCHo-.# BH%,# # # #J   X  C$J       X AcDj A A A A     EDL  C            r   r   
session_idc                     d|  dS )N__HERMES_CWD___rs   )r  s    r   _cwd_markerr    s    ):))))r   z`^declare -x (HERMES_SESSION_|HERMES_UI_SESSION_ID|HERMES_CRON_AUTO_DELIVER_|HERMES_CRON_SESSION)z^[A-Za-z_][A-Za-z0-9_]*$rs   tmp_pathexcluded_namesc                     d |D             }d                     d t          |          D                       }|rd| }d| d|  S )u  Return a shell snippet that dumps ``export -p`` to *tmp_path* minus the
    per-session bridged vars (see ``_SNAPSHOT_EXCLUDED_ENV_REGEX``) and any
    additional names supplied by the caller.

    Unset the bridged vars in a subshell *before* ``export -p``. A line-based
    ``grep -vE`` filter is unsafe: bash 3.2 prints a value containing a newline
    as a multi-line ``declare -x NAME="…`` block, so only the opener matches the
    regex and continuation lines (e.g. ``curl … | bash #`` smuggled into a
    Matrix room/display name via ``HERMES_SESSION_CHAT_NAME``) land in the
    snapshot and execute on the next ``source`` (issue #71296). Unsetting first
    means ``export -p`` never emits those vars — including any continuation
    lines. ``|| true`` keeps the success contract for callers that chain on it.

    The dump MUST be wrapped in a brace group with the redirection applied to
    the group. *tmp_path* is typically a shell-variable expansion (a
    mktemp-allocated per-writer temp name); a redirection attached to a
    pipeline segment would expand it inside that segment's subshell,
    potentially inconsistently with the parent that expands the follow-up
    ``mv``. The brace-group redirect is expanded in the current shell,
    keeping both expansions consistent.
    c                 @    h | ]}t          |t                    ||S rs   )r   r   .0names     r   	<setcomp>z6_export_dump_excluding_session_vars.<locals>.<setcomp>6  s@       dC  %)  r    c              3   >   K   | ]}t          j        |          V  d S rP   shlexquoter
  s     r   	<genexpr>z6_export_dump_excluding_session_vars.<locals>.<genexpr>:  s,      LL5;t,,LLLLLLr   zh{ ( unset ${!HERMES_SESSION_*} ${!HERMES_CRON_AUTO_DELIVER_*} AI_AGENT HERMES_AGENT HERMES_UI_SESSION_IDz( 2>/dev/null; export -p; ) || true; } > )rB   sorted)r  r  
safe_namesextra_unsets       r   #_export_dump_excluding_session_varsr    s    : '  J ((LL
9K9KLLLLLK ('+''	  +	 	 	 	r   c                   2   e Zd ZU dZdZeed<   dZeed<   dZ	e
ed<   defd	Zd6dededefdZddd
ddede
deded
z  def
dZed             Zdee         fdZdeedf         fdZd Zededefd            ZdedefdZdededefdZedededefd            Z	 d7dd d!eded"e
defd#Zed$d%d&ed'ed
z  defd(            Zd!efd)Zd*efd+Zd*efd,Z d8d-Z!	 d9d
d
d/dd0dededed
z  ded
z  d1e
d"e
defd2Z"d3 Z#d4 Z$dedeeed
z  f         fd5Z%d
S ):BaseEnvironmenta  Common interface and unified execution flow for all Hermes backends.

    Subclasses implement ``_run_bash()`` and ``cleanup()``.  The base class
    provides ``execute()`` with session snapshot sourcing, CWD tracking,
    interrupt handling, and timeout enforcement.
    r   _stdin_mode   _snapshot_timeoutF_profile_scoped_passthroughr9   c                     dS )a,  Return the backend temp directory used for session artifacts.

        Most sandboxed backends use ``/tmp`` inside the target environment.
        LocalEnvironment overrides this on platforms like Termux where ``/tmp``
        may be missing and ``TMPDIR`` is the portable writable location.
        z/tmprs   rM   s    r   get_temp_dirzBaseEnvironment.get_temp_dirf  s	     vr   Ncwdr   envc                    || _         || _        |pi | _        t          j                    j        d d         | _        |                                                     d          pd}| d| j         d| _	        | d| j         d| _
        t          | j                  | _        d| _        t                      | _        d| _        d S )N   /z/hermes-snap-z.shz/hermes-cwd-z.txtF)r   r   r!  uuiduuid4hex_session_idr  rstrip_snapshot_path	_cwd_filer  _snapshot_readyr   _snapshot_passthrough_names_prefer_nonlogin)r   r   r   r!  temp_dirs        r   r   zBaseEnvironment.__init__o  s    9":<<+CRC0$$&&--c229c!)MM8HMMM$HH$2BHHH&t'788$58UU( !&r   x   loginr   r   
cmd_stringr2  r   c                J    t          t          |           j         d          )zSpawn a bash process to run *cmd_string*.

        Returns a ProcessHandle (subprocess.Popen or _ThreadedProcessHandle).
        Must be overridden by every backend.
        z must implement _run_bash())NotImplementedErrortyper   )r   r3  r2  r   r   s        r   	_run_bashzBaseEnvironment._run_bash  s$     "T$ZZ%8"U"U"UVVVr   c                     dS )z<Release backend resources (container, instance, connection).Nrs   rM   s    r   cleanupzBaseEnvironment.cleanup  s	     	r   c                     dS )zAReturn backend-specific names that must not persist in snapshots.rs   rs   rM   s    r   ,_additional_profile_scoped_passthrough_namesz<BaseEnvironment._additional_profile_scoped_passthrough_names  s    rr   .c                 h   | j         sdS 	 ddlm}  |            rKddlm} g  |            |                                 R }| j                            d |D                        n,# t          $ r t          
                    dd           Y nw xY wt          t          | j                            S )	aY  Return profile-scoped names that must not persist in the snapshot.

        The set is monotonic for the environment lifetime. A skill/config
        allowlist can be cleared after a value was captured; retaining the
        exclusion prevents that old value from becoming visible to a later
        profile through the shared snapshot.
        rs   r   )is_multiplex_active)get_all_passthroughc              3   x   K   | ]5}t          |t                    t                              |          1|V  6d S rP   )r   r   _SHELL_ENV_NAME_RE	fullmatchr
  s     r   r  zGBaseEnvironment._snapshot_excluded_passthrough_names.<locals>.<genexpr>  s_       8 8!$,,8 2D1M1Md1S1S88 8 8 8 8 8r   z4Could not refresh profile-scoped snapshot exclusionsT)exc_info)r  agent.secret_scoper=  tools.env_passthroughr>  r;  r-  updater   loggerdebugr   r  )r   r=  r>  namess       r   $_snapshot_excluded_passthrough_namesz4BaseEnvironment._snapshot_excluded_passthrough_names  s    / 	2	>>>>>>""$$ 
EEEEEE((**FFHH  077 8 8 %8 8 8   
  	 	 	LLF      	
 VD<==>>>s   AA' '&BBc                    |                      | j                  }|                     | j                  }|                     | j        dz             }d}|                                 }d| dt          ||           d| d| d| d| d	| d
| d| d| d| d| j         d| j         d}	 |                     |d| j                  }| 	                    || j                  }t          |                    d          pd          dk    r%t          d|                    d                     d| _        |                     |           t                              d| j        | j                   dS # t$          $ r?}	d| _        t'          |	          }
d}	 |                     ddt)          d| j                            }| 	                    |t)          d| j                            }t          |                    d          pd          dk    }|s+|                    d          p|
                                p|
}
n# t$          $ r}|
 d| }
Y d}~nd}~ww xY w|| _        |r"t                              d| j        |	           n't                              d| j        |
           Y d}	~	dS Y d}	~	dS d}	~	ww xY w) zCapture login shell environment into a snapshot file.

        Called once after backend construction.  On success, sets
        ``_snapshot_ready = True`` so subsequent commands source the snapshot
        instead of running with ``bash -l``.
        .tmp.XXXXXXXXXX"$__hermes_snap_tmp"z%umask 077
__hermes_snap_tmp=$(mktemp z) || exit 1
z
__hermes_fns=$(declare -F | awk '{print $3}' | grep -vE '^_[^_]') || true
[ -n "$__hermes_fns" ] && declare -f $__hermes_fns >> z! 2>/dev/null || true
alias -p >> z#
echo 'shopt -s expand_aliases' >> z
echo 'set +e' >> z
echo 'set +u' >> z
mv -f r  z
 || rm -f z
builtin cd -- z 2>/dev/null || true
printf '\n%sz\n' "$(pwd -P)"
T)r2  r   r   r   r   z)snapshot bootstrap failed with exit code z-Session snapshot created (session=%s, cwd=%s)Ftrue   r   z; non-login probe: Nu_   init_session failed (session=%s): %s — login bash unusable; falling back to non-login bash -cuL   init_session failed (session=%s): %s — falling back to bash -l per command)_quote_cwd_for_cdr   _quote_shell_pathr*  rI  r  r  r7  r  _wait_for_processr)   r   r   r,  _update_cwdrF  infor(  r   r   rU   stripr.  warning)r   _quoted_cwd_quoted_snap_snap_tmp_template	_snap_tmpsnapshot_excluded	bootstrapr   resultr   detailprefer_nonloginprobeprobe_result	probe_excs                  r   init_sessionzBaseEnvironment.init_session  s     ,,TX66 --d.ABB* "33D4GJ[4[\\*	 EEGGU*<U U29>OPPU U" #U U$ %%U U& 2;'U U( !*)U U* !*+U U0 1U U0 ".1U U0 :C1U U2 )3U U4 *5U U4 /3.>5U U U 	:-	>>)4AW>XXD++D$:P+QQF6::l++0q11Q66"Z

<@X@XZZ   $(D V$$$KK?     
  	 	 	#(D 
 XXF#OCvUCDLbDcDcdd#55eSTMcEdEd5ee"%l&6&6|&D&D&I"J"Ja"O& V*..x88BFIIKKUvF C C C"BByBBC %4D! M$	    :$	             '	sE   +CE5 5J? J:B*IJ:
IIJ:IAJ::J?c                     | dk    r| S | dk    rdS |                      d          rdt          j        | dd                    S t          j        |           S )z7Quote a ``cd`` target while preserving ``~`` expansion.~z~/z$HOMEz$HOME/r   N)
startswithr  r  )r   s    r   rP  z!BaseEnvironment._quote_cwd_for_cd=  sd     #::J$;;7>>$ 	32EKABB00222{3r   r   c                 *    t          j        |          S )zQuote *path* for interpolation into a bash script.

        LocalEnvironment overrides this to rewrite native/mixed Windows
        paths to ``/c/...`` before quoting. Remote backends leave paths
        as-is (they already speak POSIX).
        r  )r   r   s     r   rQ  z!BaseEnvironment._quote_shell_pathH  s     {4   r   commandc                    |                     dd          }|                     | j                  }|                     | j        dz             }d}g }|                                 }g }	|D ]_}
d|
 }| d}| d}|	                    |
||f           |                    | d|
 d	           |                    | d|
 d
           `| j        r|                    d| d           |	D ]C\  }
}}|                    d| d|
 d| d|
 d	           |                    d| d|            D|                    d           |                     |          }|                    d| d           |                    d| d           |                    d           |                    d           | j        r3|                    d| dt          ||           d| d| d| d           |                    d| j         d | j         d!           |                    d"           d#	                    |          S )$zwBuild the full bash script that sources snapshot, cd's, runs command,
        re-dumps env vars, and emits CWD markers.'z'\''rK  rL  _HERMES_RUNTIME_PASSTHROUGH__PRESENT_VALUEz=${z+x}z-}zsource z >/dev/null 2>&1 || truezif [ "$z" = x ]; then export z="$z"; else unset z; fizunset r  zPexport AI_AGENT="${AI_AGENT:-hermes-agent}" HERMES_AGENT="${HERMES_AGENT:-true}"zbuiltin cd -- z || exit 126zeval 'z__hermes_ec=$?z	umask 077z__hermes_snap_tmp=$(mktemp z) && { z
 && mv -f z; } 2>/dev/null || rm -f z 2>/dev/null || truez
printf '\nrM  z\n' "$(pwd -P)"zexit $__hermes_ec
)
r>   rQ  r*  rI  rV   r,  rP  r  r  rB   )r   rh  r   escapedrX  rY  rZ  partspassthrough_namessaved_namesr  markerpresentvalue
quoted_cwds                  r   _wrap_commandzBaseEnvironment._wrap_commandQ  sH    //#w// --d.ABB "33D4GJ[4[\\*	 EEGG 35% 	2 	2D:D::F)))G%%%Egu5666LLG33333444LLE00t0001111  	LL@,@@@   %0 	5 	5 D'5LL)' ) ) ) ) ) )") ) )   LL3'33E334444 	3	
 	
 	
 ++C00
>j>>>??? 	(g((()))%&&& 	[!!!  	LLH.@ H H9)EVWWH H%H H(4H H )2H H H   	R$*RRd.>RRR	
 	
 	
 	()))yyr   c                 b    dt          j                    j        dd          }|  d| d| d| S )z;Append stdin_data as a shell heredoc to the command string.HERMES_STDIN_Nr#  z << 'z'
rn  )r%  r&  r'  )rh  r   	delimiters      r   _embed_stdin_heredocz$BaseEnvironment._embed_stdin_heredoc  sG     <DJLL$4SbS$9;;	GG	GGjGGIGGGr   )bounded_capturer   r|  c          
         |r$	 ddl m}  |            }n# t          $ r d}Y nw xY wt          }d}|r	 t	                      dz  dz  }|dt          t          j                               dt          j                     dt                    d	z  d
dz  }|
                                rpt          j                    dz
  }|                    d          D ]D}		 |	                                j        |k     r|	                                 5# t          $ r Y Aw xY wn# t          $ r d}Y nw xY wt!          ||           t#          j        d          d          fdfd}
t'          j        |
d          }|                                 t          j                    |z   }t          j                    }||d}t'          j                    j        }t3          dd          }d}|}d}t5                      du }t6          r/t8                              d||||sdndt=                                 	 d}                                \|dz  }t=                      rt6          r8t8                              d|||t          j                    |d         z
             |                                 |!                    d !           | "                    #                    d"#          d$          S t          j                    |k    rt6          rt8                              d%||||           |                                 |!                    d !           d&| d'}| "                    j$        dk    r(#                    |#          %                                n#                    |#          d(          S tM          |d)           t6          rt          j                    |z
  d*k    rvt5                      du }t8                              d+|||t          j                    |d         z
  t=                      |sdnd|r|sd,nd-           t          j                    }|}t          j'        |           |d.k     rtQ          |d/z  d.          }                                \n# tR          tT          f$ r~ t6          r8t8                              d0|||t          j                    |d         z
             	 |                                 |!                    d !           n# t          $ r Y nw xY w w xY w|!                    d !           	 j+        ,                                 n# t          $ r Y nw xY wt6          r>t8                              d1|||t          j                    |d         z
  j-                   t3          d2d          }||!                    d3!           #                                }| "                    |j-                  }t3          d4d          }|r&t]          |d                   }||d5<   |d6| d7z   |d8<   |S )9u  Poll-based wait with interrupt checking and stdout draining.

        Shared across all backends — not overridden.

        ``bounded_capture=True`` (foreground terminal-tool path only) retains
        at most ``tool_output.max_bytes`` of output in a head/tail window
        while draining, so a verbose subprocess cannot OOM the process
        (#64435). The default (False) preserves full-fidelity capture for
        internal consumers — file-operation ``cat`` reads feeding the patch
        engine, code-execution RPC reads, log reads — where truncation would
        corrupt data.

        Fires the ``activity_callback`` (if set on this instance) every 10s
        while the process is running so the gateway's inactivity timeout
        doesn't kill long-running commands.

        Also wraps the poll loop in a ``try/finally`` that guarantees we
        call ``self._kill_process(proc)`` if we exit via ``KeyboardInterrupt``
        or ``SystemExit``.  Without this, the local backend (which spawns
        subprocesses with ``os.setsid`` into their own process group) leaves
        an orphan with ``PPID=1`` when python is shut down mid-tool — the
        ``sleep 300``-survives-30-min bug Physikal and I both hit.
        r   )get_max_bytesiP  Ncachezterminal-outputzout--i  xz.logi:	 z	out-*.log)r$   r   r>   r   c                 
   	 | D ]e}|t          |t                    r)                                        |                     C                    t	          |                     fn# t
          $ r Y nw xY w	                     dd          }|r                    |           d S d S # t
          $ r Y d S w xY w# 	                     dd          }|r                    |           w w # t
          $ r Y w w xY wxY w)Nr   Tfinal)r   bytesrV   decoder   r   )streampiecerg   decoderr   s      r   _drain_iterablez:BaseEnvironment._wait_for_process.<locals>._drain_iterable%  sZ   # 2 2E} !%// 2gnnU&;&;<<<<c%jj11112    ">>#T>::D ,d+++++, ,    DD	">>#T>::D ,d++++,    Ds`   A(A, +C  ,
A96C  8A99C  =.B/ /
B=<B= D.C20D2
C?<D>C??Dc                  P   j         } | d S t          | dd           }	 t          |          r
 |            nd }n# t          $ r d }Y nw xY wt	          |t
                    r|dk     r |            d S t          j        dk    r	 	 t          j        |d          }|sn)
	                    	
                    |                     An# t          t          f$ r Y nw xY w	 	
                    dd          }|r
	                    |           nU# t          $ r Y nIw xY w# 	 	
                    dd          }|r
	                    |           w w # t          $ r Y w w xY wxY wd S d}	 	 	 t          j        |gg g d          \  }}}n# t          t          f$ r Y nw xY w|r\	 t          j        |d          }n# t          t          f$ r Y nSw xY w|snL
	                    	
                    |                     d}n                                 |d	z  }|d
k    rn	 	
                    dd          }|r
	                    |           d S d S # t          $ r Y d S w xY w# 	 	
                    dd          }|r
	                    |           w w # t          $ r Y w w xY wxY w)Nfilenor   ntT   r   r  g?r&      )r   rz   callabler   r   r)   r   r  readrV   r  
ValueErrorrF   selectr   )r  r  fdr]   rg   idle_after_exitreadyrj   r  r  r   r   s           r   _drainz1BaseEnvironment._wait_for_process.<locals>._drain?  s    [F~VXt44F!)&!1!1;VVXXXt   b#&& "q&&''' w$= "D 1 1$ "!gnnU&;&;<<<	= ""G,   D&~~c~>> 0"MM$///$   	&~~c~>> 0"MM$////0$   O"&,mRD"b#&F&Fq!!&0    ""$&GB$5$5EE *G4 " " "!E"$ "!gnnU&;&;<<<*+0 (1,*a//!)"2">>#T>::D ,d+++++, ,    DD	">>#T>::D ,d++++,    Ds   ; A
	A
AC	 D  	CD  CD  !.D 
DD E"".EE"
EE"EE"*I# ,F	 I# 	FI# FI# #F9 8I# 9G
I# GAI#  .I 
I I #J%%.JJ%
J"J%!J""J%Tr   )r   rZ   pidFzg[interrupt-debug] _wait_for_process ENTER tid=%s pid=%s timeout=%ss activity_cb=%s initial_interrupt=%sr   MISSINGg{Gzt?r&   ut   [interrupt-debug] _wait_for_process INTERRUPT DETECTED tid=%s pid=%s iter=%d elapsed=%.1fs — killing process grouprZ   r   r   z
[Command interrupted]r`      zM[interrupt-debug] _wait_for_process TIMEOUT tid=%s pid=%s iter=%d timeout=%ssz
[Command timed out after zs]|   zterminal command runningg      >@zo[interrupt-debug] _wait_for_process HEARTBEAT tid=%s pid=%s iter=%d elapsed=%.0fs interrupt=%s activity_cb=%s%sz (LOST during run)r   g?g      ?u   [interrupt-debug] _wait_for_process EXCEPTION_EXIT tid=%s pid=%s iter=%d elapsed=%.1fs — killing subprocess group before re-raisezd[interrupt-debug] _wait_for_process EXIT (natural) tid=%s pid=%s iter=%d elapsed=%.1fs returncode=%sr      r   stdin_errorz
[stdin write failed: ]r   )/tools.tool_output_limitsr~  r   _UNBOUNDED_CAPTURE_CHARSr   r)   r   r   getpididis_dirglobr   r   unlinkrF   r"   codecsgetincrementaldecoderr1   r   rZ   r   current_threadidentrz   r{   _DEBUG_INTERRUPTrF  rT  r   r   _kill_processrB   _finalize_wait_resultrq   rS   lstripr   sleeprU   KeyboardInterrupt
SystemExitr   rL   r   r   ) r   r   r   r|  r~  capture_limitr$   	spill_dircutoffoldr  drain_threaddeadline_now_activity_state_tid_pid_iter_count_last_heartbeat_last_interrupt_state_cb_was_none_poll_sleeptimeout_msg_cb_now_nonestdin_threadrenderedr]  stdin_errorserrr  r  r   s     `                           @@@r   rR  z!BaseEnvironment._wait_for_process  s   4  	5'BBBBBB - ' ' ' &' 5M
 	""+--7:KK	&)jDIKK0@0@)j)j29;;)j)jQSTXQYQY\bQb)j)j)j)jj
##%% !!Y[[94F(~~k:: ! !!"xxzz2V;; #

& ! ! ! D! " " "!


"(:NNN4 8&.w77yIII	 	 	 	 	 	4F	 F	 F	 F	 F	 F	 F	 F	P !'vdCCC>##g-~
 
 '))/tUD)) %,..$6 	KKBdG)8y    W	K))++%q !## ' \ $T^5E5EX_H`5`  
 &&t,,, %%a%00055-FGG  
 >##h..' @ $W  
 &&t,,, %%a%000"K"K"K"KK55!-22 [99@@BBB#]]+]>>   &o7QRRR
 $ 0(8(8?(Jd(R(R#8#:#:d#BLKK8 dK((?7+CC&((%1@y0<Y\Y,,WY	 	 	 '+n&6&6O#/L 
;'''$$"%kC&7"="=KA ))++%B ":. 	 	 	   g$N$$w'??	  ""4(((!!!!,,,,   )	2 	!$$$	K 	 	 	D	  	KKDdK  ?7#;;   t%;TBB#a(((==??++FHdoNNt%;TBB 	Kl1o&&C$'F=!'*JC*J*J*JJF8s    ((B$D 1DD 
DD DD D.-D.<B>R0 ;C
R0 C)R0 0AT?+T-,T?-
T:7T?9T::T?U3 3
V ?V 	collectorr"   r  r   c                 Z    ||d}|                                  }|r| j        |d<   ||d<   |S )zHAssemble a wait result, attaching spill metadata when overflow occurred.)r   r   output_total_charsfull_output_path)rN   rS   )r  r  r   r]  spills        r   r  z%BaseEnvironment._finalize_wait_result   sH     %J??%%'' 	/+4+@F'().F%&r   c                 l    	 |                                  dS # t          t          t          f$ r Y dS w xY w)zDTerminate a process. Subclasses may override for process-group kill.N)r   ProcessLookupErrorPermissionErrorrF   )r   r   s     r   r  zBaseEnvironment._kill_process+  sA    	IIKKKKK"OW= 	 	 	DD	s    33r]  c                 0    |                      |           dS )zDExtract CWD from command output. Override for local file-based read.N)_extract_cwd_from_output)r   r]  s     r   rS  zBaseEnvironment._update_cwd6  s    %%f-----r   c                 L   |                     dd          }| j        }|                    |          }|dk    rdS t          d|dz
            }|                    |||          }|dk    s||k    rdS ||t	          |          z   |                                         }|r|| _        d|d<   |                    d	d|          }|dk    r|}|                    d	|t	          |          z             }	|	dk    r|	d
z   nt	          |          }	|d|         ||	d         z   |d<   dS )a  Parse the __HERMES_CWD_{session}__ marker from stdout output.

        Updates self.cwd and strips the marker from result["output"].
        Used by remote backends (Docker, SSH, Modal, Daytona, Singularity).

        Sets ``result["cwd_observed"]`` when the marker yielded a directory for
        THIS command. The wrapper prints the marker after the command returns,
        so a killed / timed-out command never emits one and ``self.cwd`` keeps
        whatever the previous command left there. That environment is shared by
        every session, so callers must not attribute an unobserved cwd to the
        session that ran this command (see terminal_tool's session-cwd record).
        r   r   Nr   r  Tcwd_observedrn  r&   )r   r  rfindr(   rD   rU  r   find)
r   r]  r   rs  lastsearch_startr_   cwd_path
line_startline_ends
             r   r  z(BaseEnvironment._extract_cwd_from_output:  sD    Hb))!||F##2::F 1dTk**V\488B;;%4--F%#f++-45;;== 	*DH%)F>" \\$511
J;;tTCKK%788#+r>>8a<<s6{{!+:+.		1BBxr   c                     dS )u>  Hook called before each command execution.

        Remote backends (SSH, Modal, Daytona) override this to trigger
        their FileSyncManager.  Bind-mount backends (Docker, Singularity)
        and Local don't need file sync — the host filesystem is directly
        visible inside the container/process.
        Nrs   rM   s    r   _before_executezBaseEnvironment._before_executeh  s	     	r   r   T)r   r   rewrite_compound_backgroundr|  r  c                   |                                   |                     |          \  }}|rddlm}	  |	|          }|p| j        }
|p| j        }||||z   }n||}n|}|r#| j        dk    r|                     ||          }d}|                     ||          }| j	         o| j
         }|                     |||
|          }|                     ||
|          }|                     |           |S )u  Execute a command, return {"output": str, "returncode": int}.

        ``bounded_capture=True`` caps stdout/stderr retention at
        ``tool_output.max_bytes`` WHILE the stream is drained (head/tail
        window) instead of holding the full output in memory (#64435).
        It must only be set by callers whose output is destined for the
        model/tool payload (the foreground terminal tool). Internal
        full-fidelity consumers — file operations ``cat`` reads that feed
        the patch engine, code-execution RPC reads, log reads — MUST leave
        it False: truncating those corrupts data, not just display.
        r   )_rewrite_compound_backgroundNheredocr1  )r   r|  )r  _prepare_commandtools.terminal_toolr  r   r   r  r{  rw  r,  r.  r7  rR  rS  )r   rh  r   r   r   r  r|  exec_command
sudo_stdinr  effective_timeouteffective_cwdeffective_stdinwrappedr2  r   r]  s                    r   executezBaseEnvironment.executev  s]   * 	#'#8#8#A#A j ' 	FHHHHHH77EEL#3t|tx !j&<(:5OO#(OO(O  	#t/9<<44\?SSL"O$$\=AA ((F1F-F~~5*;  
 
 ''+_ ( 
 
 	   r   c                 .    |                                   dS )z.Alias for cleanup (compat with older callers).N)r9  rM   s    r   stopzBaseEnvironment.stop  s    r   c                 R    	 |                                   d S # t          $ r Y d S w xY wrP   )r9  r   rM   s    r   __del__zBaseEnvironment.__del__  s:    	LLNNNNN 	 	 	DD	s    
&&c                 $    ddl m}  ||          S )z6Transform sudo commands if SUDO_PASSWORD is available.r   )_transform_sudo_command)r  r  )r   rh  r  s      r   r  z BaseEnvironment._prepare_command  s%    ??????&&w///r   rP   )r0  r   )r   )&r   r   r   r   r  r   __annotations__r  r)   r  boolr  dictr   r   r7  r   r9  r	   r;  r   rI  rc  staticmethodrP  rQ  rw  r{  rR  r  r  rS  r  r  r  r  r  r  rs   r   r   r  r  S  s          K  s
 ).---c    & &C &# &D & & & &2 !%W W WW 	W
 W $JW 
W W W W   ^hsm    ?eCHo ? ? ? ?<y y y~  s  s       \ !c !c ! ! ! !o S o s o s o  o  o  o j Hc Hs Hs H H H \H 36NRWN N N!N,/NKON	N N N N`
 )B (+9<tHL   \-    .$ . . . .(Ct (C (C (C (C\   " <
 #!%,0 %< < << <
 t< $J< &*< < 
< < < <D    0 0c3:o0F 0 0 0 0 0 0r   r  rP   )rs   )Dr   r  r   loggingr   rer  r  r   r1   r   r%  abcr   r   collectionsr   pathlibr   typingr   r   r	   r
   hermes_constantsr   hermes_cli._subprocess_compatr   tools.interruptr   	getLoggerr   rF  r  r   r  setLevelINFOlocalrv   r  r   r   r"   r   rx   r{   r  r   r   r   r   listr   r   r   r   r   r)   r   r   r   r  _SNAPSHOT_EXCLUDED_ENV_REGEXcompiler@  r  r  rs   r   r   <module>r     s       				 				             # # # # # # # #             3 3 3 3 3 3 3 3 3 3 3 3 , , , , , , < < < < < < * * * * * *		8	$	$ 4		":;;<<  "
 OOGL!!! +9?,,  % 
 
 
 
 
 
 
 
4\S \S \S \S \S \S \S \S~+hud{3d: +t + + + +
?xt4t; ? ? ? ? 
   4    (;j& ;c ;d ; ; ; ;~ .2 	c #d
   .4 D    B4 Bt B B B B Bs uUCZ'84'?    , , , , ,H , , ,$B  B  B  B  B  B  B  B T*C *C * * * *. g   RZ ;<< 
 %'2 22SM2 	2 2 2 2ts0 s0 s0 s0 s0c s0 s0 s0 s0 s0r   