
    epj[                     >   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          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Z.defdZ/dej0        de*ddfdZ1	 d/de2e*         de*dz  dej0        fdZ3dede-fdZ4dede-ddfdZ5de*de6e7e8f         dz  fd Z9 G d! d"e          Z: G d# d$          Z;d%e*de*fd&Z<d'Z= ej>        d(          Z?	 d0d*e*d+ee*         de*fd,Z@ G d- d.e          ZAdS )1aC  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                       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)selfr   r   s      =/home/thesage/.hermes/hermes-agent/tools/environments/base.py__init__z _BoundedOutputCollector.__init__D   s    QI//t~344>D,<< "
!&
^%%
%)-"    textreturnc                 
   | j         | j        rdS 	 | j        | j         j                            dd           t          | j         d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).NTparentsexist_okwutf-8replaceencodingerrors r   z+
... [spill capped at 5,000,000 chars] ...
)r"   r%   r#   parentmkdiropenjoinr   r   writelenr$   _SPILL_CAP_CHARSr   OSError)r&   r*   backlogbudgets       r'   _maybe_spillz$_BoundedOutputCollector._maybe_spillS   s   #t'9#F	&~% '--dT-JJJ!%d&6gV_!`!`!` ''$*--
0C0CC$$W---$'LL!*T->>F{{c$ii&00$$T/3q&>>/%:;;;$$%TUUU%)""$$T***T* 	& 	& 	&!%D	&s   EE- -FF
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)r!   r#   closer>   strr"   r&   s    r'   close_spillz#_BoundedOutputCollector.close_spilll   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)r!   r   r   rF   s    r'   buffered_charsz&_BoundedOutputCollector.buffered_charsx   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 rI   )r!   r   rF   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   )r!   r<   r"   r#   r   r   rA   r   r   minr   appendr   r   clearr   popleft)	r&   r*   text_lenstarttake	remainingchunkexcessfirsts	            r'   rO   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r6   suffixrZ   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.Nr6      r   r   z

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

)
r!   r<   r   r:   r   r   r   ranger   r   )r&   rZ   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rI   )r+   rB   )__name__
__module____qualname____doc__r=   r   r(   rE   rA   rG   propertyrJ   rL   rO   rj    r)   r'   r   r   7   s/         !# ## #= # # # #& & & & & &2
) 
) 
) 
) 7 7 7 7 X7 %S % % % X%*/3 */4 */ */ */ */X ') S S S SS S S S S S Sr)   r   cbr+   c                     | t           _        dS )z>Register a callback that _wait_for_process fires periodically.N)_activity_callback_localcallback)rq   s    r'   set_activity_callbackru      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).
    rt   N)getattrrs   rp   r)   r'   get_activity_callbackrx      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_touchNrS   z (z
s elapsed))time	monotonicgetrx   r   	Exception)ry   rz   nowr|   rq   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	sandboxesTr-   )osgetenvr   r   r8   )customps     r'   get_sandbox_dirr      sT     Y-..F ,LL+GGD4G(((Hr)   procdatac                 f      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.
    c                     	 t          t                    r                    d          n} t          j        dj                  }|                    |            |                                 d S # t          t          f$ r Y d S w xY w)Nr1   buffer)	
isinstancerE   encoderw   stdinr;   rD   BrokenPipeErrorr>   )rawtargetr   r   s     r'   _writez_pipe_stdin.<locals>._write  s    	 +5T3*?*?I$++g&&&TCTZ4:>>FLLLLNNNNN) 	 	 	DD	s   A0A5 5B
	B
Tr   daemonN)r   ThreadrS   )r   r   r   s   `` r'   _pipe_stdinr     sL    "      F40006688888r)   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.
    creationflagsNTr1   r2   )stdoutstderrr   r*   r4   r5   )
setdefaultr   
subprocessPopenPIPESTDOUTDEVNULLr   )r   r   kwargsr   s       r'   _popen_bashr   /  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.r1   r4   )existsjsonloads	read_textr   )r   s    r'   _load_json_storer   F  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-      )indentr1   r   N)r7   r8   
write_textr   dumps)r   r   s     r'   _save_json_storer   P  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_sizer>   )r   sts     r'   _file_mtime_keyr   V  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.
    r+   Nc                     d S rI   rp   rF   s    r'   pollzProcessHandle.pollk        r)   c                     d S rI   rp   rF   s    r'   killzProcessHandle.killl  r   r)   timeoutc                     d S rI   rp   r&   r   s     r'   waitzProcessHandle.waitm  r   r)   c                     d S rI   rp   rF   s    r'   r   zProcessHandle.stdouto      (+r)   c                     d S rI   rp   rF   s    r'   
returncodezProcessHandle.returncoder  r   r)   r+   NrI   )rk   rl   rm   rn   r   r   r   floatr   ro   r   rE   r   r   rp   r)   r'   r   r   d  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rr1   r2   r3   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)Nr1   r2   r5   r   )_returncoder   r;   	_write_fdr   r>   r   _errorrD   _doneset)output	exit_codeexcr   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_fnr   Eventr   r   r   r   pipefdopen_stdoutr   r   rS   )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 rI   )r   rF   s    r'   r   z_ThreadedProcessHandle.stdout  s
    |r)   r+   c                     | j         S rI   )r   rF   s    r'   r   z!_ThreadedProcessHandle.returncode  s    r)   c                 F    | j                                         r| j        nd S rI   )r   is_setr   rF   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 rI   )r   r   rF   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)   rI   )rk   rl   rm   rn   r   tuplerE   r   r(   ro   r   r   r   r   r   r   rp   r)   r'   r   r   v  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___rp   )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_]*$rp   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 rp   )r   rE   .0names     r'   	<setcomp>z6_export_dump_excluding_session_vars.<locals>.<setcomp>  s@       dC  %)  r)    c              3   >   K   | ]}t          j        |          V  d S rI   shlexquoter   s     r'   	<genexpr>z6_export_dump_excluding_session_vars.<locals>.<genexpr>  s,      LL5;t,,LLLLLLr)   zR{ ( unset ${!HERMES_SESSION_*} ${!HERMES_CRON_AUTO_DELIVER_*} HERMES_UI_SESSION_IDz( 2>/dev/null; export -p; ) || true; } > )r:   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_passthroughr+   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/tmprp   rF   s    r'   get_temp_dirzBaseEnvironment.get_temp_dir"  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__+  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_stringr#  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typerk   )r&   r$  r#  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).Nrp   rF   s    r'   cleanupzBaseEnvironment.cleanupO  s	     	r)   c                     dS )zAReturn backend-specific names that must not persist in snapshots.rp   rp   rF   s    r'   ,_additional_profile_scoped_passthrough_namesz<BaseEnvironment._additional_profile_scoped_passthrough_namesX  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.
        rp   r   )is_multiplex_active)get_all_passthroughc              3   x   K   | ]5}t          |t                    t                              |          1|V  6d S rI   )r   rE   _SHELL_ENV_NAME_RE	fullmatchr   s     r'   r  zGBaseEnvironment._snapshot_excluded_passthrough_names.<locals>.<genexpr>n  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)r#  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  r:  r  r   r(  r  _wait_for_processr   r   RuntimeErrorr  _update_cwdr7  infor  r   rE   rN   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_sessionz  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'   rA  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'   rB  z!BaseEnvironment._quote_shell_path  s     {4   r)   commandc                 V   |                     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           | 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'\''r<  r=  _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   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 '\nr>  z\n' "$(pwd -P)"zexit $__hermes_ec
)
r2   rB  r  r:  rO   r  rA  r  r   r:   )r&   rZ  r  escapedrJ  rK  rL  partspassthrough_namessaved_namesr   markerpresentvalue
quoted_cwds                  r'   _wrap_commandzBaseEnvironment._wrap_command  s+    //#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 ++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'
r`  )r  r  r  )rZ  r   	delimiters      r'   _embed_stdin_heredocz$BaseEnvironment._embed_stdin_heredocq  sG     <DJLL$4SbS$9;;	GG	GGjGGIGGGr)   )bounded_capturer   rn  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-                   | "                    #                                j-                  S )2u  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   r1   r2   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   bytesrO   decoderE   r   )streampiecer`   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)   ru  g?r      )r   rw   callabler   r   r   r   r   readrO   rx  
ValueErrorr>   selectr   )ry  r~  fdrV   r`   idle_after_exitreadyrc   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}   rS   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 grouprS   r   r   z
[Command interrupted]rY      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)r6   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=%s).tools.tool_output_limitsrp  r   _UNBOUNDED_CAPTURE_CHARSr   r   r~   r   getpididis_dirglobr   r   unlinkr>   r   codecsgetincrementaldecoderr   r   rS   r   current_threadidentrw   rx   _DEBUG_INTERRUPTr7  rF  r   r   _kill_processr:   _finalize_wait_resultrj   rL   lstripr   sleeprN   KeyboardInterrupt
SystemExitr   rD   r   )r&   r   r   rn  rp  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_noner|  r{  r   s    `                      @@@r'   rC  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#;;   ))&&--//4?SSSs    ((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   renderedr   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)rG   rL   )r  r  r   rO  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PermissionErrorr>   )r&   r   s     r'   r  zBaseEnvironment._kill_process  sA    	IIKKKKK"OW= 	 	 	DD	s    33rO  c                 0    |                      |           dS )zDExtract CWD from command output. Override for local file-based read.N)_extract_cwd_from_output)r&   rO  s     r'   rE  zBaseEnvironment._update_cwd  s    %%f-----r)   c                 B   |                     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k    r|}|                    d|t	          |          z             }	|	dk    r|	dz   nt	          |          }	|d|         ||	d         z   |d<   dS )	zParse 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).
        r   r6   Nr   r  r`  r   )r   r   rfindr   r<   rG  r  find)
r&   rO  r   re  lastsearch_startrX   cwd_path
line_startline_ends
             r'   r  z(BaseEnvironment._extract_cwd_from_output  s:    Hb))!||F##2::F 1dTk**V\488B;;%4--F%#f++-45;;== 	 DH \\$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.
        Nrp   rF   s    r'   _before_executezBaseEnvironment._before_execute  s	     	r)   r6   T)r   r   rewrite_compound_backgroundrn  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heredocr"  )r   rn  )r  _prepare_commandtools.terminal_toolr  r   r  r  rm  ri  r  r  r(  rC  rE  )r&   rZ  r  r   r   r  rn  exec_command
sudo_stdinr  effective_timeouteffective_cwdeffective_stdinwrappedr#  r   rO  s                    r'   executezBaseEnvironment.execute
  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)r*  rF   s    r'   stopzBaseEnvironment.stopL  s    r)   c                 R    	 |                                   d S # t          $ r Y d S w xY wrI   )r*  r   rF   s    r'   __del__zBaseEnvironment.__del__P  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&   rZ  r  s      r'   r  z BaseEnvironment._prepare_commandV  s%    ??????&&w///r)   rI   )r!  r   )r6   )&rk   rl   rm   rn   r  rE   __annotations__r  r   r  boolr  dictr(   r   r(  r   r*  r	   r,  r   r:  rU  staticmethodrA  rB  ri  rm  rC  r  r  rE  r  r  r  r  r  r  rp   r)   r'   r
  r
    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 ! ! ! !^ S ^ s ^ s ^  ^  ^  ^ H Hc Hs Hs H H H \H 36TRWT T T!T,/TKOT	T T T TB
 )B (+9<tHL   \-    .$ . . . . Ct  C  C  C  CL   " <
 #!%,0 %< < << <
 t< $J< &*< < 
< < < <D    0 0c3:o0F 0 0 0 0 0 0r)   r
  rI   )rp   )Brn   r  r   loggingr   rer  r  r   r   r~   r  abcr   r   collectionsr   pathlibr   typingr   r   r	   r
   hermes_constantsr   hermes_cli._subprocess_compatr   tools.interruptr   	getLoggerrk   r7  r  r   r  setLevelINFOlocalrs   r  r   rE   ru   rx   r  r   r   r   r   listr   r   r   r   r   r   r   r   r   r   _SNAPSHOT_EXCLUDED_ENV_REGEXcompiler1  r  r
  rp   r)   r'   <module>r     s`       				 				             # # # # # # # #             3 3 3 3 3 3 3 3 3 3 3 3 , , , , , , < < < < < < * * * * * *		8	$	$ 4		":;;<<  "
 OOGL!!! +9?,,  % US US US US US US US USp+hud{3d: +t + + + +
?xt4t; ? ? ? ? 
   4    (9j& 9c 9d 9 9 9 9F .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 ;<< 
 %'+ ++SM+ 	+ + + +fK0 K0 K0 K0 K0c K0 K0 K0 K0 K0r)   