+
    lHàjC2  ã                   ó@  € R t ^ RIt^ RIt^ RIt^ RIt^ RIHtHtHt ^ RI	H
t
HtHt ^ RIHt ]P                  ! R4      tR R ltRR/R	 R
 lltRRRRRR/R R lltR R ltR R ltR$R R lltR R ltR R ltR%R R lltR R ltR tRR/R  R! lltR" R# ltR# )&a¤  Sandbox lifecycle for the terminal tool: idle reaping, teardown, manual/atexit
cleanup, and the lazy ensure_task_env bring-up. The env cache dicts and locks
stay in tools.terminal_tool (tests patch them there) and are read through it
at call time.

Split out of ``tools/terminal_tool.py``; every public/patched name is re-imported there,
so ``tools.terminal_tool.<name>`` keeps resolving (and monkeypatching) as before.
N)ÚAnyÚDictÚOptional)Ú_container_config_from_configÚ_local_config_from_configÚ_ssh_config_from_config)Ú_quietztools.terminal_toolc                ó¤   € V ^8„  d   QhR\         \        \        3,          R\        R\        R\        R\        R\        R\        \        ,          /# )é   ÚconfigÚenv_typeÚimageÚcwdÚtimeoutÚtask_idÚhost_cwd)ÚdictÚstrr   Úintr   )Úformats   "ÚB/home/ubuntu/.hermes/hermes-agent/tools/terminal_tool_lifecycle.pyÚ__annotate__r      sP   € ÷ ñ Ü””c��NðÜ&)ðÜ58ðÜ?BðÜMPðäðä$¤S�Mñó    c               ó°   € ^ RI Hp ^ RIHp T! YY4VR8X  d   \	        V 4      MRV! V4      '       d   \        V 4      MRVR8X  d   \        V 4      MRWVR7	      # )z­``_create_environment`` with the ssh/local/container kwargs shaped from *config*
(shared by the terminal tool, the file tools and the lazy :func:`ensure_task_env` bring-up).)Ú_create_environment)Ú_is_container_backendÚsshNÚlocal)	r   r   r   r   Ú
ssh_configÚcontainer_configÚlocal_configr   r   )Útools.terminal_tool_backendsr   Útools.terminal_tool_configr   r   r   r   )	r   r   r   r   r   r   r   r   r   s	   &&$$$$$  r   Ú_create_configured_envr#      s[   € õ AÝ@ÙØ¨CØ6>À%Ô6GÔ*¨6Ô2ÈTá5JÈ8×5TÒ5TÔ)¨&Ô1ÐZ^à:BÀgÔ:MÔ.¨vÔ6ÐSWØôð r   Úforce_removec                óJ   € V ^8„  d   QhR\         R\        \        ,          RR/# )r
   Úenvr$   ÚreturnN)r   r   Úbool)r   s   "r   r   r   -   s%   € ÷ ñ ”cð ¬H´T­Nð Àdñ r   c               ól  € \        V R4      '       dZ   VeD   R\        P                  ! V P                  4      P                  9   d   V P                  VR7       R# V P                  4        R# \        V R4      '       d   V P                  4        R# \        V R4      '       d   V P                  4        R# R# )ai  Tear down one environment via cleanup()/stop()/terminate(), whichever it has.

``force_remove`` is forwarded to ``cleanup()`` only when given and the backend's
signature accepts it (``DockerEnvironment``, issue #20561; other backends don't).
Shared by ``cleanup_vm``, the idle reaper and the prompt-time backend probe so
the signature check lives in one place.
ÚcleanupNr$   ©r$   ÚstopÚ	terminate)ÚhasattrÚinspectÚ	signaturer*   Ú
parametersr,   r-   )r&   r$   s   &$r   Ú_cleanup_envr2   -   s{   € ô ˆs�I×ÒØÒ#¨¼'×:KÒ:KÈCÏKÉKÓ:X×:cÑ:cÔ(cØ�K‰K \ˆKÖ2à�K‰KŽMÜ	��f×	Ò	Ø�‰Ž
Ü	��k×	"Ò	"Ø�‰Žñ 
#r   Údone_msgz,Cleaned up inactive environment for task: %sÚ	stop_idleFc                ón   € V ^8„  d   QhR\         R\        R\        \        ,          R\        R\        RR/# )r
   r&   r   r$   r3   r4   r'   N)r   r   r   r(   )r   s   "r   r   r   @   sM   € ÷ Xñ X”sð X¤Sð X¼8ÄD½>ð XÔ\_ð Xô  ^bð Xð  osñ Xr   c               ó`  €  V'       d   V P                  4        M\        WR7       \        P                  W14       R#   \         de   p\        T4      pRT9   g   RTP                  4       9   d   \        P                  RT4        Rp?R# \        P                  RY4        Rp?R# Rp?ii ; i)u¨   ``_cleanup_env`` (or, with *stop_idle*, the backend's ``stop_idle``) plus outcome logging.
A 404/"not found" error means the sandbox is already gone â€” logged at info.r+   Ú404z	not foundz*Environment for task %s already cleaned upz-Error cleaning up environment for task %s: %sN)r4   r2   ÚloggerÚinfoÚ	Exceptionr   ÚlowerÚwarning)r&   r   r$   r3   r4   ÚeÚ	error_strs   &&$$$  r   Ú_teardown_envr?   @   s}   € ðXßØ�M‰M�Oä˜Õ8Ü�‰�HÖ&øÜô XÜ˜“Fˆ	Ø�IÔ °	·±Ó0AÔ!AÜ�K‰KÐDÀg×NÒNä�N‰NÐJÈG×WÒWûðXús   ‚> Š2> ¾B-Á	=B(ÂB(Â(B-c                ó(   € V ^8„  d   QhR\         RR/# ©r
   r   r'   N©r   )r   s   "r   r   r   Q   s   € ÷ ñ ¤3ð ¨4ñ r   c                óH   €  ^ RI Hp V! V 4       R#   \         d     R# i ; i)zZInvalidate the file_ops cache entry so ShellFileOperations can't reference a dead sandbox.)Úclear_file_ops_cacheN)Útools.file_toolsrD   ÚImportError)r   rD   s   & r   Ú_clear_file_ops_cacherG   Q   s$   € ðÝ9Ù˜WÖ%øÜô Úðús   ‚ ’! !c                ó$   € V ^8„  d   QhR\         /# ©r
   r   rB   )r   s   "r   r   r   Z   s   € ÷ ñ œSñ r   c                ó(  € ^ RI HpHpHpHpHp V;_uu_ 4        VP                  V R4      pVP                  V R4       RRR4       V;_uu_ 4        VP                  V R4       RRR4       X#   + '       g   i     L7; i  + '       g   i     X# ; i)uò   Pop *task_id* from the env cache, activity map and creation locks; return
the env (or None). Callers run the (slow) teardown OUTSIDE the lock â€”
Modal/Docker teardown can block 10-15s and would stall every concurrent
terminal/file tool call.©Ú_active_environmentsÚ_creation_locksÚ_creation_locks_lockÚ	_env_lockÚ_last_activityN)Útools.terminal_toolrL   rM   rN   rO   rP   Úpop)r   rL   rM   rN   rO   rP   r&   s   &      r   Ú_unregister_envrS   Z   sr   € ÷
õ ÷ 
ŠØ"×&Ñ& w°Ó5ˆØ×Ñ˜7 DÔ)÷ 
÷ 
Ò	Ø×Ñ˜G TÔ*÷ 
à€J÷ 
�ú÷ 
Ö	à€Jús   ™%A-ÁB Á-A=	Â B	c                ó$   € V ^8„  d   QhR\         /# )r
   Úlifetime_seconds)r   )r   s   "r   r   r   k   s   € ÷ ',ñ ',¬Sñ ',r   c           
     óî  € ^ RI HpHpHpHpHp \        P                  ! 4       p ^ RIHp \        VP                  4       4       FN  pVP                  V4      '       g1   \        V4      '       g   K-  VP                  V3R l4      '       g   KJ  WeV&   KP  	  T;_uu_ 4        \        VP                  4       4       U	U
u. uF  w  ršWj,
          V 8”  g   K  V	NK  	  pp	p
V U	u. uF  q™VP!                  V	R4      3NK  	  pp	V F  p	VP!                  V	R4       K  	  T;_uu_ 4        V F  p	VP!                  V	R4       K  	  RRR4       RRR4       X F]  w  r�Vf   K  \#        V4       \        V4      '       d,   \%        \'        VRR4      4      '       d   \)        WØRR7       KR  \)        WØ4       K_  	  R#   \         d     EL,i ; iu up
p	i u up	i   + '       g   i     Lš; i  + '       g   i     L¥; i)zOClean up environments that have been inactive for longer than lifetime_seconds.rK   )Úprocess_registryc                 ó    € V P                   V8H  # ©N)r   )ÚsÚkeys   &&r   Ú<lambda>Ú(_cleanup_inactive_envs.<locals>.<lambda>{   s   € ÈQÏYÉYÐZ]ÒM]r   Nr4   T)r4   )rQ   rL   rM   rN   rO   rP   ÚtimeÚtools.process_registryrW   ÚlistÚkeysÚhas_active_processesÚ_is_workspace_keyÚ_any_runningrF   ÚitemsrR   rG   ÚcallableÚgetattrr?   )rU   rL   rM   rN   rO   rP   Úcurrent_timerW   r   ÚtÚlastÚstaleÚenvs_to_stopr&   s   &             r   Ú_cleanup_inactive_envsrm   k   s‘  € ÷õ ô —9’9“;€Lð
Ý;Ü˜N×/Ñ/Ó1Ö2ˆGð  ×4Ñ4°W×=Ò=Ü% g×.Ô.Ø(×5Ñ5ÀGÓ6]×^Ô^Ø*6˜wÓ'ñ 3÷ 
ŠÜ"& ~×';Ñ';Ó'=Ô">ÔiÑ">‘w�qÀ,ÕBUÐXhÑBh—�Ñ">ˆÑiÙHMÓNÉÀ1Ð0×4Ñ4°Q¸Ó=Ó>ÉˆÐNÛˆAØ×Ñ˜q $Ö'ñ ç!Ò!Û�Ø×#Ñ# A tÖ,ñ ÷ "÷ 
ó %‰ˆØŒ?Ü! 'Ô*Ü  ×)Ò)¬h´w¸sÀKÐQUÓ7V×.WÒ.Wô ˜c°d×;ä˜cÖ+ó %øô ô Úðüó jùÚN÷ "×!ú÷ 
�úsf   ¥AF4 Á1F4 ÂF4 Â G$Â=G
ÃG
ÃG$Ã GÃ<'G$Ä#G	Ä?G$Æ4GÇGÇG$ÇG!ÇG$Ç$G4	c                ó0   € V ^8„  d   QhR\         R\        /# )r
   r[   r'   ©r   r(   )r   s   "r   r   r   •   s   € ÷ $ñ $œ3ð $¤4ñ $r   c                ó2   € R\        T ;'       g    R4      9   # )zPA persistent Docker key bound to one session workspace (``<base>:ws:<digest>``).z:ws:Ú rB   )r[   s   &r   rc   rc   •   s   € à”S˜Ÿ˜ “^Ñ#Ð#r   c                ó$   € V ^8„  d   QhR\         /# rI   rB   )r   s   "r   r   r   š   s   € ÷ Uñ UœCñ Ur   c                óÌ   € ^ RI HpHpHp V! V 4      pT;_uu_ 4        VP	                  V4      ;'       g    VP	                  V 4      uuRRR4       #   + '       g   i     R# ; i)z9Return the active BaseEnvironment for *task_id*, or None.)rL   rO   Ú_resolve_container_task_idN)rQ   rL   rO   rt   Úget)r   rL   rO   rt   Úlookups   &    r   Úget_active_envrw   š   sE   € ç_Ñ_Ù'¨Ó0€Fß	ŠØ#×'Ñ'¨Ó/×TÐTÐ3G×3KÑ3KÈGÓ3T÷ 
��‹ús   �*AÁA#	c                ó:   € V ^8„  d   QhR\         \        ,          /# rI   ©r   r   )r   s   "r   r   r   ¢   s   € ÷ ?ñ ?œX¤c�]ñ ?r   c                óê  € ^ RI HpHpHpHpHpHpHpHpH	p	H
p
Hp V! 4       pVR,          pVR8X  d   R# V! V 4      p\        V4      pVe-   V;_uu_ 4        \        P                  ! 4       Wn&   RRR4       V# V	! WÛ! V 4      V4      pV
! 4        V;_uu_ 4        VP                  V\        P                   ! 4       4      pRRR4       X;_uu_ 4        \        V4      pVe   VuuRRR4       #  \#        WÍVVR,          VR,          VV! WÀ4      R7      pT;_uu_ 4        TY&   \        P                  ! 4       Yn&   RRR4       \&        P+                  R
YÞR	,          4       TuuRRR4       #   + '       g   i     T# ; i  + '       g   i     LÇ; i  \$         d2   p\&        P)                  RYÞR	,          T4        Rp?RRR4       R# Rp?ii ; i  + '       g   i     LŸ; i  + '       g   i     R# ; i)ub  Lazily create and cache the sandbox env for *task_id* if none is active.

Lets non-terminal callers (``tools.image_source`` reading container-only
paths) bring the sandbox up on demand with the same machinery as the
terminal tool. No-op on local. Returns the env, or ``None`` when local or
when creation fails (best-effort; the caller's fail-closed path stays intact).

:func:`terminal_tool` creates the environment on the first terminal command, but nothing else did â€” so
under a non-local backend (ssh, docker, â€¦) a session whose first action is ``vision_analyze`` on a
container-only path hit "no active sandbox session" because the SSH/Docker handshake never ran (issue
#62825). vision reads such paths inside the sandbox (see ``tools.image_source``), so it calls this to
bring the env up on demand, reusing the same creation machinery as the terminal tool.
)rL   rM   rN   rO   Ú_get_env_configrP   rt   Ú_resolve_task_host_cwdÚ_select_imageÚ_start_cleanup_threadÚresolve_task_overridesr   r   Nr   r   )r   r   r   r   r   z/Lazy %s environment init failed for task %s: %s:Né   Nz-%s environment lazily initialized for task %s)rQ   rL   rM   rN   rO   r{   rP   rt   r|   r}   r~   r   rw   r^   Ú
setdefaultÚ	threadingÚLockr#   r:   r8   r<   r9   )r   rL   rM   rN   rO   r{   rP   rt   r|   r}   r~   r   r   r   Úeffective_task_idÚexistingr   Ú	task_lockÚnew_envÚexcs   &                   r   Úensure_task_envr‰   ¢   s¢  € ÷÷ ÷ ñ ñ
 Ó€FØ�jÕ!€HØ�7ÔÙá2°7Ó;ÐäÐ/Ó0€HØÒßŠYÜ04·	²	³ˆNÑ-÷ àˆá˜(Ð$:¸7Ó$CÀVÓL€EáÔç	Ò	Ø#×.Ñ.Ð/@Ä)Ç.Â.ÓBRÓSˆ	÷ 
÷ 
ŠÜ!Ð"3Ó4ˆØÒØ÷ 
Šð	Ü,Ø¨°6¸%µ=Ø˜yÕ)Ð3DÙ/°Ó@ôˆG÷ ŠYØ6=Ð Ñ3Ü04·	²	³ˆNÑ-÷ ô 	�‰Ø;Ø¨Õ+ô	
ð ÷1 
Š÷ ŽYàˆú÷ 
×	ûô ô 	Ü�N‰NØAØ¨BÕ/°ôó ÷ 
‰ûð	ú÷ �Yú÷# 
�ˆúsf   ÁE(Â&E<ÃG!Ã,%FÄ
G!ÄG	Ä7&G!Å(E9	Å<F	ÆGÆGÆ8G!ÇGÇG!ÇGÇG!Ç!G2	c                ó0   € V ^8„  d   QhR\         R\        /# )r
   r   r'   ro   )r   s   "r   r   r   ä   s   € ÷ ^ñ ^œsð ^¤tñ ^r   c                óz   € \        V 4      pVf   R# \        \        VRR4      ;'       g    \        VRR4      4      # )u£  True if *task_id*'s active env persists across turns.

The agent loop skips per-turn teardown for these (persistent docker,
daytona, modal, â€¦); non-persistent backends are torn down at end of turn
to prevent leakage, and the idle reaper handles the rest. Session-scoped
docker containers count as persistent HERE: their lifetime is the session
(removed by ``AIAgent.close()`` â†’ ``cleanup_vm`` and the idle reaper).
FÚ_session_scopedÚ_persistent)rw   r(   rg   )r   r&   s   & r   Úis_persistent_envrŽ   ä   s=   € ô ˜Ó
!€CØ
‚{ÙÜ”˜Ð.°Ó6×\Ð\¼'À#À}ÐV[Ó:\Ó]Ð]r   c            	     ó6  € ^ RI Hp  ^ RIHp V ! 4        ^ p\	        VP                  4       4       F  p \        V4       V^,          pK  	  V^ 8”  d   \        P                  RV4       V#   \         d$   p\        P                  RY4RR7        Rp?Kd  Rp?ii ; i)zBClean up ALL active environments (process exit). Use with caution.)Úkill_live_foreground_processes)rL   zError cleaning %s: %sT)Úexc_infoNzCleaned %d environments)Útools.environments.baser�   rQ   rL   r`   ra   Ú
cleanup_vmr:   r8   Úerrorr9   )r�   rL   Úcleanedr   r=   s        r   Úcleanup_all_environmentsr–   ó   sŠ   € åFÝ8á"Ô$Ø€GÜÐ,×1Ñ1Ó3Ö4ˆð	MÜ�wÔØ�q�LŠGñ 5ð �„{Ü�‰Ð-¨wÔ7Ø€Nøô ô 	MÜ�L‰LÐ0°'ÀtˆL×LÒLûð	Mús   ³A*Á*BÁ5BÂBc                ó0   € V ^8„  d   QhR\         R\        /# )r
   r   r$   ro   )r   s   "r   r   r     s   € ÷ ñ œð ¬dñ r   c               óZ   € \        V 4      p\        V 4       Vf   R# \        W VRR7       R# )u¢  Manually clean up a specific environment by task_id.

*force_remove* is forwarded to backends that accept it (currently only
``DockerEnvironment``). Default False matches session-lifecycle semantics:
callers (``AIAgent.close()`` on TUI/gateway session teardown, the per-turn
cleanup of non-persistent envs) must honor the user's persist-mode
preference â€” stopping the container here would break the "ONE long-lived
container shared across sessions" contract. Pass ``force_remove=True``
only for user-initiated teardown. The idle reaper calls ``env.cleanup()``
directly, so persist-mode idle envs are likewise no-op'd; only the orphan
reaper at next startup reclaims them.
Nz,Manually cleaned up environment for task: %s)r$   r3   )rS   rG   r?   )r   r$   r&   s   &$ r   r“   r“     s.   € ô ˜'Ó
"€CÜ˜'Ô"Ø
‚{ÙÜØ <Ø?÷r   c                ó>   € V ^8„  d   QhR\         \        ,          RR/# rA   ry   )r   s   "r   r   r     s   € ÷ ñ ¬´#­ð ¸4ñ r   c                óÌ  € ^ RI HpHpHpHp V! V 4      0pV '       d   VP                  V 4       . pT;_uu_ 4        V F>  pVP                  VR4      pVP                  VR4       Vf   K-  VP                  V4       K@  	  RRR4       V F/  p\        R4      ;_uu_ 4        VP                  4        RRR4       K1  	  R#   + '       g   i     LG; i  + '       g   i     KY  ; i)zˆDrop any cached env for *task_id* (and its collapsed key) after an
infrastructure failure, so later calls don't reuse a dead connection.)rL   rO   rP   rt   Nz&cleanup of degraded environment failed)
rQ   rL   rO   rP   rt   ÚaddrR   Úappendr   r*   )	r   rL   rO   rP   rt   ra   Úevictedr[   r&   s	   &        r   Ú_evict_environment_for_taskrž     s³   € ÷ó ñ ' wÓ/Ð0€DßØ�‰�ÔØ€Gß	ŠÛˆCØ&×*Ñ*¨3°Ó5ˆCØ×Ñ˜s DÔ)ØŒØ—‘˜sÖ#ñ	 ÷ 
ó ˆÜÐ<×=Õ=Ø�K‰KŒM÷ >Ñ=ó ÷ 
�ú÷ >×=Ð=ús   »+B?Á+B?Â CÂ?C	ÃC#)i,  rY   )Ú__doc__Úloggingr/   r‚   r^   Útypingr   r   r   r!   r   r   r   r"   r   Ú	getLoggerr8   r#   r2   r?   rG   rS   rm   rc   rw   r‰   rŽ   r–   r“   rž   © r   r   Ú<module>r¤      s¶   ðñó Û Û Û ß &Ñ &÷ñ õ
 .ð 
×	Ò	Ð0Ó	1€õð&¸T÷ ð&XÈDð Xð  cQð Xð  ej÷ Xõ"õ÷"',õT$õ
U÷?õD^òð&°U÷ ÷.r   