
    ,j                    X   U 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 ddlmZmZmZmZ ddlmZ  ej        e          ZddlmZmZ ddlmZ ddlmZm Z m!Z!m"Z"m#Z# d	e$d
ede$fdZ% e%dde&d          Z' e%dde(d          Z)d Z*i Z+e,e$e$f         e-d<    e	j.                    Z/ e	j0                    Z1d Z2d Z3d Z4d Z5de$fdZ6de$fdZ7de$ddfdZ8ddZ9ddl:m;Z< d e$d!e$de,fd"Z= ej>        d#          Z?d$e$de$dz  fd%Z@d&e$d!e$de$fd'ZAdd)e&de$fd*ZBdd ed,e&de$fd-ZCd.e$deDfd/ZEd e$d0e&deFe$e&f         fd1ZGd e$deFe$eDf         fd2ZHdeDfd3ZId e$de$fd4ZJd e$dz  deFe$dz  e$dz  f         fd5ZKdd6lLmMZN dd7lmOZP dd8lQmRZS dd9lTmUZV dd:lWmXZY dd;lZm[Z\ dd<l]m^Z^ ddl_Z_d=Z`i Zaee$ef         e-d><   i Zbee$e(f         e-d?<    e	j.                    Zci Zdee$e	j.        f         e-d@<    e	j.                    ZedafdAagdAah e	j.                    ZidBee$ef         ddfdCZji Zkee$ee$ef         f         e-dD<   dEe$dFee$ef         fdGZldEe$fdHZmdEee$         de$fdIZne&dfd	e$d
e$dJede$fdKZode$fdLZpdee$ef         fdMZqdNerdz  dee$ef         fdOZs	 	 	 	 dd!e$dPe$dQe$dRe&dSe,dBe,dTe,dEe$dUe$fdVZtddXe&fdYZudZ Zvd[ Zwd\ ZxdEe$fd]ZydEe$deDfd^Zzd_ Z{dAd`dEe$daeDfdbZ|dc Z} e
j~        e}           d e$dde&de$dz  fdeZd e$deDfdfZ ej>        dgej        ej        z            Z ej>        dh          Z ej>        di          Zd e$de$fdjZ ej>        dkej                   ej>        dlej                   ej>        dmej                   ej>        dnej                   ej>        doej                   ej>        dpej                   ej>        dqej                   ej>        drej                  fZd e$deDfdsZd e$de$dz  fdtZdueDdveDdeFfdwZd$ee$         dxedye$de$fdzZ	 	 	 	 	 	 	 	 dd e$dveDdRee&         dEee$         d{eDd$ee$         d|eDdueDd}eee$                  de$fd~ZdeDfdZedk    r\ ed            ed            eq            Z ed            eded!                      eded                      eded                      ededQ                      ededR          d            ededX          d            e            s ed            e_j        d            ed            ed            ed            ed            ed            ed            ed            ed            ed            ed           dZ ed ej        dd           d            ed ej        de                       ed ej        dde                        ed ej        de                       ed ej        de                       ed ej        d ep                                  ddlmZ  ed ej        d e             d                       ed ej        dd                       ed ej        dd                      ddlmZ de`dddddddAddde' de' dddddddddAddddAddddiddƜdǜd gdȜdɜZdʄ Z ej~        ddeeedd̬ͦ           dS )a7  
Terminal Tool Module

A terminal tool that executes commands in local, Docker, Modal, SSH,
Singularity, and Daytona environments. Supports local execution,
containerized backends, and cloud sandboxes, including managed Modal mode.

Supported environments:
- "local": Execute directly on the host machine (default, fastest)
- "docker": Execute in Docker containers (isolated, requires Docker)
- "modal": Execute in Modal cloud sandboxes (direct Modal or managed gateway)

Features:
- Multiple execution backends (local, docker, modal)
- Background task support
- VM/container lifecycle management
- Automatic cleanup after inactivity

Cloud sandbox note:
- Persistent filesystems preserve working state across sandbox recreation
- Persistent filesystems do NOT guarantee the same live sandbox or long-running processes survive cleanup, idle reaping, or Hermes exit

Usage:
    from terminal_tool import terminal_tool

    # Execute a simple command
    result = terminal_tool("ls -la")

    # Execute in background
    result = terminal_tool("python server.py", background=True)
    N)Path)OptionalDictAnyList)env_var_enabled)is_interrupted_interrupt_event)_get_scratch_dir)coerce_modal_modehas_direct_modal_credentialsmanaged_nous_tools_enabled%nous_tool_gateway_unavailable_messageresolve_modal_backend_statenamedefault
type_labelc                     t          j        |           }||dk    r|S 	  ||          S # t          t          f$ r# t                              d| |||           |cY S w xY w)zParse module-level numeric env vars without breaking import.

    Terminal tool is imported by CLI, ACP, tests, and tool discovery. A single
    malformed env var must not make the whole module unloadable at import time.
    N z;Invalid value for %s: %r (expected %s). Falling back to %r.)osgetenv	TypeError
ValueErrorloggerwarningr   r   	converterr   raws        8/home/ubuntu/.hermes/hermes-agent/tools/terminal_tool.py_safe_parse_import_envr    O   s     )D//C
{cRii
y~~z"   I	
 	
 	
 s   
+ 1AATERMINAL_MAX_FOREGROUND_TIMEOUTiX  integerTERMINAL_DISK_WARNING_GBg     @@numberc                  d   	 t                      } d}ddl}|                    t          | dz                      D ]}t          |                              d          D ]g}|                                rQ	 ||                                j        z  }4# t          $ r&}t          
                    d||           Y d}~_d}~ww xY wh|dz  }|t          k    r#t                              d|t                     dS d	S # t          $ r(}t          
                    d
|d           Y d}~d	S d}~ww xY w)z4Check if total disk usage exceeds warning threshold.r   Nhermes-**zCould not stat file %s: %si   @z\Disk usage (%.1fGB) exceeds threshold (%.0fGB). Consider running cleanup_all_environments().TFz#Disk usage warning check failed: %sexc_info)r   globstrr   rglobis_filestatst_sizeOSErrorr   debugDISK_USAGE_WARNING_THRESHOLD_GBr   	Exception)scratch_dirtotal_bytesr*   pathfetotal_gbs          r   _check_disk_usage_warningr:   {   s}   &(( IIc+
":;;<< 	I 	ID$ZZ%%c** I I99;; II#qvvxx'77" I I I%A1aHHHHHHHHIII ),555NNy#%DF F F4u   :AMMMuuuuusB   A3C= 6BC= 
CB>9C= >C6C= =
D/D**D/_sudo_password_cachec                  .    t          t          dd           S )Nsudo_passwordgetattr_callback_tls     r   _get_sudo_password_callbackrC      s    =/4888rB   c                  .    t          t          dd           S )Napprovalr>   rA   rB   r   _get_approval_callbackrF      s    =*d333rB   c                     | t           _        dS )u   Register a callback for sudo password prompts (used by CLI).

    Per-thread scope — ACP sessions that run concurrently in a
    ThreadPoolExecutor each have their own callback slot.
    N)r@   r=   cbs    r   set_sudo_password_callbackrJ      s     #%MrB   c                     | t           _        dS )u   Register a callback for dangerous command approval prompts.

    Per-thread scope — ACP sessions that run concurrently in a
    ThreadPoolExecutor each have their own callback slot. See
    GHSA-qg5c-hvr5-hjgr.
    N)r@   rE   rH   s    r   set_approval_callbackrL      s      MrB   returnc                     	 ddl m}   | dd          }n%# t          $ r t          j        dd          }Y nw xY w|rd| S t                      }|Zt          |dd          }t          |dd          }|$|"d	t          |           d
t          |           S dt          |           S dt          j	                     S )z6Return the cache scope for interactive sudo passwords.r   get_session_envHERMES_SESSION_KEYr   zsession:N__self____func__zcallback-owner::z	callback:zthread:)
gateway.session_contextrP   r3   r   r   rC   r?   id	threading	get_ident)rP   session_keycallbackownerfuncs        r   _get_sudo_password_cache_scoper]      s    :;;;;;;%o&:B?? : : :i 4b99: ('+'''*,,H*d33xT22!1;RYY;;D;;;)2h<<))),Y(**,,,s    77c                      t                      } t          5  t                              | d          cddd           S # 1 swxY w Y   dS )z6Return the cached sudo password for the current scope.r   N)r]   _sudo_password_cache_lockr;   get)scopes    r   _get_cached_sudo_passwordrb      s    *,,E	" 3 3#''r223 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3s   >AApasswordc                     t                      }t          5  | r| t          |<   nt                              |d           ddd           dS # 1 swxY w Y   dS )z.Persist a sudo password for the current scope.N)r]   r_   r;   pop)rc   ra   s     r   _set_cached_sudo_passwordrf      s    *,,E	" 2 2 	2*2 '' $$UD111	2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2s   )AAAc                  x    t           5  t                                           ddd           dS # 1 swxY w Y   dS )z`Clear all cached sudo passwords.

    Internal helper for tests and process teardown paths.
    N)r_   r;   clearrA   rB   r   _reset_cached_sudo_passwordsri      s    
 
# % %""$$$% % % % % % % % % % % % % % % % % %s   /33)check_all_command_guardscommandenv_typec                 >    t          | |t                                S )zJDelegate to consolidated guard (tirith + dangerous cmd) with CLI callback.)approval_callback)_check_all_guards_implrF   )rk   rl   s     r   _check_all_guardsrp     s*    !'84J4L4LN N N NrB   z^[A-Za-z0-9/\\:_\-.~ +@=,]+$workdirc                     | sdS t                               |           s6| D ]1}t                               |          sdt          |           dc S 2dS dS )zReject workdir values that don't look like a filesystem path.

    Uses an allowlist of safe characters rather than a deny-list, so novel
    shell metacharacters can't slip through.

    Returns None if safe, or an error message string if dangerous.
    Nz/Blocked: workdir contains disallowed character z<. Use a simple filesystem path without shell metacharacters.z0Blocked: workdir contains disallowed characters.)_WORKDIR_SAFE_REmatchrepr)rq   chs     r   _validate_workdirrw     s      t!!'** B 	 	B#))"-- Qd2hh Q Q Q  
 BA4rB   outputc                 x    t          d          }|s| S g d}|D ]}|| v rddlm} | d |             dz   c S  | S )z
    Check for sudo failure and add helpful message for messaging contexts.
    
    Returns enhanced output if sudo failed in messaging context, else original.
    HERMES_GATEWAY_SESSION)zsudo: a password is requiredzsudo: no tty presentzsudo: a terminal is requiredr   display_hermes_homeu@   

💡 Tip: To enable sudo over messaging, add SUDO_PASSWORD to z/.env on the agent machine.)r   hermes_constantsr|   )rx   rl   
is_gatewaysudo_failuresfailure_dhhs         r   _handle_sudo_failurer   '  s     !!9::J   M ! E EfDDDDDD  Eaeaeagag  E  E  E  E  E  E  E  MrB   -   timeout_secondsc                 F   ddl }t                      }|	  |            pdS # t          $ r Y dS w xY wdddfd}	 dt          j        d<   t          j        d	           t                       t          d
           t          d           t          d           t          d           t          d           t          d|  ddz   dz              t          d           t                       t          ddd           t          j	        |d          }|
                                 |                    |            d         r~d         pd}t                       |rt          d           nt          d           t                       |j                                         |dt          j        v rt          j        d= S S t          d           t          d           t                       |j                                         	 dt          j        v rt          j        d= dS dS # t          t          f$ re t                       t          d           t                       |j                                         Y dt          j        v rt          j        d= dS dS t          $ rT}t          d | d!           |j                                         Y d}~dt          j        v rt          j        d= dS dS d}~ww xY w# dt          j        v rt          j        d= w xY w)"a  
    Prompt user for sudo password with timeout.
    
    Returns the password if entered, or empty string if:
    - User presses Enter without input (skip)
    - Timeout expires (45s default)
    - Any error occurs
    
    Only works in interactive mode (HERMES_INTERACTIVE=1).
    If a _sudo_password_callback is registered (by the CLI), delegates to it
    so the prompt integrates with prompt_toolkit's UI.  Otherwise reads
    directly from /dev/tty with echo disabled.
    r   Nr   F)rc   donec                     d} d}	 t          j                    dk    r\ddl}g }	 |                                }|dv rn#|dk    rt          |                    |           <d                    |          
d<   nddl}t          j	        d	t          j
                  } |                    |           }|                    |           }|d
         |j         z  |d
<   |                    | |j        |           g }	 t          j        | d          }|r|dv rn|                    |           2d                    |                              dd          
d<   n2# t"          t          t$          f$ r d
d<   Y nt&          $ r d
d<   Y nw xY w| V|T	 ddl}|                    | |j        |           n2# t&          $ r%}	t(                              d|	           Y d}	~	nd}	~	ww xY w| H	 t          j        |            n2# t&          $ r%}	t(                              d|	           Y d}	~	nd}	~	ww xY wd
d<   dS # | V|T	 ddl}|                    | |j        |           n2# t&          $ r%}	t(                              d|	           Y d}	~	nd}	~	ww xY w| H	 t          j        |            n2# t&          $ r%}	t(                              d|	           Y d}	~	nd}	~	ww xY wd
d<   w xY w)zKRead password with echo disabled. Uses msvcrt on Windows, /dev/tty on Unix.NWindowsr   T>   
r   rc   z/dev/tty      >      
   rB   zutf-8replace)errorsz)Failed to restore terminal attributes: %szFailed to close tty fd: %sr   )platformsystemmsvcrtgetwchKeyboardInterruptappendjointermiosr   openO_RDONLY	tcgetattrECHO	tcsetattr	TCSAFLUSHreaddecodeEOFErrorr0   r3   r   r1   close)tty_fd	old_attrsr   charscr   	new_attrsb_termiosr8   results             r   read_password_threadz7_prompt_for_sudo_password.<locals>.read_password_thread[  s   	*	"  I--$AL((F{{//LLOOO$ &(WWU^^z""R[99#--f55	#--f55	(|w|m;	!!!&'*;YGGG$**A ^ 3 3LLOOO	$
 &)XXe__%;%;GI%;%V%Vz"+W5 	$ 	$ 	$!#F: 	$ 	$ 	$!#F:	$ !i&;Q....&&vx/A9MMMM  Q Q QLL!LaPPPPPPPPQ!BHV$$$$  B B BLL!=qAAAAAAAAB!F6NNN !i&;Q....&&vx/A9MMMM  Q Q QLL!LaPPPPPPPPQ!BHV$$$$  B B BLL!=qAAAAAAAAB!F6N!!!!s   EE H5 F	6H5 8F	H5 F		H5  F2 2
G!<GG!'G< <
H+H&&H+5K; IK
J&JKJKJ&%K&
K0KKK	K1HERMES_SPINNER_PAUSEg?u   ┌──────────────────────────────────────────────────────────┐uA   │  🔐 SUDO PASSWORD REQUIRED                              │u   ├──────────────────────────────────────────────────────────┤u?   │  Enter password below (input is hidden), or:            │uA   │    • Press Enter to skip (command fails gracefully)     │u   │    • Wait zs to auto-skipz                           u   │u   └──────────────────────────────────────────────────────────┘z  Password (hidden): T)endflushtargetdaemontimeoutr   rc   u1     ✓ Password received (cached for this session)u'     ⏭ Skipped - continuing without sudou(   
  ⏱ Timeout - continuing without sudoz    (Press Enter to dismiss)u)     ⏭ Cancelled - continuing without sudoz
  [sudo prompt error: z] - continuing without sudo
)sysrC   r3   r   environtimesleepprintrW   Threadstartr   stdoutr   r   r   )r   r   _sudo_cbr   password_threadrc   r8   r   s          @r   _prompt_for_sudo_passwordr   A  s    JJJ +,,H	8::## 	 	 	22	 ..F." ." ." ." ."`03-0
)*
3()))CDDD()))OPPPQRRR@@@@8KeSTTT()))%2T::::#*2FtTTT_555&> 	j)/RHGGG AIJJJJ?@@@GGGJ& "RZ//
122 0# =>>>0111GGGJ "RZ//
1222 0/ '(   9:::
 "RZ//
1222 0/    IIIIJJJ
rrr!RZ//
1222 0/
 "RZ//
122222sJ   # 
11E-H. 	AH. .AL L #	L ,,K;L ;L  L L    limitc                     | dS t          | t                    r
| d|         S 	 t          |           d|         S # t          $ r dt	          |           j         dcY S w xY w)z>Return a log-safe preview for possibly-invalid command values.Nz<None><>)
isinstancer+   ru   r3   type__name__)rk   r   s     r   _safe_command_previewr     s    x'3 vv-G}}VeV$$ - - -,4==),,,,,,-s   < "A! A!tokenc                     d| vs|                      d          rdS |                     dd          \  }}t          t          j        d|                    S )zCReturn True when *token* is a leading shell environment assignment.=Fr   z^[A-Za-z_][A-Za-z0-9_]*$)
startswithsplitboolrert   )r   r   _values      r   _looks_like_env_assignmentr     sW    
%5++C00u;;sA&&LD&4d;;<<<rB   r   c                    |}t          |           }||k     r| |         }|                                s|dv rn|dk    r:|dz  }||k     r#| |         dk    r|dz  }||k     r| |         dk    ||k     r|dz  }g|dk    r@|dz  }||k     r4| |         }|dk    r|dz   |k     r|dz  }#|dk    r|dz  }n|dz  }||k     4|dk    r|dz   |k     r|dz  }|dz  }||k     | ||         |fS )zERead one shell token, preserving quotes/escapes, starting at *start*.z;|&()'r   "\   )lenisspace)rk   r   inrv   inners         r   _read_shell_tokenr     s]   AGA
a%%QZ::<< 	2==99FAa%%GAJ#--Q a%%GAJ#--1uuQ99FAa%%
D==QUQYYFAC<<FAQ a%% ::!a%!))FA	Q5 a%%8 57QrB   c                    g }d}t          |           }d}d}||k     r| |         }|                                r#|                    |           |dk    rd}|dz  }F|dk    r]|r[|                     d|          }|dk    r|                    | |d                    n8|                    | ||                    |}|                     d	|          s,|                     d
|          s|                     d|          r)|                    | ||dz                       |dz  }d}|dv r|                    |           |dz  }d}6|dk    r|                    |           |dz  }d}Zt          | |          \  }}	|r|dk    r|                    d           d}n|                    |           |rt          |          rd}nd}|	}||k     d                    |          |fS )zGRewrite only real unquoted sudo command words, not plain text mentions.r   TFr   r   #N&&||z;;r   z;|&()sudozsudo -S -p ''r   )r   r   r   findr   r   r   r   )
rk   outr   r   command_startfoundrv   comment_endr   next_is
             r   _rewrite_real_sudo_invocationsr     s<   C	AGAME
a%%QZ::<< 	JJrNNNTzz $FA999!,,tQ//Kb  

7122;'''JJwq}-...AdA&& 	'*<*<T1*E*E 	I[I[\`bcIdId 	JJwqQw'(((FA M<<JJrNNNFA M99JJrNNNFA!M)'155v 	Uf__JJ'''EEJJu 	"7>> 	" MM!Ma a%%d 773<<rB   c                  6   t          j        dd                                                                          pd} | dk    rdS 	 t	          j        g dt          j        t          j        t          j        dd          }|j        dk    S # t          $ r Y dS w xY w)aN  Return True when local sudo currently works without prompting.

    Only probes for the `local` terminal backend; Docker/SSH/Modal/etc. must
    not inherit the host's sudo state. Re-probes every call (no process-level
    cache) so an expired sudo timestamp cannot make a later command silently
    block waiting for a password.
    TERMINAL_ENVlocalF)r   z-ntruer   )stdinr   stderrr   checkr   )	r   r   striplower
subprocessrunDEVNULL
returncoder3   )terminal_envprobes     r   _sudo_nopasswd_worksr   1  s     9^W55;;==CCEEPLwu"""$%%
 
 
 1$$   uus   AB
 

BBc                 T   t          |           }d}d}d}d}g }||k     rv| |         }|dk    r|dk    r|dk    rd}|dz  })|                                r|dz  }C|dk    r!|                     d|          }|dk    rn|}j|dk    r|dz   |k     r|dz  }|dv r't          | |          \  }	}
t	          |
|dz             }|d	k    r|dz  }|dz  }|d
k    rt	          d|dz
            }|dz  }|dk    rA|dz   |k     r8| |dz                                            s| |dz            dk    r|dz  }|dz  }!|dk    r|dk    r|dz  }d}|dz  };|dk    s|dk    r|dz  }N|                     d|          s|                     d|          r|dz   }|dz  }|dk    r	d}|dz  }|dk    r	d}|dz  }|dk    r|dz   |k     r| |dz            dk    r|dz  }|dz
  }|dk    r?| |                                         r%|dz  }|dk    r| |                                         %|dk    r| |         dv r|dz  }*|dk    r|                    ||f           d}|dz  }Pt          | |          \  }	}
t	          |
|dz             }||k     v|s| S | }t          |          D ]{\  }}|}||k     r?||                                         r%|dz  }||k     r||                                         %|d|         }|||         }||dz   d         }|dz   |z   dz   |z   }||S )u  Wrap `A && B &` (or `A || B &`) to `A && { B & }` at depth 0.

    Bash parses ``A && B &`` with `&&` tighter than `&`, so it forks a
    subshell for the whole `A && B` compound and backgrounds it. Inside
    the subshell, `B` runs foreground, so the subshell waits for `B` to
    finish. When `B` is a long-running process (`python3 -m http.server`,
    `yes > /dev/null`, anything that doesn't naturally exit), the subshell
    never exits. It leaks as a process stuck in ``wait4`` forever — and
    on the way, its open stdout pipe can prevent the terminal tool from
    returning promptly.

    Rewriting the tail to `A && { B & }` preserves `&&`'s error semantics
    (skip B if A fails) while replacing the subshell with a brace group.
    The brace group runs in the current shell (no fork), backgrounds B as
    a simple command (bash doesn't wait for it in non-interactive mode),
    and exits immediately. B runs as a normal backgrounded child, orphaned
    when the parent shell exits.

    Handles redirects (``&>``, ``2>&1``) and skips content inside quoted
    strings and parenthesised subshells. Leaves simple ``cmd &`` alone —
    that construct doesn't have the subshell-wait bug.
    r   r   r   r   r   r   r   >   r   r   (r   {}r   r   ;|&r   z<>Nz{ z& })r   r   r   r   maxr   r   reversed)rk   r   r   paren_depthbrace_depthlast_chain_op_endrewritesrv   nl_r   jr   	chain_endamp_pos
insert_posprefixmiddlesuffixs                      r   _rewrite_compound_backgroundr  K  sg   . 	GA	AKK &(H
a%%QZ ::+**{a/?/? "FA::<< 	FA
 99dA&&BRxxA::!a%!))FA )'155IAvFAE""A991KFA99aq11KFA 99QA(>(>(@(@GAPQENVZDZDZ1KFA99q1K "FA
 ??kAooFA dA&& 	'*<*<T1*E*E 	 !AFA 99 "FA 99 "FA 991uqyyWQU^s22QAAq&&WQZ//11&Q q&&WQZ//11&Avv'!*,,Q A%%!2A 6777 "FA &gq11	6A] a%%`   F&x00 9 9	7 
7""vj'9'A'A'C'C"!OJ 7""vj'9'A'A'C'C"$
7*+!& $'%/&8MrB   c                    | dS t          |           \  }}|s| dfS dt          j        v }|r t          j                            dd          nt	                      }|s|st                      r| dfS t                      du}t          d          p|}|s%|s#|r!t          d          }|rt          |           |s|r||dz   fS | dfS )	a  
    Transform sudo commands to use -S flag if SUDO_PASSWORD is available.

    This is a shared helper used by all execution environments to provide
    consistent sudo handling across local, SSH, and container environments.

    Returns:
        (transformed_command, sudo_stdin) where:
        - transformed_command has every bare ``sudo`` replaced with
          ``sudo -S -p ''`` so sudo reads its password from stdin.
        - sudo_stdin is the password string with a trailing newline that the
          caller must prepend to the process's stdin stream.  sudo -S reads
          exactly one line (the password) and passes the rest of stdin to the
          child command, so prepending is safe even when the caller also has
          its own stdin_data to pipe.
        - If no password is available, sudo_stdin is None and the command is
          returned unchanged so it fails gracefully with
          "sudo: a password is required".

    Callers that drive a subprocess directly (local, ssh, docker, singularity)
    should prepend sudo_stdin to their stdin_data and pass the merged bytes to
    Popen's stdin pipe.

    Callers that cannot pipe subprocess stdin (modal, daytona) must embed
    the password in the command string themselves; see their execute()
    methods for how they handle the non-None sudo_stdin case.

    If SUDO_PASSWORD is not set and an interactive UI is available
    (HERMES_INTERACTIVE=1 or a registered sudo password callback):
      Prompts user for password with 45s timeout, caches for session.

    If SUDO_PASSWORD is not set and NOT interactive:
      Command runs as-is (fails gracefully with "sudo: a password is required").
    N)NNSUDO_PASSWORDr   HERMES_INTERACTIVEr   )r   r   )
r   r   r   r`   rb   r   rC   r   r   rf   )rk   transformedhas_real_sudohas_configured_passwordr=   has_sudo_prompt_callbackshould_prompt_for_sudos          r   _transform_sudo_commandr    s&   F z!?!H!HK }-; #	)
+++&((  # = =Q=S=S }:<<DH,--I1I  # 5= 5=S 51"EEE 	5%m444 1- 1MD000D=rB   )LocalEnvironment)SingularityEnvironment)SSHEnvironment)DockerEnvironment)ModalEnvironment)ManagedModalEnvironment)is_managed_tool_gateway_readyu(	  Execute shell commands on a Linux environment. Filesystem, current working directory, and exported environment variables persist between calls.

Do NOT use cat/head/tail to read files — use read_file instead.
Do NOT use grep/rg/find to search — use search_files instead.
Do NOT use ls to list directories — use search_files(target='files') instead.
Do NOT use sed/awk to edit files — use patch instead.
Do NOT use echo/cat heredoc to create files — use write_file instead.
Reserve terminal for: builds, installs, git, processes, scripts, network, package managers, and anything that needs a shell.
Because exported environment state persists, activate a virtualenv or export setup variables once per session; do not re-source the same environment before every command unless a command proves the shell state was reset.

Foreground (default): Commands return INSTANTLY when done, even if the timeout is high. Set timeout=300 for long builds/scripts — you'll still get the result in seconds if it's fast. Prefer foreground for short commands.
Background: Set background=true to get a session_id. Almost always pair with notify_on_complete=true — bg without notify runs SILENTLY and you have no way to learn it finished short of calling process(action='poll') yourself. Two legitimate uses:
  (1) Long-lived processes that never exit (servers, watchers, daemons) — silent is correct, there's no exit to notify on.
  (2) Long-running bounded tasks (tests, builds, deploys, CI pollers, batch jobs) — MUST set notify_on_complete=true. Without it you'll either forget to poll or sit blocked waiting for the user to surface the result.
For servers/watchers, do NOT use shell-level background wrappers (nohup/disown/setsid/trailing '&') in foreground mode. Use background=true so Hermes can track lifecycle and output.
After starting a server, verify readiness with a health check or log signal, then run tests in a separate terminal() call. Avoid blind sleep loops.
Use process(action="poll") for progress checks, process(action="wait") to block until done.
Working directory: Use 'workdir' for per-command cwd.
PTY mode: Set pty=true for interactive CLI tools (Codex, Claude Code, Python REPL).

Do NOT use vim/nano/interactive tools without pty=true — they hang without a pseudo-terminal. Pipe git output to cat if it might page.
_active_environments_last_activity_creation_locksFcontainer_configc                 d   |                      dd          sdS t          rdS t          5  t          r	 ddd           dS daddd           n# 1 swxY w Y   	 t          t	          j        dd                    }n# t          t          f$ r d}Y nw xY wt          d|          }|dz  }	 d	d
l	m
}m} n# t          $ r Y dS w xY w	  |            } |||          }|rt                              d||           dS dS # t          $ r&}t                              d|           Y d}~dS d}~ww xY w)uo  Run the docker orphan reaper once per process, if enabled.

    Sweeps long-Exited containers labeled ``hermes-agent=1`` for the current
    profile that match the issue #20561 leak class — containers left behind
    by Hermes processes that exited without firing ``atexit`` (SIGKILL,
    OOM, terminal-window-close). The reaper is conservative by default:
    only Exited containers older than ``2 × lifetime_seconds`` and scoped to
    the current profile.

    Gates:

    * ``terminal.docker_orphan_reaper: false`` disables it entirely (the
      operator opted out — usually because they're running multiple
      Hermes processes in the same profile and don't trust the
      conservative defaults).
    * ``_docker_orphan_reaper_ran`` flag — sweep runs once per Python
      interpreter, not on every subagent / RL-rollout / parallel
      ``terminal()`` call.
    docker_orphan_reaperTNTERMINAL_LIFETIME_SECONDS300,  <   r   r   )reap_orphan_containers_get_active_profile_name)max_age_secondsprofile_filterzADocker orphan reaper removed %d stale container(s) for profile %szDocker orphan reaper raised: %s)r`   _docker_orphan_reaper_ran_docker_orphan_reaper_lockintr   r   r   r   r  tools.environments.dockerr-  r.  ImportErrorr   infor3   r1   )r&  lifetimemax_ager-  r.  profileremovedr8   s           r   _maybe_reap_docker_orphansr;  l  s2   *  6==  ! 	# ) )$ 	) ) ) ) ) ) ) ) %)!) ) ) ) ) ) ) ) ) ) ) ) ) ) )ry!<eDDEEz"   2x  HlG	
 	
 	
 	
 	
 	
 	
 	
 	
    ;**,,((#G
 
 
  	KKS    	 	
  ; ; ;6:::::::::;sR   	AAAA"A< <BB+B4 4
CC5C? ?
D/	D**D/_task_env_overridestask_id	overridesc                    |t           | <   |                    d          }t          |t                    r|                                rt          |           }t          5  t                              |           pt                              |          }ddd           n# 1 swxY w Y   |t          |dd          ||_	        dS dS dS dS dS )a#  
    Register environment overrides for a specific task/rollout.

    Called by Atropos environments before the agent loop to configure
    per-task sandbox settings (e.g., a custom Dockerfile for the Modal image).

    Supported override keys:
        - modal_image: str -- Path to Dockerfile or Docker Hub image name
        - docker_image: str -- Docker image name
        - cwd: str -- Working directory inside the sandbox

    Args:
        task_id: The rollout's unique task identifier
        overrides: Dict of config keys to override
    cwdN)
r<  r`   r   r+   r   _resolve_container_task_id	_env_lockr#  r?   r@  )r=  r>  new_cwdcontainer_idenvs        r   register_task_env_overridesrF    s(     $-  mmE""G'3 
GMMOO 
 2':: 	^ 	^&**733]7K7O7OP\7]7]C	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^ 	^?wsE488DCGGG
 
 
 
 ?DDs   5B  B$'B$c                 <    t                               | d           dS )z
    Clear environment overrides for a task after rollout completes.

    Called during cleanup to avoid stale entries accumulating.
    N)r<  re   )r=  s    r   clear_task_env_overridesrH    s      GT*****rB   c                     t          h d          }| r<| t          v r3t          |          }t          |                                          |z  r| S dS )u  
    Map a tool-call ``task_id`` to the container/sandbox key used by
    ``_active_environments``.

    The top-level agent passes ``task_id=None`` and lands on ``"default"``.
    ``delegate_task`` children pass their own subagent ID so that
    file-state tracking, the active-subagents registry, and TUI events stay
    distinct per child -- but we deliberately collapse that ID back to
    ``"default"`` here so subagents share the parent's long-lived container
    (one bash, one /workspace, one set of installed packages).

    Exception: RL / benchmark environments (TerminalBench2, HermesSweEnv, ...)
    call ``register_task_env_overrides(task_id, {...})`` to request a
    per-task Docker/Modal image. When an override is registered for a
    task_id, we honour it by returning the task_id unchanged -- those
    rollouts need their own isolated sandbox, which is the whole point of
    the override.

    CWD-only overrides (registered by the ACP adapter for workspace
    tracking) are *not* isolation signals — they should not cause each
    session to spin up its own container.  Only overrides containing
    backend-specific image keys or ``env_type`` trigger isolation.
    >   rl   modal_imagedocker_imagedaytona_imagesingularity_imager   )	frozensetr<  setkeys)r=  _ISOLATION_KEYSr>  s      r   rA  rA    sj    0   ! ! !  O  7111'0	y~~  ?2 	N9rB   r   c           
          t          j        | |          }	  ||          S # t          t          j        f$ r t          d|  d|d| d          w xY w)zParse an environment variable with *converter*, raising a clear error on bad values.

    Without this wrapper, a single malformed env var (e.g. TERMINAL_TIMEOUT=5m)
    causes an unhandled ValueError that kills every terminal command.
    zInvalid value for : z (expected z1). Check ~/.hermes/.env or environment variables.)r   r   r   jsonJSONDecodeErrorr   s        r   _parse_env_varrV    s     )D'
"
"C
y~~,- 
 
 
> > > > >: > > >
 
 	

s	   
" 0Ac                      	 t          j                    S # t          $ r6 t          j        d          pt           j                            d          cY S w xY w)am  Return the current working directory, tolerating a deleted CWD.

    ``os.getcwd()`` raises FileNotFoundError when the process's working
    directory has been removed out from under it (e.g. a scratch workspace
    that was cleaned up mid-session). Fall back to TERMINAL_CWD, then the
    user's home directory, so terminal setup never crashes on a stale CWD.
    TERMINAL_CWD~)r   getcwdFileNotFoundErrorr   r6   
expanduserrA   rB   r   _safe_getcwdr]    s^    Dy{{ D D Dy((CBG,>,>s,C,CCCCDs    =AAc                  n	   d} t          j        dd          }t          j        dd                                          dv }|dv }|dk    }|r8t          d	d
t          d          }t          dd          }t          dd          }nd}d}d}|rqt          ddt
          j        d          }t          ddt
          j        d          }	t          ddt
          j        d          }
t          ddt
          j        d          }ng }g }	i }
g }|dk    rt                      }n|dk    rd}nd}t          j        d|          rt           j        	                              d}d}|dk    r|rt          j        d          pt                      }t           j        
                    t           j        	                    |                    t          fd |D                       sSt           j                                      r8t           j                                      r                    d!          s}d"nj|dv rfrdt          fd#|D                       }t           j                                       }|s|r%|k    rt                              d$||           |i d%|d&t#          t          j        d'd(                    d)t          j        d*|           d+|d,t          j        d-d.|            d/t          j        d0|           d1t          j        d2|           d3d4|d5|d6t          d7d8          d9t          d:d;          d<t          j        d=d>          d?t          j        d@d>          dAt          dBdC          dDt          j        dEd>          dFt          j        dGt          j        dHdI                                                    dv t          j        dJd                                          dv |||t          j        dKdI                                          dv |	|
t          j        dLd                                          dv |t          j        dMdI                                          dv t          j        dNdI                                          dv dOS )PzBGet terminal environment configuration from environment variables.*nikolaik/python-nodejs:python3.11-nodejs20r   r   &TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACEfalse>   r   yesr   >   modaldockerdaytonasingularityrd  TERMINAL_CONTAINER_CPUr   r$   TERMINAL_CONTAINER_MEMORY5120TERMINAL_CONTAINER_DISK51200g      ?      TERMINAL_DOCKER_FORWARD_ENVz[]z
valid JSONTERMINAL_DOCKER_VOLUMESTERMINAL_DOCKER_ENVz{}TERMINAL_DOCKER_EXTRA_ARGSsshrY  /rootrX  N)z/Users/z/home/zC:\zC:/c              3   B   K   | ]}                     |          V  d S Nr   ).0p	candidates     r   	<genexpr>z"_get_env_config.<locals>.<genexpr>f  s1      ??A	$$Q''??????rB   )
/workspacers  r{  c              3   B   K   | ]}                     |          V  d S ru  rv  )rw  rx  r@  s     r   rz  z"_get_env_config.<locals>.<genexpr>m  s/      DD3>>!,,DDDDDDrB   zeIgnoring TERMINAL_CWD=%r for %s backend (host/relative path won't work in sandbox). Using %r instead.rl   
modal_modeTERMINAL_MODAL_MODEautorK  TERMINAL_DOCKER_IMAGEdocker_forward_envrM  TERMINAL_SINGULARITY_IMAGE	docker://rJ  TERMINAL_MODAL_IMAGErL  TERMINAL_DAYTONA_IMAGEr@  host_cwddocker_mount_cwd_to_workspacer   TERMINAL_TIMEOUT180lifetime_secondsr)  r*  ssh_hostTERMINAL_SSH_HOSTr   ssh_userTERMINAL_SSH_USERssh_portTERMINAL_SSH_PORT22ssh_keyTERMINAL_SSH_KEYssh_persistentTERMINAL_SSH_PERSISTENTTERMINAL_PERSISTENT_SHELLr   TERMINAL_LOCAL_PERSISTENTTERMINAL_CONTAINER_PERSISTENT TERMINAL_DOCKER_RUN_AS_HOST_USER(TERMINAL_DOCKER_PERSIST_ACROSS_PROCESSESTERMINAL_DOCKER_ORPHAN_REAPER)local_persistentcontainer_cpucontainer_memorycontainer_diskcontainer_persistentdocker_volumes
docker_envdocker_run_as_host_userdocker_extra_argsdocker_persist_across_processesr(  )r   r   r   rV  floatrT  loadsr]  r6   r\  abspathanyisabsisdirr   r   r6  r   )default_imagerl   mount_docker_cwdcontainer_backenddocker_backendr  r  r  r  r  r  r  default_cwdr  host_prefixesdocker_cwd_sourceis_host_pathis_relativery  r@  s                     @@r   _get_env_configr  -  s^    AMy11Hy!I7SSYY[[_ss $QQ)N  &'?eXVV)*EvNN'(A7KK 	+,I4QUQ[]ijj'(A4Uabb#$94\ZZ
*+Gtz[ghh

 7"nn	U		 )NK
0
0C
 &g  %%H8M8 0In55GGOOBG$6$67H$I$IJJ	?????????	i((	-/W]]9-E-E	NWNbNbczN{N{	 !HC	B	B	Bs	BDDDDmDDDDD'--,,, 	K 	SK-?-?KK XX{4 4 4 C4H4'	2G(P(PQQ4 		"9=II4 	0	4
 	RY'CE`Q^E`E`aa4 	ry!7GG4 	#;]KK4 	s4 	H4 	()94 	>"4e<<4 	N+FNN4 	BI12664 	BI12664  	N#6==!4" 	29/44#4* 	")%I16::
 
 %'')*+42 I&A7KKQQSSWkk ',( "	*I6 R R X X Z Z^r r( #%9-OQX#Y#Y#_#_#a#aey#y. ,.96,
 ,

%''),* !#	+V!
 !

%'')!*c4 4 4 4rB   r}  c                 X    t          | t                      t          d                    S )z2Resolve direct vs managed Modal backend selection.rc  )
has_directmanaged_ready)r   r   r"  )r}  s    r   _get_modal_backend_stater    s0    &/113G<<   rB   imager@  r   
ssh_configlocal_configr  c	           
      8   |pi }	|	                     dd          }
|	                     dd          }|	                     dd          }|	                     dd          }|	                     d	g           }|	                     d
g           }|	                     di           }|	                     dg           }| dk    rt          ||          S | dk    rt          |	           t          dEi d|d|d|d|
d|d|d|d|d|d|d|	                     dd          d|d|d|	                     d d          d!|d"|	                     d#d          S | d$k    rt	          ||||
||||%          S | d&k    rXi }|
d'k    r|
|d<   |d'k    r||d<   |d'k    rE	 d'd(l}d'd(l}d)|                    |j        j	                  j
        v r||d)<   n# t          $ r Y nw xY wt          |	                     d*                    }|d+         d,k    rt          ||||||-          S |d+         d.k    r|d/         r"t          d0t          d1          z   d2z             |d3         d,k    rt          d4t          d1          z             |d3         d.k    rt          d5          d6}t!                      rd7}t          |          t#          ||||||-          S | d8k    r&d'd9lm}  ||||t)          |
          ||||%          S | d:k    r|r*|                     d;          r|                     d<          st          d=          t+          |d;         |d<         |                     d>d?          |                     d@dA          ||B          S t          dC|  dD          )Fa  
    Create an execution environment for sandboxed command execution.
    
    Args:
        env_type: One of "local", "docker", "singularity", "modal",
            "daytona", "ssh"
        image: Docker/Singularity/Modal image name (ignored for local/ssh)
        cwd: Working directory
        timeout: Default command timeout
        ssh_config: SSH connection config (for env_type="ssh")
        container_config: Resource config for container backends (cpu, memory, disk, persistent)
        task_id: Task identifier for environment reuse and snapshot keying
        host_cwd: Optional host working directory to bind into Docker when explicitly enabled
        
    Returns:
        Environment instance with execute() method
    r  r   r  rl  r  rm  r  Tr  r  r  r  r   )r@  r   rd  r  r@  r   cpumemorydiskpersistent_filesystemr=  volumesr  auto_mount_cwdr  Fforward_envrE  run_as_host_userr  
extra_argspersist_across_processesr  rf  )r  r@  r   r  r  r  r  r=  rc  r   Nephemeral_diskr}  selected_backendmanaged)r  r@  r   modal_sandbox_kwargsr  r=  directmanaged_mode_blockedzModal backend is configured for managed mode, but Nous Tool Gateway access is not currently available and no direct Modal credentials/config were found. managed Modal executionzH Choose TERMINAL_MODAL_MODE=direct/auto to use direct Modal credentials.modez[Modal backend is configured for managed mode, but the managed tool gateway is unavailable. z_Modal backend is configured for direct mode, but no direct Modal credentials/config were found.zHModal backend selected but no direct Modal credentials/config was found.z`Modal backend selected but no direct Modal credentials/config or managed tool gateway was found.re  )DaytonaEnvironmentrr  hostuserz?SSH environment requires ssh_host and ssh_user to be configuredport   keyr   )r  r  r  key_pathr@  r   zUnknown environment type: zD. Use 'local', 'docker', 'singularity', 'modal', 'daytona', or 'ssh'rA   )r`   _LocalEnvironmentr;  _DockerEnvironment_SingularityEnvironmentinspectrc  	signatureSandboxcreate
parametersr3   r  _ManagedModalEnvironmentr   r   r   _ModalEnvironmenttools.environments.daytonar  r3  _SSHEnvironment)rl   r  r@  r   r  r&  r  r=  r  ccr  r  r  
persistentr  r  r  r  sandbox_kwargsr  rc  modal_statemessage_DaytonaEnvironments                           r   _create_environmentr    sB   , 
	RB
&&!
$
$CVV&--F66"E**D.55Jff%r**G 4b99b))J2B777 S'::::	X		 	#2&&&! 
 
 
%
 S
*1'

"F
)-
 #-*
 7>g
 G	

 X
 66"A5III
 +*
 

  VV$=uEEE
 )(
 &(VV,Mt%T%T%T
 	
 
]	"	"&S'F",g
 
 
 	
 
W		77$'N5!A::'-N8$!88%%%%%%%%#w'8'89M'N'N'YYY7;N#34    /rvvl/C/CDD)*i77+g%3&0'    )*h6612 	 < <1  aa   6"i// q;1    6"h.. u   aG)++ v  W%%% S'!/",g
 
 
 	
 
Y		XXXXXX""S'Cd",g
 
 
 	
 
U		 	`!7!7 	`z~~f?U?U 	`^___F#F#++^^E2..
 
 
 	
 ; ; ; ;
 
 	
s   3G 
GGr+  r  c                    t          j                     }	 ddlm} t          t                                                    D ]!}|                    |          r
|t          |<   "n# t          $ r Y nw xY wg }t          5  t          t          	                                          D ]]\  }}||z
  | k    rOt                              |d          }t                              |d           ||                    ||f           ^t          5  |D ] \  }}t                              |d           !	 ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   |D ]+\  }}	 ddlm}  ||           n# t          $ r Y nw xY w	 t#          |d          r|                                 nIt#          |d          r|                                 n$t#          |d          r|                                 t*                              d|           # t.          $ rl}	t1          |	          }
d	|
v sd
|
                                v rt*                              d|           nt*                              d||	           Y d}	~	%d}	~	ww xY wdS )zOClean up environments that have been inactive for longer than lifetime_seconds.r   process_registryNclear_file_ops_cachecleanupstop	terminatez,Cleaned up inactive environment for task: %s404	not found*Environment for task %s already cleaned up-Error cleaning up environment for task %s: %s)r   tools.process_registryr  listr$  rP  has_active_processesr5  rB  itemsr#  re   r   _creation_locks_lockr%  tools.file_toolsr  hasattrr  r  r  r   r6  r3   r+   r   r   )r  current_timer  r=  envs_to_stop	last_timerE  r
  r  r8   	error_strs              r   _cleanup_inactive_envsr  K  sq   9;;L;;;;;;N//1122 	7 	7G44W== 7*6w'	7     L	 3 3"&~';';'='=">"> 	8 	8GYi'*:::*..w==""7D111? ''#777 " 	3 	3* 3 3
##GT22223	3 	3 	3 	3 	3 	3 	3 	3 	3 	3 	3 	3 	3 	3 	33 3 3 3 3 3 3 3 3 3 3 3 3 3 3 % \ \	======  )))) 	 	 	D		\sI&&  f%%  



k**  KKFPPPP 	\ 	\ 	\AI	!![IOO4E4E%E%EH'RRRRNPWYZ[[[	\'\ \su   AA$ $
A10A1=BE	$D:.E:D>	>ED>	EEE%E77
FFB	H
JA!JJc                  6   t           r	 t                      } t          | d                    n4# t          $ r'}t                              d|d           Y d}~nd}~ww xY wt          d          D ]}t           s nt          j        d            t           dS dS )zKBackground thread worker that periodically cleans up inactive environments.r  zError in cleanup thread: %sTr(   Nr,  r   )	_cleanup_runningr  r  r3   r   r   ranger   r   )configr8   r
  s      r   _cleanup_thread_workerr    s    
 
	L$&&F"6*<#=>>>> 	L 	L 	LNN8!dNKKKKKKKK	L r 	 	A# JqMMMM  
 
 
 
 
s   #- 
AAAc                      t           5  t          t                                          s6dat	          j        t          d          at                                           ddd           dS # 1 swxY w Y   dS )z;Start the background cleanup thread if not already running.NTr   )rB  _cleanup_threadis_aliver  rW   r   r  r   rA   rB   r   _start_cleanup_threadr
    s     
 $ $"/*B*B*D*D"#'.6LUYZZZO!!###	$ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $s   AA,,A03A0c                      da t          6	 t                              d           dS # t          t          f$ r Y dS w xY wdS )z#Stop the background cleanup thread.FN   r   )r  r  r   
SystemExitr   rA   rB   r   _stop_cleanup_threadr    sb     "	   +++++-. 	 	 	DD	 #"s   ( ==c                     t          |           }t          5  t                              |          pt                              |           cddd           S # 1 swxY w Y   dS )z9Return the active BaseEnvironment for *task_id*, or None.N)rA  rB  r#  r`   )r=  lookups     r   get_active_envr    s    '00F	 U U#''//T3G3K3KG3T3TU U U U U U U U U U U U U U U U U Us   4AAAc                 d    t          |           }|dS t          t          |dd                    S )a  Return True if the active environment for task_id is configured for
    cross-turn persistence (``persistent_filesystem=True``).

    Used by the agent loop to skip per-turn teardown for backends whose whole
    point is to survive between turns (docker with ``container_persistent``,
    daytona, modal, etc.). Non-persistent backends (e.g. Morph) still get torn
    down at end-of-turn to prevent leakage. The idle reaper
    (``_cleanup_inactive_envs``) handles persistent envs once they exceed
    ``terminal.lifetime_seconds``.
    NF_persistent)r  r   r?   )r=  rE  s     r   is_persistent_envr    s5     
!
!C
{u]E22333rB   c                  t   t          t                                                    } d}| D ]L}	 t          |           |dz  }# t          $ r(}t
                              d||d           Y d}~Ed}~ww xY wt                      }ddl}|                    t          |dz                      D ]g}	 t          j        |d           t
                              d	|           5# t          $ r&}t
                              d
||           Y d}~`d}~ww xY w|dk    rt
                              d|           |S )z3Clean up ALL active environments. Use with caution.r   r   zError cleaning %s: %sTr(   Nr&   )ignore_errorszRemoved orphaned: %sz%Failed to remove orphaned path %s: %szCleaned %d environments)r  r#  rP  
cleanup_vmr3   r   errorr   r*   r+   shutilrmtreer6  r0   r1   )task_idscleanedr=  r8   r4   r*   r6   s          r   cleanup_all_environmentsr    s   (--//00HG M M	MwqLGG 	M 	M 	MLL0'1tLLLLLLLLL	M #$$KKKK		#kJ67788 K K	KM$d3333KK.5555 	K 	K 	KLL@$JJJJJJJJ	K {{-w777Ns/   A
A5A00A521C$$
D.DDforce_remover  c                   d}t           5  t                              | d          }t                              | d           ddd           n# 1 swxY w Y   t          5  t
                              | d           ddd           n# 1 swxY w Y   	 ddlm}  ||            n# t          $ r Y nw xY w|dS 	 t          |d          rSddl
}|                    |j                  }d|j        v r|                    |           n^|                                 nIt          |d          r|                                 n$t          |d          r|                                 t                               d	|            dS # t$          $ rr}t'          |          }d
|v sd|                                v rt                               d|            n"t                               d| |           Y d}~dS Y d}~dS d}~ww xY w)u$  Manually clean up a specific environment by task_id.

    *force_remove* (default False) is forwarded to backends that accept it
    — currently only ``DockerEnvironment``. The default of False matches
    session-lifecycle semantics: this function is called from
    ``AIAgent.close()`` (TUI session close, gateway session teardown) and the
    per-turn cleanup branch for non-persistent envs, both of which should
    honor the user's persist-mode preference. Stopping the container here
    would defeat the "ONE long-lived container shared across sessions"
    contract — exactly the bug Ben reported when the container was killed
    on every TUI session close.

    Pass ``force_remove=True`` for actual user-initiated teardown
    (e.g. ``/reset``-style flows that haven't been wired yet, or future
    "destroy my sandbox" commands).

    The idle reaper passes the env through ``env.cleanup()`` directly (not
    via this function), so persist-mode idle envs are similarly no-op'd —
    only the orphan reaper at next startup reclaims them.
    Nr   r  r  r  r  r  r  z,Manually cleaned up environment for task: %sr  r  r  r  )rB  r#  re   r$  r  r%  r  r  r5  r  r  r  r  r  r  r  r   r6  r3   r+   r   r   )r=  r  rE  r  r  sigr8   r   s           r   r  r    s   0 C	 * *"&&w557D)))* * * * * * * * * * * * * * *
 
 + +GT***+ + + + + + + + + + + + + + +999999W%%%%    {X3	"" 	 NNN##CK00C//6666S&!! 	HHJJJJS+&& 	MMOOOBGLLLLL X X XFF	I	0A0A!A!AKKDgNNNNNNJGUVWWWWWWWWW ONNNNNXsO   7AAABBBB% %
B21B2:CF 
G?A!G::G?c                     t                       t          rt          t                    } t                              d|            t          t                                                    }t                       |D ]W}t          |dd          }|	  |d           $# t          $ r%}t          
                    d|           Y d}~Nd}~ww xY wdS dS )zBStop cleanup thread and shut down all remaining sandboxes on exit.z)Shutting down %d remaining sandbox(es)...wait_for_cleanupNg      .@r   z#wait_for_cleanup raised on exit: %s)r  r#  r   r   r6  r  valuesr  r?   r3   r1   )countenvs_to_waitrE  wait_fnr8   s        r   _atexit_cleanupr(  '  s
    G())?GGG 07799:: """
   	G 	GCc#5t<<GG%%%%% G G GBAFFFFFFFFG%G G	G 	Gs   B
C)C		C	exit_codec                    |dk    rdS t          j        d|           }|r|d         n|                                 }|                                }d}|D ]7}d|v r|                    d          s|                    d          d         } |sdS d	d
id	d
id	d
id	d
id	d
id	d
id	did	did	did	did	didddddd	did}|                    |          }|r||v r||         S dS )a
  Return a human-readable note when a non-zero exit code is non-erroneous.

    Returns None when the exit code is 0 or genuinely signals an error.
    The note is appended to the tool result so the model doesn't waste
    turns investigating expected exit codes.
    r   Nz\s*(?:\|\||&&|[|;])\s*r   r   r   -/r   zNo matches found (not an error)z%Files differ (expected, not an error)zGSome directories were inaccessible (partial results may still be valid)z5Condition evaluated to false (expected, not an error)zCould not resolve hostzFailed to connect to hostz2HTTP response code indicated error (e.g. 404, 500)zOperation timed out)      r     uL   Non-zero exit (often normal — e.g. 'git diff' returns 1 when files differ))grepegrepfgreprgagackdiff	colordiffr   test[curlgit)r   r   r   r   r`   )	rk   r)  segmentslast_segmentwordsbase_cmdw	semanticscmd_semanticss	            r   _interpret_exit_coderC  J  st    A~~t
 x17;;H$,9HRLL'@@BBL   EH  !88ALL--8773<<# t
 676767676767<=@A^_LMLM (*D%	
 
 cd1, ,I6 MM(++M (m33Y''4rB   c                     d                     |                                                                           }|                    d          od|v S )au  Return True when PTY mode would break stdin-driven commands.

    Some CLIs change behavior when stdin is a TTY. In particular,
    `gh auth login --with-token` expects the token to arrive via piped stdin and
    waits for EOF; when we launch it under a PTY, `process.submit()` only sends a
    newline, so the command appears to hang forever with no visible progress.
     zgh auth loginz--with-token)r   r   r   r   rk   
normalizeds     r   _command_requires_pipe_stdinrH    sL     '--////1122Jo.. 	)j(rB   z=(?:^|[;&|]\s*|&&\s*|\|\|\s*|\$\(\s*)(?:nohup|disown|setsid)\bz\s&\sz\s&\s*(?:#.*)?$c                     t          j        dd|           }t          j        dd|          }t          j        dd|          }|S )a:  Remove single- and double-quoted content so regex checks don't match inside strings.

    This prevents false positives when keywords like 'nohup' or 'setsid' appear
    in commit messages, Python -c code, echo arguments, or PR body text.
    Also strips backtick-quoted content and heredoc-style inline text.
    z'[^']*'z''z"(?:[^"\\]|\\.)*"z""z`[^`]*`z``)r   sub)rk   r   s     r   _strip_quotesrK    sC     VJg..FV($77FVJf--FMrB   z@\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start|serve|watch)\bz\bdocker\s+compose\s+up\bz\bnext\s+dev\bz\bvite(?:\s|$)z\bnodemon\bz\buvicorn\bz\bgunicorn\bz$\bpython(?:3)?\s+-m\s+http\.server\bc                     d                     |                                                                           }d|v p-|                    d          pd|v p|                    d          S )zGReturn True for informational invocations that should never be blocked.rE  z --helpz -hz
 --versionz -v)r   r   r   endswithrF  s     r   #_looks_like_help_or_version_commandrN    so    '--////1122JZ 	&u%%	&:%	& u%%	rB   c                 4   t          |           rdS t          |           }t                              |          r	 dS t                              |          st
                              |          r	 dS t          D ]}|                    |          r	  dS dS )zSuggest background mode when a foreground command looks long-lived.

    Prevents workflows that start a server/watch process and then stall before
    follow-up checks or test commands run.
    NzForeground command uses shell-level background wrappers (nohup/disown/setsid). Use terminal(background=true) so Hermes can track the process, then run readiness checks and tests in separate commands.zForeground command uses '&' backgrounding. Use terminal(background=true) for long-lived processes, then run health checks and tests in follow-up terminal calls.zThis foreground command appears to start a long-lived server/watch process. Run it with background=true, verify readiness (health endpoint/log signal), then execute tests in a separate command.)rN  rK  _SHELL_LEVEL_BACKGROUND_REsearch_INLINE_BACKGROUND_AMP_RE_TRAILING_BACKGROUND_AMP_RE_LONG_LIVED_FOREGROUND_PATTERNS)rk   unquotedpatterns      r   _foreground_background_guidancerW    s     +733 t W%%H!((22 
?	
 	
 !''11 
5P5W5WX`5a5a 
W	
 	

 3  >>(## 	<  	 4rB   notify_on_complete
backgroundc                 "    |r
| r|rd}d|fS |dfS )uK  Decide what to do when both notify_on_complete and watch_patterns are set.

    These flags produce duplicate, delayed notifications when combined — one
    notification per watch-pattern match AND one on process exit, with async
    delivery that can spam the user long after the process ends. When both are
    set, we drop watch_patterns in favor of notify_on_complete (the more useful
    "let me know when it's done" signal) and return a human-readable note.

    Returns:
        (watch_patterns_to_use, conflict_note). conflict_note is "" when there
        is no conflict.
    zuwatch_patterns ignored because notify_on_complete=True; these two flags produce duplicate notifications when combinedNr   rA   )rX  watch_patternsrY  notes       r   #_resolve_notification_flag_conflictr]    s=    $  ( ^ L 	 Tz2rB   rE  r  c                     | r| S t          |dd          }t          |t                    r|                                r|S |S )a  Return the cwd for a command, preferring the live session cwd.

    ``terminal_tool`` historically re-sent the init-time/config cwd on every
    call. That broke session-local ``cd`` state: the environment tracked the
    new directory in ``env.cwd``, but foreground/background calls kept forcing
    the old cwd back through ``env.execute(..., cwd=...)``. Explicit
    ``workdir=`` must still override everything.
    r@  N)r?   r   r+   r   )rq   rE  r  live_cwds       r   _resolve_command_cwdr`    sP      sE4((H(C   X^^%5%5 rB   forceptyr[  c	                    	 t          | t                    s]t                              dt	          |           j                   t          j        dddt	          |           j         ddd          S t                      }	|	d	         }
t          |          }|rt                              |          nd
pt                              |i           }|
dk    r|                    d          p|	d         }nn|
dk    r|                    d          p|	d         }nJ|
dk    r|                    d          p|	d         }n&|
dk    r|                    d          p|	d         }nd}|                    d          p|	d         }|	d         }|p|}|s1|r/|t          k    r$t          j        dd| dt           did          S |s,t          |           }|rt          j        dd|ddd          S t                       t          5  |t           v r|n|r|t           v r|nd
}|+t#          j                    t$          |<   t           |         }d}nd}d
d
d
           n# 1 swxY w Y   |rt&          5  |t(          vrt+          j                    t(          |<   t(          |         }d
d
d
           n# 1 swxY w Y   |5  t          5  |t           v r|n|r|t           v r|nd
}|*t#          j                    t$          |<   t           |         }d}d
d
d
           n# 1 swxY w Y   |r|
dk    rt/                       t                              d|
|d
d                    	 d
}|
dk    rl|	                    dd          |	                    dd          |	                    dd          |	                    d d          |	                    d!d          d"}d
}|
d#v r|	                    d$d%          |	                    d&d'          |	                    d(d)          |	                    d*d          |	                    d+d,          |	                    d-g           |	                    d.d          |	                    d/g           |	                    d0i           |	                    d1d          |	                    d2g           |	                    d3d          |	                    d4d          d5}d
}|
d6k    rd7|	                    d8d          i}t3          |
||||||||	                    d9          :	  	        }nB# t4          $ r5}t          j        ddd;| d<d=dd          cY d
}~cd
d
d
           S d
}~ww xY wt          5  |t           |<   t#          j                    t$          |<   |}d
d
d
           n# 1 swxY w Y   t                              d>|
|d
d                    d
d
d
           n# 1 swxY w Y   d
}|s:t7          | |
          }|d?         s|                    d@          dAk    r[t          j        ddddAd|                    dB|           |                    dCdD          |                    dEd          dFd          S |                    dCdD          }dG| dH}t          j        dd|                    dI|          dJdd          S |                    dK          r|                    dCdL          }dM| dN}n1|                    dO          r|                    dCdL          }dP| dQ}|r]t9          |          }|rLt                              dR|d
dS         t;          |                      t          j        dd|dJdd          S d
} |}!|rt=          |           rd}!dT} |rdUdVlm }" dUdWl!m"}#  |"dX          }$tG          |||Y          }%	 |
d6k    r3|#$                    | |%||$tK          |dZ          r|j&        nd
|![          }&n|#'                    || |%||$\          }&d]|&j(        |&j)        dUd
d^}'|r||'d_<   | r| |'d`<   |r	|s|sda|'db<   |rG| rEdc| v pdd| v }(de| v pdf| v pdg| v })dh| v p|(o|)}*|*r'|'                    dbd          }+di},|+r|+djz   |,z   n|,|'db<   |r~|s|rzdUdkl*m+}-  |-dld          }.|.rf |-dmd          }/ |-dnd          }0 |-dod          }1 |-dpd          }2 |-dqd          }3|.|&_,        |/|&_-        |1|&_.        |2|&_/        |0|&_0        |3|&_1        te          tg          |          |tg          |          r          \  }}4|4r&t                              ds|&j(        |4           |4|'dt<   |rd|rbd|&_4        d|'du<   |&j,        rOdv|&_5        |#j6        7                    |&j(        dv|$|&j,        |&j-        |&j.        |&j/        |&j0        |&j1        ddw
           |r |rtq          |          |&_9        |&j9        |'dx<   t          j        |'d          S # tt          $ r4}t          j        dddyt          |           dzd          cY d
}~S d
}~ww xY wd{}5dU}6d
}7|6|5k    ro	 |tG          |||Y          d|}8 |j;        | fi |8}7nH# tt          $ r:}t          |          <                                }9d|9v r#t          j        dd}d~| ddzd          cY d
}~S |6|5k     rd|6d%z  }6d|6z  }:t                              d|:|6|5t;          |           t	          |          j        |||
	  	         t#          j=        |:           Y d
}~t          >                    d|5t;          |           t	          |          j        |||
           t          j        dddt	          |          j         dt          |           dzd          cY d
}~S d
}~ww xY w	 |7                    dd          };|7                    ddU          }<t          |;|
          };	 dUdl@mA}=  |=d| |;|<|pd|
          }>|>D ]}?t          |?t                    r|?}; nn# tt          $ r Y nw xY wdUdlBmC}@  |@            }At          |;          |Ak    r[t          |Adz            }B|A|Bz
  }Ct          |;          |Bz
  |Cz
  }Dd|D dt          |;           d}E|;d
|B         |Ez   |;|C d
         z   };dUdlFmG}F  |F|;          };dUdlHmI}G |;r |G|;J                                          nd};t          | |<          }H|;|<d
dz}I|r||Id_<   |Hr|H|Id<   t          j        |Id          S # tt          $ ri}dUd
lL}J|JM                                }Kt          >                    d|K           t          j        dddt          |           |Kddd          cY d
}~S d
}~ww xY w)u.  
    Execute a command in the configured terminal environment.

    Args:
        command: The command to execute
        background: Whether to run in background (default: False)
        timeout: Command timeout in seconds (default: from config)
        task_id: Unique identifier for environment isolation (optional)
        force: If True, skip dangerous command check (use after user confirms)
        workdir: Working directory for this command (optional, uses session cwd if not set)
        pty: If True, use pseudo-terminal for interactive CLI tools (local backend only)
        notify_on_complete: If True and background=True, you'll be notified exactly once when the process exits. The right choice for almost every long task. MUTUALLY EXCLUSIVE with watch_patterns.
        watch_patterns: List of strings to watch for in background output. HARD rate limit: 1 notification per 15s per process. After 3 strike windows in a row, watch_patterns is disabled and the session is auto-promoted to notify_on_complete. Use ONLY for rare, one-shot mid-process signals on long-lived processes (server readiness, migration-done markers). NEVER use in loops/batch jobs — error patterns there will hit the strike limit and get disabled. MUTUALLY EXCLUSIVE with notify_on_complete — set one, not both.

    Returns:
        str: JSON string with output, exit_code, and error fields

    Examples:
        # Execute a simple command
        >>> result = terminal_tool(command="ls -la /tmp")

        # Run a background task
        >>> result = terminal_tool(command="python server.py", background=True)

        # With custom timeout
        >>> result = terminal_tool(command="long_task.sh", timeout=300)
        
        # Force run after user confirmation
        # Note: force parameter is internal only, not exposed to model API
    z+Rejected invalid terminal command value: %sr   r   z&Invalid command: expected string, got r  )rx   r)  r  statusF)ensure_asciirl   Nrd  rK  rf  rM  rc  rJ  re  rL  r@  r   zForeground timeout zs exceeds the maximum of zNs. Use background=true with notify_on_complete=true for long-running commands.Tz*Creating new %s environment for task %s...   rr  r  r  r  r  r  r  )r  r  r  r  r  >   rc  rd  re  rf  r  r   r  rl  r  rm  r  r}  r  r  r  r  r  r  r  r  r(  )r  r  r  r  r}  r  r  r  r  r  r  r  r(  r   r  r  r  )	rl   r  r@  r   r  r&  r  r=  r  z5Terminal tool disabled: environment creation failed (r   disabledz %s environment ready for task %sapprovedrd  pending_approvalrk   descriptionzcommand flaggedpattern_key)rx   r)  r  rd  approval_pendingrk   rj  rk  zCommand denied: z?. Use the approval prompt to allow it, or rephrase the command.r  blockeduser_approvedzflagged as dangerouszCommand required approval (z) and was approved by the user.smart_approvedzCommand was flagged (z&) and auto-approved by smart approval.z+Blocked dangerous workdir: %s (command: %s)r   zPTY disabled for this command because it expects piped stdin/EOF (for example gh auth login --with-token). For local background processes, call process(action='close') after writing so it receives EOF.r   )get_current_session_keyr  )r   )rq   rE  r  rE  )rk   r@  r=  rY   env_varsuse_pty)rE  rk   r@  r=  rY   zBackground process started)rx   
session_idpidr)  r  rE   pty_noteu  background=true without notify_on_complete=true means this process runs SILENTLY — you will not be told when it exits. If this is a bounded task (test suite, build, CI poller, deploy, anything with a defined end), you almost certainly wanted notify_on_complete=true so the system pings you on exit. Re-launch with notify_on_complete=true, or call process(action='poll') / process(action='wait') yourself to learn the outcome. Only ignore this hint for genuine long-lived processes that never exit (servers, watchers, daemons).hintz
gh pr viewzgh pr checksz jq z| jqz$(jqstatusCheckRollupuR  This looks like a homebrewed CI poller built from `gh pr view --json statusCheckRollup` and/or `gh pr checks | jq`. That shape has burned us repeatedly in hermes-agent dev work (PRs #31329, #31448, #31695, #31709, #31745, #32264, #33131) — stdout buffering kills output capture, jq null-key edge cases silently exit the loop, conclusion-vs-status field confusion exits early with bogus all-green verdicts, TTY-only summary banners never appear when piped. Use the canonical snippets in the green-ci-policy skill instead: the exit-code-driven `gh pr checks $PR >/dev/null` (rc 0 = green, 8 = pending, else fail) for exit-on-first-fail behavior, or the column-2 awk-on-tabs poller (`awk -F"\t" "$2==\"pending\""`) for sharded matrices. Load skill_view(name='github/hermes-agent-dev', file_path='references/green-ci-policy.md') for the verbatim snippets. If you must roll a custom loop with rich structured output, write each tick to a known file (`tee -a /tmp/ci.log`) and rely on `process(action='log')` to read THAT file — do not rely on background-process stdout capture for line-buffered shell loops.z

rO   HERMES_SESSION_PLATFORMHERMES_SESSION_CHAT_IDHERMES_SESSION_THREAD_IDHERMES_SESSION_USER_IDHERMES_SESSION_USER_NAMEHERMES_SESSION_MESSAGE_ID)rX  r[  rY  zbackground proc %s: %swatch_patterns_ignoredrX  r  )
rs  check_intervalrY   r   chat_iduser_id	user_name	thread_id
message_idrX  r[  z$Failed to start background process: )rx   r)  r  r   )r   r@  |   zCommand timed out after z secondsr   zfExecution error, retrying in %ds (attempt %d/%d) - Command: %s - Error: %s: %s - Task: %s, Backend: %szWExecution failed after %d retries - Command: %s - Error: %s: %s - Task: %s, Backend: %szCommand execution failed: rS  rx   r   )invoke_hooktransform_terminal_output)rk   rx   r   r=  rl   )get_max_bytesg?z

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

)
strip_ansi)redact_sensitive_textexit_code_meaningzterminal_tool exception:
%szFailed to execute command: )rx   r)  r  	tracebackrd  )Nr   r+   r   r   r   r   rT  dumpsr  rA  r<  r`   FOREGROUND_MAX_TIMEOUTrW  r
  rB  r#  r   r$  r  r%  rW   Lockr:   r6  r  r5  rp   rw   r   rH  tools.approvalrp  r  r  r`  spawn_localr  rE  spawn_via_envrV   rt  rU   rP   watcher_platformwatcher_chat_idwatcher_user_idwatcher_user_namewatcher_thread_idwatcher_message_idr]  r   rX  watcher_intervalpending_watchersr   r  r[  r3   executer   r   r  r   hermes_cli.pluginsr  tools.tool_output_limitsr  r   r3  tools.ansi_stripr  agent.redactr  r   rC  r  
format_exc)Lrk   rY  r   r=  ra  rq   rb  rX  r[  r  rl   effective_task_idr>  r  r@  default_timeouteffective_timeoutguidance_existing_keyrE  needs_creation	task_lockr  r&  r  new_envr8   approval_noterE   descfallback_msgworkdir_errorpty_disabled_reasoneffective_ptyrp  r  rY   effective_cwdproc_sessionresult_data_gh_has_jq
_bad_shapeexistingcanonical_hint_gse_gw_platform_gw_chat_id_gw_thread_id_gw_user_id_gw_user_name_gw_message_idconflict_notemax_retriesretry_countr   execute_kwargsr   	wait_timerx   r   r  hook_resultshook_resultr  MAX_OUTPUT_CHARS
head_chars
tail_charsomittedtruncated_noticer  r  	exit_noteresult_dictr  tb_strsL                                                                               r   terminal_toolr    s   RM	'3'' 
	#NN=W&   :Z$w--BXZZ!	 
 "# # # # !""*% 7w?? 29B $$W---d >"&&'8"== 	 xMM.11KVN5KEE&&MM"566U&AT:UEE  MM-00IF=4IEE""MM/22Mf_6MEEEmmE""3fUm +#6  	#g 	#'4J*J*J:J' J J-J J J "# # # #  	'6w??H 'z !#%%	# #
 !&' ' ' ' 	  	& 	& &7:N%N%N!!!(VW8L-L-LggRV  (04	}-*=9!&!%	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	&"  Q	e% ? ?$O;;9B9I9IO$56+,=>	? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
  Je Je / /->BV-V-V)))0^W@T5T5TggZ^ " %08<	}52=A)./ / / / / / / / / / / / / / / " >e=001333KK LhXijlkljlXmnnn4/%)
#u,,(.

:r(B(B(.

:r(B(B(.

:r(B(B'-zz)R'@'@.4jj9I5.Q.Q* *J ,0(#'TTT17OQ1O1O4:JJ?QSW4X4X28**=Mu2U2U8>

CY[_8`8`.4jjv.N.N28**=Mr2R2RAGLkmrAsAs6<jjAUWY6Z6Z.4jjr.J.J;A::F_af;g;g5;ZZ@SUW5X5XCI::NoquCvCv8>

CY[_8`8`0 0,  (,#w.. ,fjj9KU.S.S,L #6%-"' #$5'1-=)5$5%+ZZ
%;%;
# 
# 
# ' / / /#z&()+%a]^%a%a%a&0	+ +
 ). /  /  / / / / / /Je Je Je Je Je Je Je Je|/ # & &BI,->?<@IKK'89%& & & & & & & & & & & & & & & KK BHN_`bab`bNcdddUJe Je Je Je Je Je Je Je Je Je Je Je Je Je Je\  !	e((;;HJ' '<<))-???:"$%'!#"4,0#+<<	7#C#C'/||MCT'U'U'/||M2'F'F	' 	' %*	+ 	+ 	+ 	+  ||M3DEETt T T T  z !#%\\)\BB'	# #
 !&' ' ' ' ||O,, e||M3IJJ cd c c c.// e||M3IJJ d d d d  
	'-g66M 'L&tt}.CG.L.LN N Nz !#*'	# #
 !&' ' ' ' # 	/88 	!M    I	? ?>>>>>??????11"===K0  M
M'w&&#3#?#? ') 1$/,3C,?,?!IT - $@ $ $LL $4#A#A ') 1$/ $B $ $L ;"./'+!"!  ! <.;K
+& B.AK
+  &8  	H  'T  0' 0'72On6OC')SVw->S&GBS  ,w6 -
  OG  "  #.??62#>#>= '8 CK 0Hv->>!/ $F+  I#5 I IOOOOOO#'4(A2#F#FL# I&*d+CR&H&H(,-G(L(L&*d+CR&H&H(,-G(L(L)-.I2)N)N8D57B47B49F69F6:H7 1T'+,>'?'?#1#J//1 1 1-
 ! JNN#;\_m\\\<IK 89 & * 6:L38<K 45
 $4 895(9@@*6/./+6(4(E'3'C'3'C)5)G)5)G*6*I26B B    " Pj P26~2F2FL/4@4OK 01z+EBBBB ' ' 'z !#LCFFLL# # !&	' ' ' ' ' ' ' ' '' KKF,,"+#43$+ #(+     & &N )S[CCNCCFF  + + + #AI I--#z&(),%[@Q%[%[%[+ + ).	 /  /  / / / / / / / #[00#q($%$4	  (P'0+{LabiLjLjlpqrlslsl|~  BS  U]^ ^ ^
9--- LL!z!,.CG.L.LdSTggN^`actv~@ @ @:"$%'!Zd1gg>N!Z!ZRUVWRXRX!Z!Z' ' %*	+ + + + + + + + +)+6  ZZ"--FL!44J *&(;;F::::::*{/#!)-3%      $0  K!+s33 !,     ?>>>>>,}6{{--- !1C!788
-
:
f++
2Z?;7 ; ;!&kk; ; ; !  ,/??&*BVV 433333Z''F ;:::::>DL**6<<>>:::"F -WjAAI !' K
  8*7J' =3</0:k>>>> 
 
 
%%''3V<<<z;3q66;;
 
    	 	 	 	 	 		
s  A1u 4Eu  -u .u A
Iu Iu  I!u .2J, u ,J00u 3J04u 9W.ALW.L	W.L	>W.GT65W.6
U5 U0U5W.#u 0U55
W.?(V3'W.3V7	7W.:V7	;'W."u .W22u 5W26Bu 
A
u Cu Au H;h 
i$)iiu iu )#j u oAoou !A%ou A=oo	u oA u 7q u 
qu qC2u 
v?Av:4v?:v?c                  H   	 t                      } | d         }|dk    rdS |dk    r]ddlm}  |            }|st                              d           dS t          j        |d	gdd
t
          j                  }|j        dk    S |dk    r[t          j
        d          pt          j
        d          }|r/t          j        |dgdd
t
          j                  }|j        dk    S dS |dk    rH|                     d          r|                     d          st                              d           dS dS |dk    rwt          |                     d                    }|d         dk    rdS |d         dk    r|d         r*t                              dt          d                     dS |d         dk    r*t                              dt          d                     dS |d         dk    rEt                      rt                              d           nt                              d           dS t                      rt                              d           nt                              d            dS t          j                            d          t                              d"           dS dS |d#k    rdd$lm} t)          j        d%          d!uS t                              d&|           dS # t,          $ r(}t                              d'|d(           Y d!}~dS d!}~ww xY w))z8Check if all requirements for the terminal tool are met.rl   r   Trd  r   )find_dockerz?Docker executable not found in PATH or common install locationsFversionr  )capture_outputr   r   rf  	apptainerz	--versionrr  r  r  zSSH backend selected but TERMINAL_SSH_HOST and TERMINAL_SSH_USER are not both set. Configure both or switch TERMINAL_ENV to 'local'.rc  r}  r  r  r  r  zModal backend selected with TERMINAL_MODAL_MODE=managed, but Nous Tool Gateway access is not currently available and no direct Modal credentials/config were found. %s Choose TERMINAL_MODAL_MODE=direct/auto to use direct Modal credentials.r  r  zhModal backend selected with TERMINAL_MODAL_MODE=managed, but the managed tool gateway is unavailable. %szModal backend selected with TERMINAL_MODAL_MODE=direct, but no direct Modal credentials/config were found. Configure Modal or choose TERMINAL_MODAL_MODE=managed/auto.zModal backend selected with TERMINAL_MODAL_MODE=direct, but no direct Modal credentials/config were found. Configure Modal or choose TERMINAL_MODAL_MODE=auto.zModal backend selected but no direct Modal credentials/config or managed tool gateway was found. Configure Modal, set up the managed gateway, or choose a different TERMINAL_ENV.z|Modal backend selected but no direct Modal credentials/config was found. Configure Modal or choose a different TERMINAL_ENV.NzFmodal is required for direct modal terminal backend: pip install modalre  )DaytonaDAYTONA_API_KEYzWUnknown TERMINAL_ENV '%s'. Use one of: local, docker, singularity, modal, daytona, ssh.z&Terminal requirements check failed: %sr(   )r  r4  r  r   r  r   r   r   r   r  whichr`   r  r   r   	importlibutil	find_specre  r  r   r   r3   )	r  rl   r  rd  r   
executabler  r  r8   s	            r   check_terminal_requirementsr  	  s   i ""*%w4!!====== []]F ^___u^VY$7VW_i_qrrrF$))&&k22Qfl=6Q6QJ .#[(ARV`aisi{|||(A--5::j)) J1G1G Z   u4  26::l3K3KLLK-.);;t-.(::56 
!LL[ >5    !5v&)33LL:=5    !5 (H44133 @    8  
 !5133 
B    R   !5~''008efffu4""''''''9.//t;; LL'  
 5   =q4PPPuuuuusb   K/ 2K/ .K/ AK/ $A
K/ 25K/ )<K/ '4K/ AK/ .AK/ 39K/ 0!K/ K/ /
L!9LL!__main__zTerminal Tool Modulez2==================================================z
Current Configuration:z  Environment type: z  Docker image: rK  z  Modal image: rJ  z  Working directory: z  Default timeout: sz  Lifetime: u;   
❌ Requirements not met. Please check the messages above.r   u   
✅ All requirements met!z
Available Tool:z=  - terminal_tool: Execute commands in sandboxed environmentsz
Usage Examples:z  # Execute a commandz*  result = terminal_tool(command='ls -la')z  z  # Run a background taskzE  result = terminal_tool(command='python server.py', background=True)z
Environment Variables:r_  z  TERMINAL_ENV: r   r   z- (local/docker/singularity/modal/daytona/ssh)z  TERMINAL_DOCKER_IMAGE: r  z  TERMINAL_SINGULARITY_IMAGE: r  r  z  TERMINAL_MODAL_IMAGE: r  z  TERMINAL_DAYTONA_IMAGE: r  z  TERMINAL_CWD: rX  r{   z  TERMINAL_SANDBOX_DIR: TERMINAL_SANDBOX_DIRz
/sandboxesz  TERMINAL_TIMEOUT: r  60z  TERMINAL_LIFETIME_SECONDS: r)  r*  )registryterminalobjectstringz The command to execute on the VM)r   rj  booleanus  Run the command in the background. Almost always pair with notify_on_complete=true — without it, the process runs silently and you'll have no way to learn it finished short of calling process(action='poll') yourself (easy to forget, leading to silent blindness on long jobs). Two legitimate patterns: (1) Long-lived processes that never exit (servers, watchers, daemons) — these stay silent because there's no exit to notify on. (2) Long-running bounded tasks (tests, builds, deploys, CI pollers, batch jobs) — these MUST set notify_on_complete=true. For short commands, prefer foreground with a generous timeout instead.)r   rj  r   z3Max seconds to wait (default: 180, foreground max: u   ). Returns INSTANTLY when command finishes — set high for long tasks, you won't wait unnecessarily. Foreground timeout above z7s is rejected; use background=true for longer commands.)r   rj  minimumz^Working directory for this command (absolute path). Defaults to the session working directory.zRun in pseudo-terminal (PTY) mode for interactive CLI tools like Codex, Claude Code, or Python REPL. Only works with local and SSH backends. Default: false.u  When true (and background=true), you'll be automatically notified exactly once when the process finishes. **This is the right choice for almost every long-running task** — tests, builds, deployments, multi-item batch jobs, anything that takes over a minute and has a defined end. Use this and keep working on other things; the system notifies you on exit. MUTUALLY EXCLUSIVE with watch_patterns — when both are set, watch_patterns is dropped.arrayr   u
  Strings to watch for in background process output. HARD RATE LIMIT: at most 1 notification per 15 seconds per process — matches arriving inside the cooldown are dropped. After 3 consecutive 15-second windows with dropped matches, watch_patterns is automatically disabled for that process and promoted to notify_on_complete behavior (one notification on exit, no more mid-process spam). USE ONLY for truly rare, one-shot mid-process signals on LONG-LIVED processes that will never exit on their own — e.g. ['Application startup complete'] on a server so you know when to hit its endpoint, or ['migration done'] on a daemon. DO NOT use for: (1) end-of-run markers like 'DONE'/'PASS' — use notify_on_complete instead; (2) error patterns like 'ERROR'/'Traceback' in loops or multi-item batch jobs — they fire on every iteration and you'll hit the strike limit fast; (3) anything you'd ever combine with notify_on_complete. When in doubt, choose notify_on_complete. MUTUALLY EXCLUSIVE with notify_on_complete — set one, not both.)r   r  rj  )rk   rY  r   rq   rb  rX  r[  )r   
propertiesrequired)r   rj  r  c                 f   t          |                     d          |                     dd          |                     d          |                    d          |                     d          |                     dd          |                     dd          |                     d	          
          S )Nrk   rY  Fr   r=  rq   rb  rX  r[  )rk   rY  r   r=  rq   rb  rX  r[  )r  r`   )argskws     r   _handle_terminalr  g
  s    ##88L%00##y!!##HHUE""88$8%@@xx 011	 	 	 	rB   u   💻i )r   toolsetschemahandlercheck_fnemojimax_result_size_chars)rM   N)r   )r   )NNNr   N)r+  )FNNFNFFN)__doc__importlib.utilr  rT  loggingr   r   r   r   rW   atexitr  r   pathlibr   typingr   r   r   r   utilsr   	getLoggerr   r   tools.interruptr	   r
   tools.environments.singularityr   tools.tool_backend_helpersr   r   r   r   r   r+   r    r3  r  r  r2   r:   r;   dict__annotations__r  r_   r   r@   rC   rF   rJ   rL   r]   rb   rf   ri   r  rj   ro   rp   compilers   rw   r   r   r   r   r   tupler   r   r   r  r  tools.environments.localr  r  r  r  tools.environments.sshr  r  r4  r  r  tools.environments.modalr   r   tools.environments.managed_modalr!  r  tools.managed_tool_gatewayr"  r   TERMINAL_TOOL_DESCRIPTIONr#  r$  rB  r%  r  r  r  r1  r2  r;  r<  rF  rH  rA  rV  r]  r  r  r  r  r  r  r
  r  r  r  r  r  r(  registerrC  rH  
IGNORECASE	MULTILINErP  rR  rS  rK  rT  rN  rW  r]  r`  r  r  r   r  exitdefault_imgr   r}   r|   r   tools.registryr  TERMINAL_SCHEMAr  rA   rB   r   <module>r     s    @       				  				                  , , , , , , , , , , , , ! ! ! ! ! !		8	$	$ = < < < < < < < < ; ; ; ; ;             
 	   8 0/%	   #9"8			# #   F (* d38n ) ) )*IN,,   	!!9 9 94 4 4% % %     - - - - -,33 3 3 3 32 2 2 2 2 2% % % %     
Ns Nc Nd N N N N 2:=>> s sTz    ,      4z3 z3s z3C z3 z3 z3 z3x	- 	-3 	-s 	-S 	- 	- 	- 	-=c =d = = = =!s !3 !5c? ! ! ! !H:C :E#t)4D : : : :zd    4b# b# b b b bJFS4Z FE#*cDj:P4Q F F F FT K J J J J J \ \ \ \ \ \ D D D D D D M M M M M M J J J J J J ` ` ` ` ` ` D D D D D D 



 0 (* d38n ) ) )#%S%Z  % % %IN	-/c9>)* / / /%y~''  
 " +Y^-- >;c3h >;D >; >; >; >;R 24 T#tCH~-. 3 3 3& &c3h & & & &R+c + + + +   #        J >AT] 
 
 
s 
s 
c 
 
 
 
 Dc D D D D|c3h | | | |~$ 4S>     KO-1'0(,	S
 S
# S
c S
 S
c S
$(S
CGS
&*S
 "%S
 #&	S
 S
 S
 S
l;\ ;\S ;\ ;\ ;\ ;\|  $ $ $  UC U U U U4s 4t 4 4 4 4&  6 6; AX AX AX AXd AX AX AX AXHG G G0       =# =# =#* = = = =@# $     (RZDbmVXVbFb   'BJx00 (bj);<< 3 3    " BJRTVTabbBJ+R];;BJ "-00BJ "-00BJ~r}--BJ~r}--BJ..BJ6FF	#      "S "S4Z " " " "J 	
    6c] 
 	
 	   4 !!!$*.v	 v	v	v	 c]v	 c]	v	
 v	 c]v	 
v	 v	 T#Y'v	 	v	 v	 v	 v	rkT k k k k\ z	E
 !!!	E(OOO_F	E
$%%%	E
5
!3
5
5666	E
5VN3
5
5666	E
3F=1
3
3444	E
1&-
1
1222	E
4y 1
4
4
4555	E
6 23
6
6
6777&&(( LMMM	E
'(((	E
	E
IJJJ	E
	E
!"""	E
6777	E$KKK	E
%&&&	E
QRRR	E
$%%%>K	E	729^W--	7 	7 	7  
 
E
Wibi0G&U&U
W
WXXX	E
o9295QSl_jSlSl+m+m
o
oppp	E
UYRY/E{%S%S
U
UVVV	E
Yyry1I;'W'W
Y
YZZZ	E
HYRY~||~~FF
H
HIII<<<<<<	E
_YRY/E$$&&G\G\G\%]%]
_
_```	E
F+=t!D!D
F
FGGG	E
Y)")4OQV*W*W
Y
YZZZ $ # # # # # , !A 
 "  U
   "  |Uk   |   |  mC   |   |   |  ! 
 "  ~   "  ` # #   (+  l ;"
 "
F KK& &* *Z
 
 
  	(
!     rB   