
    Fj              	       ^   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m	Z	 ddl
mZ ddlmZmZ ddlmZ  ej        e          Zg dZdaee         ed<    ej        d	          Zd
ee         dz  dee         fdZdedz  deeef         fdZdeeef         fdZ ej        d          ZdedefdZ defdZ!ddddde"dedz  dedz  de"fdZ#dedefdZ$dee         fdZ%g dZ&g dZ'de(dee         fd Z)dee         fd!Z*da+ee(         ed"<   d&d#Z, G d$ d%e          Z-dS )'zDocker execution environment for sandboxed command execution.

Security hardened (cap-drop ALL, no-new-privileges, PID limits),
configurable resource limits (CPU, memory, disk), and optional filesystem
persistence via bind mounts.
    N)Path)Optional)BaseEnvironment_popen_bash)_HERMES_PROVIDER_ENV_BLOCKLIST)z/usr/local/bin/dockerz/opt/homebrew/bin/dockerz6/Applications/Docker.app/Contents/Resources/bin/docker_docker_executablez^[A-Za-z_][A-Za-z0-9_]*$forward_envreturnc                    g }t                      }| pg D ]}t          |t                    st                              d|           3|                                }|sJt                              |          st                              d|           ||v r|                    |           |	                    |           |S )z?Return a deduplicated list of valid environment variable names.z0Ignoring non-string docker_forward_env entry: %rz-Ignoring invalid docker_forward_env entry: %r)
set
isinstancestrloggerwarningstrip_ENV_VAR_NAME_REmatchaddappend)r	   
normalizedseenitemkeys        8/usr/local/lib/hermes-agent/tools/environments/docker.py_normalize_forward_env_namesr   %   s    JUUD!r  $$$ 	NNMtTTTjjll 	%%c** 	NNJDQQQ$;;#    envc                 ^   | si S t          | t                    st                              d|            i S i }|                                 D ]\  }}t          |t
                    r,t                              |                                          st                              d|           b|                                }t          |t
                    sOt          |t          t          t          f          rt          |          }nt                              d||           |||<   |S )zValidate and normalize a docker_env dict to {str: str}.

    Filters out entries with invalid variable names or non-string values.
    zdocker_env is not a dict: %rz#Ignoring invalid docker_env key: %rz/Ignoring non-string docker_env value for %r: %r)r   dictr   r   itemsr   r   r   r   intfloatbool)r   r   r   values       r   _normalize_env_dictr%   >   s   
  	c4   5s;;;	!#Jiikk    
U#s## 	+;+A+A#))+++N+N 	NN@#FFFiikk%%% 	 %#ud!344 E

PRUW\]]]
3r   c                  L    	 ddl m}   |             pi S # t          $ r i cY S w xY w)zDLoad ~/.hermes/.env values without failing Docker command execution.r   load_env)hermes_cli.configr(   	Exceptionr'   s    r   _load_hermes_env_varsr+   \   sN    ......xzzR   			s    ##z[^A-Za-z0-9_.-]r$   c                     t          | t                    r| sdS t                              d|           }|dd         pd}|S )u  Coerce *value* into a Docker label-safe form (alnum + ``_.-``, ≤63 chars).

    Empty or all-invalid inputs collapse to ``"unknown"`` so the resulting
    label is always queryable. Used at container-create time; never round-trip
    a sanitized value back into application logic.
    unknown_N?   )r   r   _LABEL_VALUE_OK_REsub)r$   cleaneds     r   _sanitize_label_valuer3   l   sN     eS!!  y $$S%00Gcrcl'iGNr   c                  J    	 ddl m}   |             pdS # t          $ r Y dS w xY w)a   Return the active Hermes profile name, or ``"default"`` on any error.

    Resolved at container-create time so a single container is permanently
    tagged with the profile that created it. Profile switches inside the
    same process don't retroactively relabel running containers.
    r   get_active_profile_namedefault)hermes_cli.profilesr6   r*   r5   s    r   _get_active_profile_namer9   z   sO    ??????&&((5I5   yys    
""iX  )max_age_secondsprofile_filter
docker_exer:   r;   r<   c                    |pt                      pd}g d}|r'|                    ddt          |           g           	 t          j        |ddg|ddd	d	d
d          }n?# t          j        t          f$ r&}t                              d|           Y d}~dS d}~ww xY w|j	        dk    r:t                              d|j	        |j
                                                   dS d |j                                        D             }|sdS ddl}|j                            |j        j                  }	d}
|D ]}t%          ||          }||	|z
                                  }|| k     r4	 t          j        |dd|gd	d	d          }|j	        dk    r7|
dz  }
t                              d|dd         t+          |                     n;t                              d|dd         |j
                                                   # t          j        t          f$ r/}t                              d|dd         |           Y d}~d}~ww xY w|
S )u{  Remove stale hermes-tagged containers left behind by prior processes.

    Targets containers that match all of:

    * ``label=hermes-agent=1`` (created by this codebase)
    * ``status=exited`` (running containers are NEVER reaped — they may
      belong to a sibling Hermes process whose reuse path will pick them
      up; killing them would crash the sibling mid-command)
    * (optional) ``label=hermes-profile=<profile_filter>`` (sweep only the
      caller's profile by default; a hermes process in profile A must not
      tear down profile B's containers)
    * ``State.FinishedAt`` older than *max_age_seconds* ago (so a sibling
      process that just exited and is about to be replaced doesn't get
      its container yanked out from under it)

    Returns the number of containers removed. Best-effort: any failure
    (docker daemon unreachable, slow inspect, parse error) is logged at
    debug level and the function returns whatever it managed before the
    failure. Safe to call repeatedly; idempotent.

    Issue #20561 — this is the safety net for SIGKILL / OOM / crashed
    terminal exits that bypass the ``atexit`` cleanup hook. Without it,
    even with the cleanup-fix in the prior commit, a hard-killed Hermes
    process leaves its container behind permanently because there's no
    subsequent Hermes process scheduled to reuse that exact (task, profile)
    pair.
    docker)--filterlabel=hermes-agent=1r?   zstatus=exitedr?   label=hermes-profile=ps-a--formatz{{.ID}}T   Fcapture_outputtexttimeoutcheckz"orphan reaper docker ps failed: %sNr   z'orphan reaper docker ps returned %d: %sc                 ^    g | ]*}|                                 |                                 +S  r   .0lns     r   
<listcomp>z*reap_orphan_containers.<locals>.<listcomp>   s-    TTTBTRXXZZTTTr   rm-f   rG   rH   rI      z2Reaped orphan container %s (exited %d seconds ago)   docker rm -f %s failed: %sz%orphan reaper docker rm %s failed: %s)find_dockerextendr3   
subprocessrunTimeoutExpiredOSErrorr   debug
returncodestderrr   stdout
splitlinesdatetimenowtimezoneutc_container_finished_attotal_secondsinfor!   )r:   r;   r<   r>   filterslistingecandidate_idsrd   re   removedcidfinished_atageresults                  r   reap_orphan_containersrt      s   B 4;==4HFOOOG f
$c<QR`<a<a$c$cdeee.T4A'A:AyAdBe
 
 
 %w/   91===qqqqq Q5 4 4 6 6	
 	
 	
 qTT'.*C*C*E*ETTTM q
 OOO



 1 5
6
6CG O O,VS99[ //11  	O^tS)#$  F  A%%1HHc#hh   
 0Hfm1133   )73 	O 	O 	OLL@#crc(ANNNNNNNN	ONs1   "A$ $B :BB 0BH

I $I

Icontainer_idc                 j   	 t          j        | ddd|gdddd          }nH# t           j        t          f$ r/}t                              d|d	d
         |           Y d	}~d	S d	}~ww xY w|j        dk    rd	S |j                                        }|r|	                    d          rd	S dd	l
}|                    dd|          }|                    dd          }	 dd	l}|j                            |          S # t          $ r0}t                              d||d	d
         |           Y d	}~d	S d	}~ww xY w)u@  Parse ``docker inspect`` FinishedAt for *container_id*.

    Returns a timezone-aware datetime, or ``None`` if the field is missing,
    unparseable, or the zero-value ``0001-01-01T00:00:00Z`` Docker emits
    for never-finished containers. ``None`` means "don't reap" — the caller
    leaves the container alone.
    inspectrD   z{{.State.FinishedAt}}T
   FrF   z*orphan reaper docker inspect %s failed: %sNrW   r   z
0001-01-01z(\.\d{6})\d+z\1Zz+00:00z(could not parse FinishedAt %r for %s: %s)r[   r\   r]   r^   r   r_   r`   rb   r   
startswithrer1   replacerd   fromisoformat
ValueError)r<   ru   rs   rm   raw_rerd   s          r   rh   rh      s   J0GVdBe
 
 
 %w/   A<PSQSPSCTVWXXXttttt At
-



C #.... t 
''/5#
.
.C
++c8
$
$C ..s333   ?lSVTVSVFWYZ[[[ttttts,   ! A&$A!!A&C8 8
D2%D--D2c                  x   t           t           S t          j        d          } | r]t          j                            |           r>t          j        | t          j                  r| a t                              d|            | S t          j
        d          }|r|a |S t          j
        d          }|r|a t                              d|           |S t          D ]a}t          j                            |          r@t          j        |t          j                  r!|a t                              d|           |c S bdS )u  Locate the docker (or podman) CLI binary.

    Resolution order:
    1. ``HERMES_DOCKER_BINARY`` env var — explicit override (e.g. ``/usr/bin/podman``)
    2. ``docker`` on PATH via ``shutil.which``
    3. ``podman`` on PATH via ``shutil.which``
    4. Well-known macOS Docker Desktop install locations

    Returns the absolute path, or ``None`` if neither runtime can be found.
    NHERMES_DOCKER_BINARYz'Using HERMES_DOCKER_BINARY override: %sr>   podmanz%Using podman as container runtime: %sz%Found docker at non-PATH location: %s)r   osgetenvpathisfileaccessX_OKr   rj   shutilwhich_DOCKER_SEARCH_PATHS)overridefoundr   s      r   rY   rY     s7    %!! y/00H BGNN8,, 8RW1M1M %=xHHH L""E " L""E ";UCCC %  7>>$ 	BIdBG$<$< 	!%KK?FFFKKK4r   )z
--cap-dropALL	--cap-addDAC_OVERRIDEr   CHOWNr   FOWNERz--security-optzno-new-privilegesz--pids-limit256--tmpfsz/tmp:rw,nosuid,size=512mr   z#/var/tmp:rw,noexec,nosuid,size=256mr   z/run:rw,noexec,nosuid,size=64m)r   SETUIDr   SETGIDrun_as_host_userc                     | rt          t                    S t          t                    t          t                    z   S )zBReturn the security/cap/tmpfs args tailored to the privilege mode.)list_BASE_SECURITY_ARGS_PRIVDROP_CAP_ARGS)r   s    r   _build_security_argsr   Y  s6     )'(((#$$t,>'?'???r   c                      t          t          dd          } t          t          dd          }| |dS 	  |              d |             S # t          $ r Y dS w xY w)au  Return ``<uid>:<gid>`` for the current host user, or ``None`` on platforms
    where this is not meaningful (e.g. Windows without posix ids).

    We intentionally read ``os.getuid()``/``os.getgid()`` directly rather than
    going through ``getpass``/``pwd`` so this stays cheap and never raises on
    nameless UIDs (nss lookups can fail inside sandboxed launchers).
    getuidNgetgid:)getattrr   r*   )get_uidget_gids     r   _resolve_host_user_specr   `  s{     b(D))Gb(D))G'/t'))))ggii)))   tts   A 
AA_storage_opt_okc                     t                      } | s)t                              d           t          d          	 t	          j        | dgddd          }|j        dk    rHt                              d| |j        |j                                                   t          d	          dS # t          $ r- t                              d
| d           t          d          t          j
        $ r- t                              d| d           t          d          t          $ r t                              dd            w xY w)zBest-effort check that the docker CLI is available before use.

    Reuses ``find_docker()`` so this preflight stays consistent with the rest of
    the Docker backend, including known non-PATH Docker Desktop locations.
    zDocker backend selected but no docker executable was found in PATH or known install locations. Install Docker Desktop and ensure the CLI is available.z|Docker executable not found in PATH or known install locations. Install Docker and ensure the 'docker' command is available.versionT   rU   r   zIDocker backend selected but '%s version' failed (exit code %d, stderr=%s)zXDocker command is available but 'docker version' failed. Check your Docker installation.zVDocker backend selected but the resolved docker executable '%s' could not be executed.)exc_infozHDocker executable could not be executed. Check your Docker installation.zYDocker backend selected but '%s version' timed out. The Docker daemon may not be running.zHDocker daemon is not responding. Ensure Docker is running and try again.z4Unexpected error while checking Docker availability.N)rY   r   errorRuntimeErrorr[   r\   r`   ra   r   FileNotFoundErrorr]   r*   )r<   rs   s     r   _ensure_docker_availabler   u  s    J 	
 	
 	
 	

 K
 
 	

-#	
 
 
B !!LL,!##%%   2   "!7  	
 	
 	
	 	 	
 	
 	
 V
 
 	
 $ 	
 	
 	
4	 	 	
 	
 	
 V
 
 	
    B 	 	
 	
 	
 	s   B* *BEc            #       `    e Zd ZdZ	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d,d	ed
ededededededededee         dz  de	dz  dedededededef" fdZ
dee         fdZddddded eded!edz  dej        f
d"Zedefd#            Zd$ed%edeeeef                  fd&Zdd'd(efd)Zd-dedefd+Z xZS ).DockerEnvironmentu  Hardened Docker container execution with resource limits and persistence.

    Security: all capabilities dropped, no privilege escalation, PID limits,
    size-limited tmpfs for scratch dirs. The container itself is the security
    boundary — the filesystem inside is writable so agents can install packages
    (pip, npm, apt) as needed. Writable workspace via tmpfs or bind mounts.

    Persistence: when enabled, bind mounts preserve /workspace and /root
    across container restarts.
    /root<   r   Fr7   NTimagecwdrI   cpumemorydiskpersistent_filesystemtask_idvolumesr	   r   networkhost_cwdauto_mount_cwdr   
extra_argspersist_across_processesc                    |dk    rd}t                                          ||           || _        || _        || _        t          |
          | _        t          |          | _        d | _	        i | _
        t                              d|	            |	4t          |	t                    st                              d|	           g }	t!                       g }|dk    r$|                    dt%          |          g           |dk    r|                    d| d	g           |dk    rZt&          j        d
k    rJ|                                 r|                    dd| d	g           nt                              d           |s|                    d           ddlm} g }d}|	pg D ]}t          |t$                    st                              d|           5|                                }|sLd|v r|                    d|g           d|v rd}nt                              d| d           |r<t4          j                            t4          j                            |                    nd}|o0t=          |          o!t4          j                            |          o| }|r>|r<t4          j                            |          st                               d|            d | _!        d | _"        g }| j        r |            dz  |z  }t%          |dz            | _"        t5          j#        | j"        d           |                    d| j"         dg           |sS|sQt%          |dz            | _!        t5          j#        | j!        d           |                    d| j!         dg           n2|s|s|                    dd g           |                    g d!           |r't                              d"|            d| dg|}n|rt                               d#           	 dd$l$m%}m&}m'}  |            D ]}tQ          |d%                   }|)                                rt                              d&|           G|*                                st                              d'|           w|                    d|d%          d|d(          d)g           t                              d*|d%         |d(                     |            D ]} tQ          | d%                   }|)                                st                              d+|           G|                    d| d%          d| d(          d)g           t                              d,| d%         | d(                     |            D ]}!tQ          |!d%                   }|)                                st                              d-|           G|                    d|!d%          d|!d(          d)g           t                              d.|!d%         |!d(                    n2# tV          $ r%}"t                               d/|"           Y d }"~"nd }"~"ww xY wg }#tY          | j                  D ])}$|#                    d0|$ d1| j        |$          g           *g }%|rJt[                      }&|& d2|&g}%t                              d3|&           nt                              d4           t]          |ot=          |%                    }'t                              d5|            g }(|pg D ]H})t          |)t$                    st                              d6|)           3|(                    |)           I|'|%z   |z   |z   |z   |#z   |(z   }*t                              d7|*            t_                      pd| _0        d8tc          j2                    j3        d d9          }+ti          tk                                },ti          |          }-d:d;d:d<|- d:d=|, g}.d>|-|,d?| _
        d}/|r| 6                    |-|,          }0|0|0\  }1}2|1| _	        |2d@k    rw	 to          j8        | j0        dA|1gdddBdC           nT# tn          j9        tn          j:        f$ r6}"t                              dD|1d dE         |2|"           d | _	        Y d }"~"nd }"~"ww xY w| j	        r(t                              dF|1d dE         |-|,|2           d}/|/s| j0        dGdHdIdJ|+g|.dK||*|dLdM}3t                               dNdO;                    |3                      to          j8        |3dddPdC          }4|4j<                                        | _	        t                              dQ|+ dR| j	        d dE          dS           | =                                | _>        | ?                                 d S )TN~r   )r   rI   zDockerEnvironment volumes: z%docker_volumes config is not a list: r   z--cpusz--memorymdarwin--storage-optzsize=zDocker storage driver does not support per-container disk limits (requires overlay2 on XFS with pquota). Container will run without disk quota.z--network=none)get_sandbox_dirFz%Docker volume entry is not a string: r   z-vz:/workspaceTzDocker volume 'z' missing colon, skipping z>Skipping docker cwd mount: host_cwd is not a valid directory: r>   home)exist_okz:/root	workspacer   z/workspace:rw,exec,size=10g)r   z/home:rw,exec,size=1gr   z/root:rw,exec,size=1gz,Mounting configured host cwd to /workspace: zDSkipping docker cwd mount: /workspace already mounted by user config)get_credential_file_mountsget_skills_directory_mountget_cache_directory_mounts	host_pathug   Docker: skipping credential mount — source is a directory (likely Docker-in-Docker auto-creation): %su:   Docker: skipping credential mount — source not found: %scontainer_pathz:roz$Docker: mounting credential %s -> %su?   Docker: skipping skills mount — source is not a directory: %sz$Docker: mounting skills dir %s -> %su>   Docker: skipping cache mount — source is not a directory: %sz#Docker: mounting cache dir %s -> %sz1Docker: could not load credential file mounts: %s-e=z--userz)Docker: running container as host user %szdocker_run_as_host_user is enabled but this platform does not expose POSIX uid/gid; container will start as its image default user.zDocker volume_args: z/Ignoring non-string docker_extra_args entry: %rzDocker run_args: zhermes-   z--labelzhermes-agent=1zhermes-task-id=zhermes-profile=1)zhermes-agentzhermes-task-idzhermes-profilerunningstartrT   rF   u[   Failed to start existing container %s (state=%s): %s — falling back to a fresh container.rW   z:Reusing container %s (task=%s, profile=%s, prior state=%s)r\   z-dz--initz--namez-wsleepinfinityzStarting container:  x   zStarted container z ())@super__init___persistent_persist_across_processes_task_idr   _forward_envr%   _env_container_id_labelsr   rj   r   r   r   r   rZ   r   sysplatform_storage_opt_supportedr   tools.environments.baser   r   r   r   abspath
expanduserr#   isdirr_   _workspace_dir	_home_dirmakedirstools.credential_filesr   r   r   r   is_diris_filer*   sortedr   r   rY   _docker_exeuuiduuid4hexr3   r9   _find_reusable_containerr[   r\   CalledProcessErrorr]   joinrb   _build_init_env_args_init_env_argsinit_session)6selfr   r   rI   r   r   r   r   r   r   r	   r   r   r   r   r   r   r   resource_argsr   volume_argsworkspace_explicitly_mountedvolhost_cwd_absbind_host_cwdwritable_argssandboxr   r   r   mount_entrysrcskills_mountcache_mountrm   env_argsr   	user_args	user_specsecurity_argsvalidated_extraargall_run_argscontainer_nameprofile_name
task_label
label_argsreusedexistingru   staterun_cmdrs   	__class__s6                                                        r   r   zDockerEnvironment.__init__  s   ( #::CS'2220)A&8EE',,	,0');';;<<<z'4'@'@NNN7NNOOOG 	!""" 77  (CHH!5666A::  *lll!;<<<!8800**,, $$ot%GHHHHe    	3  !1222
 	<;;;;; ',$Mr 	Q 	QCc3'' NsNNOOO))++C czz""D#;/// C''370OOOOPPPPHPXrwrw'9'9('C'CDDDVX 1\""1l++1 10	 	  	fh 	frw}}\/J/J 	fLLdZbddeee-1(, 	%o''(2W<G 6!122DNK6666  ///"    ! )E &)'K*?&@&@#D/$????$$T0===&    ! )E $$<&       " " "   
  	aKKU|UUVVVL!=!=!=LLKK) 	aLL_```L	Q           :9;;  ;{344::<< 	 NNF  
 {{}} NNTVY   """;/TT+>N2OTTT$    :, 01    !; : < <  <455zz|| NNY   ""#K0VV<@P3QVVV$    : - !12     :9;;  ;{344zz|| NNX   """;/TT+>N2OTTT$    9, 01   "  	Q 	Q 	QLLLaPPPPPPPP	Q
 $)$$ 	? 	?COOTc#<#<DIcN#<#<=>>>>  "	 
	/11I$%y1	GSSSS*   --=-Q$y//RR8;88999 $" 	( 	(Cc3'' PRUVVV""3''''   	
   	 	666777 '==4H :4:<<#3BQB#799 --E-G-GHH*733
'555777

  (*
 
$ # 	"44ZNNH#&.#e%1"I%%2"!-wE+/!%$&"&     '9:;TU 2 2 2H("-ua  
 .2******2 % "KKT$SbS):|U   "F 	[ %.	 		
 	
 	 	 	 	 $	G LLC0A0ACCDDD^#  F "(!4!4!6!6DKKY^YYt?QRUSURU?VYYYZZZ
 #7799 	s1   0H#Y 
ZY>>Z9!b c,6,c''c,r
   c                    t          | j                  }t          | j                  }t                      }	 ddlm} t           |                      }n# t          $ r Y nw xY w||t          z
  z  }|rt                      ni }t          |          D ]4}t          j        |          }||                    |          }||||<   5g }	t          |          D ]$}|	                    d| d||          g           %|	S )zBuild -e KEY=VALUE args for injecting host env vars into init_session.

        These are used once during init_session() so that export -p captures
        them into the snapshot.  Subsequent execute() calls don't need -e flags.
        r   )get_all_passthroughNr   r   )r   r   r   r   tools.env_passthroughr  r*   r   r+   r   r   r   getrZ   )
r   exec_envexplicit_forward_keyspassthrough_keysr  forward_keys
hermes_envr   r$   argss
             r   r   z&DockerEnvironment._build_init_env_args  s>    $(	?? #D$5 6 6%(UU	AAAAAA"#6#6#8#899 	 	 	D	
 -0@Ca0ab0<D*,,,"
,'' 	& 	&CIcNNE}"s++  %(## 	: 	:CKK#777789999s   A 
A#"A#r   )loginrI   
stdin_data
cmd_stringr"  r#  c                Z   | j         s
J d            | j        dg}||                    d           |r|                    | j                   |                    | j         g           |r|                    ddd|g           n|                    dd|g           t          ||          S )z1Spawn a bash process inside the Docker container.zContainer not startedexecNz-ibashz-lz-c)r   r   r   rZ   r   r   )r   r$  r"  rI   r#  cmds         r   	_run_bashzDockerEnvironment._run_bash;  s     !::#:::!(!JJt  	,JJt*+++

D&'((( 	3JJdJ78888JJj12223
+++r   c                     t           t           S 	 t                      pd} t          j        | dddgddd          }|j                                                                        }|d	k    rd
a d
S t          j        | ddddgddd          }|j        dk    r8|j                                        }|rt          j        | d|gdd           da nd
a n# t          $ r d
a Y nw xY wt          
                    dt                      t           S )zCheck if Docker's storage driver supports --storage-opt size=.
        
        Only overlay2 on XFS with pquota supports per-container disk quotas.
        Ubuntu (and most distros) default to ext4, where this flag errors out.
        Nr>   rj   rD   z{{.Driver}}Trx   rU   overlay2Fcreater   zsize=1mzhello-worldrE   r   rR   r   rG   rI   z Docker --storage-opt support: %s)r   rY   r[   r\   rb   r   lowerr`   r*   r   r_   )r>   rs   driverproberu   s        r   r   z(DockerEnvironment._storage_opt_supportedR  sR    &""	$ ]].hF^];#$  F ]((**0022F##"'u N?I}M#$  E 1$$$|1133 CNFD,#?26C C C C"&"' 	$ 	$ 	$#OOO	$7IIIs   AC 1A"C C#"C#r  profile_labelc                    	 t          j        | j        dddddd| dd| ddgd	d	d
d          }n?# t           j        t          f$ r&}t
                              d|           Y d}~dS d}~ww xY w|j        dk    r:t
                              d|j        |j        	                                           dS d |j
                                        D             }|sdS d}d}|D ]`}|                    dd          }	t          |	          dk    r,|	d         |	d                                         }}
||
|f}|dk    r||
|f}a|p|S )u  Look for an existing container labeled for this (task, profile).

        Returns ``(container_id, state)`` on hit, ``None`` on miss / on any
        failure (including ``docker ps`` itself failing). State is one of the
        values Docker reports via ``{{.State}}`` — e.g. ``running``, ``exited``,
        ``created``, ``paused``, ``restarting``, ``dead``. The caller decides
        whether the state warrants ``docker start`` before reuse.

        Restricted to the docker-stored label set this class creates; never
        matches containers that happened to be named ``hermes-*`` but were
        started by some other tool.
        rB   rC   r?   r@   zlabel=hermes-task-id=rA   rD   z{{.ID}}	{{.State}}Trx   FrF   u;   docker ps probe failed: %s — will start a fresh containerNr   u@   docker ps probe returned %d: %s — will start a fresh containerc                 ^    g | ]*}|                                 |                                 +S rL   rM   rN   s     r   rQ   z>DockerEnvironment._find_reusable_container.<locals>.<listcomp>  s-    OOOBHHJJOOOOr   	rV      r   )r[   r\   r   r]   r^   r   r_   r`   ra   r   rb   rc   splitlenr.  )r   r  r1  rs   rm   linesr   firstrP   partsrp   r  s               r   r   z*DockerEnvironment._find_reusable_containerz  s   	^$dD 6 D
 D D G G G 5  $  FF )73 	 	 	LLVXYZZZ44444	 !!LLR!6=#6#6#8#8   4OOfm&>&>&@&@OOO 	4  	' 	'BHHT1%%E5zzQq58>>#3#3C}e	!!go,%s   /2 A.A))A.)force_remover;  c                  	
 | j         s2| j        s)| j        | j        fD ]}|rt	          j        |d           dS |rd
d	n| j        r	d| _         dS d
d	| j        dd         d
	
fd}ddl}|	                    |dd 	          }|
                                 || _        d| _         	r0| j        s+| j        | j        fD ]}|rt	          j        |d           dS dS dS )u}	  Tear down the container according to persist mode and *force_remove*.

        Persist-mode (``persist_across_processes=True``, the default) leaves the
        container **running** untouched. The docs promise "ONE long-lived
        container shared across sessions" and stopping it on every Hermes exit
        breaks that promise:

        * Background processes inside the container (``npm run dev``, watchers,
          long-running pytest) get killed every time the user runs ``/quit``.
        * Every reuse requires ``docker start`` + waiting for the container to
          come back up, adding 1–2s to the first tool call of the new session.
        * The user-visible difference between "ONE long-lived container" and
          "a new container that happens to share state" is exactly this:
          processes survive in the former, die in the latter.

        Resource reclamation for the persist-mode case lives in the
        ``reap_orphan_containers()`` path (see issue #20561 commit 3): if no
        Hermes process touches a labeled container for ``2 × lifetime_seconds``
        it gets ``docker rm -f``'d at the next Hermes startup. That covers the
        SIGKILL / OOM / abandoned-laptop cases without us needing to stop the
        container on every graceful exit.

        Opt-out mode (``persist_across_processes=False``) still does
        ``docker stop`` + ``docker rm -f`` on every cleanup, matching the
        pre-PR behavior for users who explicitly want per-process isolation.

        ``force_remove=True`` overrides persist mode and always tears the
        container down (``docker stop`` + ``docker rm -f``). This is the
        explicit-teardown path for ``/reset``, ``cleanup_vm(task_id)``-driven
        resets, or any caller that wants a guaranteed fresh container on next
        ``DockerEnvironment(task_id=...)``. No current caller passes
        ``force_remove=True``; the parameter is here so the explicit-teardown
        semantics can be wired up later without changing this method's
        signature.

        Cleanup runs on a daemon thread with bounded ``subprocess.run`` calls
        (not the racy ``Popen(... &)`` pattern from before PR #33645). The
        atexit hook in ``tools/terminal_tool.py`` waits up to 15s for the
        thread to finish before the interpreter exits, so ``docker stop`` /
        ``docker rm`` actually completes when we do trigger it.
        T)ignore_errorsNrW   r
   c                     r]	 t          j        dddgdd           n?# t           j        t          f$ r&} t                              d|            Y d } ~ nd } ~ ww xY wr^	 t          j        dd	gdd           d S # t           j        t          f$ r'} t                              d
|            Y d } ~ d S d } ~ ww xY wd S )Nstopz-t10TrT   r-  z%docker stop %s timed out / failed: %srR   rS   rX   )r[   r\   r]   r^   r   r   )rm   ru   r<   log_idshould_removeshould_stops    r   _do_cleanupz.DockerEnvironment.cleanup.<locals>._do_cleanup  s<    WWN#VT4F'+R     #17; W W WNN#JFTUVVVVVVVVW LLN#T4>'+R      #17; L L LNN#?KKKKKKKKKLL Ls,   " AAA$B B>B99B>r   zhermes-cleanup-)targetdaemonnamer
   N)r   r   r   r   r   rmtreer   r   	threadingThreadr   _cleanup_thread)r   r;  drD  rJ  tru   r<   rA  rB  rC  s         @@@@@r   cleanupzDockerEnvironment.cleanup  s   T ) 	 # =-t~> = =A =at<<<<F  	!K MM+ 	! "&DFK M %
crc"	L 	L 	L 	L 	L 	L 	L 	L 	L 	L0 	KC]U[C]C]^^				 !
  	9!1 	9)4>: 9 9 9M!48888	9 	9 	9 	99 9r         >@c                     t          | dd          }||                                sdS |                    |           |                                 S )u  Block up to *timeout* seconds for the cleanup worker thread.

        Returns ``True`` if the thread finished (or no thread was started),
        ``False`` on timeout. The atexit hook in terminal_tool.py calls this
        on every active environment so docker stop/rm actually completes
        before the Python process exits — without this, ``hermes /quit``
        races the interpreter shutdown and leaves stopped containers behind.
        rL  NT)rI   )r   is_aliver   )r   rI   threads      r   wait_for_cleanupz"DockerEnvironment.wait_for_cleanup(  sU     0$77>!2!2>4G$$$??$$$$r   )r   r   r   r   r   Fr7   NNNTNFFNT)rP  )__name__
__module____qualname____doc__r   r!   r"   r#   r   r   r   r   r[   Popenr)  staticmethodr   r   tupler   rO  rT  __classcell__)r  s   @r   r   r     s       	 	 &+ (,$!&)-%V VV V 	V
 V V V  $V V V #Y%V D[V V V V  !V" #V$ #'%V V V V V Vp
d3i    @ ;@!$+/, , ,C ,4 ,,!Dj,4>4D, , , ,. %D % % % \%N7 3 7 s 7 xX]^acf^fXgOh 7  7  7  7 r /4 s9 s9 s9t s9 s9 s9 s9j% % % % % % % % % % %r   r   rH  ).rX  loggingr   r{   r   r[   r   r   pathlibr   typingr   r   r   r   tools.environments.localr   	getLoggerrU  r   r   r   r   __annotations__compiler   r   r   r   r%   r+   r0   r3   r9   r!   rt   rh   rY   r   r   r#   r   r   r   r   r   rL   r   r   <module>rd     sC      				 				      



              @ @ @ @ @ @ @ @ C C C C C C		8	$	$    %) HSM ( ( (2:9:: d3i$.> 49    2TD[ T#s(^    <tCH~      RZ 233      #    " !%!	Y Y YY $JY d
	Y
 	Y Y Y Yxs #    D*Xc] * * * *x
 
 
     @4 @DI @ @ @ @#    $ #'$ & & &? ? ? ?D~	% ~	% ~	% ~	% ~	% ~	% ~	% ~	% ~	% ~	%r   