
    Jj              	          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	m
Z
 ddlmZ ddlmZmZ ddlmZ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(dZ)dZ*d Z+g d!Z,d/d#e-d$e-dee         fd%Z.ded&ede-fd'Z/dee         fd(Z0da1ee-         ed)<   da2ee-         ed*<   d&ede-fd+Z3d0d,Z4 G d- d.e          Z5dS )1zDocker 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_is_hermes_internal_secret)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        ?/home/rurouni/.hermes/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&   B   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_valuer4   p   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.profilesr7   r+   r6   s    r   _get_active_profile_namer:   ~   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t          j                  }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t          j                  }|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checkstdinz"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   rH   rI   rJ   rL      z2Reaped orphan container %s (exited %d seconds ago)   docker rm -f %s failed: %sz%orphan reaper docker rm %s failed: %s)find_dockerextendr4   
subprocessrunDEVNULL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_idsrg   rh   removedcidfinished_atageresults                  r   reap_orphan_containersrw      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+;B$H  I%6$I  I%container_idc                    	 t          j        | ddd|gddddt           j                  }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.
    inspectrE   z{{.State.FinishedAt}}T
   FrG   z*orphan reaper docker inspect %s failed: %sNrY   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`   ra   r   rb   rc   re   r   
startswithrer2   replacerg   fromisoformat
ValueError)r=   rx   rv   rp   raw_rerg   s          r   rk   rk      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-   ), A1$A,,A1%D 
D=%D88D=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   rm   shutilwhich_DOCKER_SEARCH_PATHS)overridefoundr   s      r   r[   r[     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-privileges--tmpfsz/tmp:rw,nosuid,size=512mr   z#/var/tmp:rw,noexec,nosuid,size=256m256)r   z/run:rw,noexec,nosuid,size=64m)r   z/run:rw,exec,nosuid,size=64m)r   SETUIDr   SETGIDFrun_as_host_userrun_execc                     t          |rt          nt                    }t          t                    |z   }| r|S |t          t                    z   S )aI  Return the security/cap/tmpfs args tailored to the privilege mode.

    ``run_exec`` mounts ``/run`` with ``exec`` instead of the hardened
    ``noexec`` default. This is required for s6-overlay images whose ``/init``
    entrypoint execs ``/run/s6/basedir/bin/init`` during startup; see
    ``_image_uses_init_entrypoint``.
    )list_RUN_TMPFS_EXEC_RUN_TMPFS_NOEXEC_BASE_SECURITY_ARGS_PRIVDROP_CAP_ARGS)r   r   	run_tmpfsargss       r   _build_security_argsr   o  sO     G__6GHHI#$$y0D $)****r   imagec                    	 t          j        | dd|ddgdddt           j                  }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 |j
        pd	                                }|r|dk    rd
S 	 t          j        |          }n# t          t          f$ r Y d
S w xY wt          |t                     r|g}t          |t"                    r|sd
S t!          |d                   	                                }|dv S )a  Return True if ``image``'s entrypoint is the s6-overlay ``/init``.

    Such images (e.g. anything built on ``s6-overlay``, including
    ``hermes-agent:latest``) already provide their own PID-1 init and execute
    ``/run/s6/basedir/bin/init`` during stage0 startup. They are incompatible
    with Docker's ``--init`` (two competing PID-1 inits) and with a ``noexec``
    ``/run`` mount. Detection is best-effort: on any inspection failure we
    return False and keep the hardened defaults.
    r   rz   rE   z{{json .Config.Entrypoint}}TrF   rW   z/Docker: could not inspect entrypoint for %s: %sNFr   z4Docker: image inspect for %s returned %d (stderr=%s) null)z/initz&/package/admin/s6-overlay/command/init)r]   r^   r_   SubprocessErrorra   r   rb   rc   rd   r   re   jsonloadsr   	TypeErrorr   r   r   )r=   r   rv   rp   r   
entrypointfirsts          r   _image_uses_init_entrypointr   ~  s   )U68$
 
 
 &0   FqQQQuuuuu A 	B6$fm&9&9&;&;	
 	
 	
 u=B
%
%
'
'C #--uZ__

	"   uu*c"" " \
j$'' z u
1$$&&EGGGs'   ), A)A$$A)C- -DD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_ok_cgroup_limits_okc                    t           t           S t                      }|r| sda dS 	 t          j        |ddddddd	d
| ddgdddt          j                  }|j        dk    a t           s<t                              d|j        pd	                                dd                    n4# t          $ r'}da t                              d|           Y d}~nd}~ww xY wt           S )u/  Probe whether cgroup resource limits work in this environment.

    Tests ``--cpus``, ``--memory`` and ``--pids-limit`` together by spawning
    a throwaway container from *image* (the same sandbox image we are about
    to use for real, so no extra pull and no dependency on a public
    registry). The container runs ``sleep 0`` — sleep is guaranteed to be
    present because the sandbox itself uses ``sleep 2h`` as its long-lived
    entrypoint.

    On hosts where the corresponding cgroup controllers are not delegated
    to this process (typical inside unprivileged LXCs and some rootless
    setups) these flags cause every container start to fail with ``OCI
    runtime error`` / exit 126. The probe runs once per process and the
    result — which is host-wide, not image-specific — is cached.
    NFr^   z--rm--cpusz0.5--memory64m--pids-limit32sleep0T<   rW   r   zCgroup resource limits (--cpus/--memory/--pids-limit) not available in this environment. Containers will run without CPU, memory or PID limits. To enable, delegate the cpu, memory and pids cgroup controllers to this container. Probe stderr: %sr   i  z8Cgroup limit probe failed; disabling resource limits: %s)r   r[   r]   r^   r_   rc   r   r   rd   r   r+   )r   r=   rv   rp   s       r   _cgroup_limits_availabler     s-   " $  J U !uVuj%GS"  $
 
 
 #-2  	NN#
 $"++--dsd3    V V V!QSTUUUUUUUUV s   A=B$ $
C.CCc                     t                      } | s)t                              d           t          d          	 t	          j        | dgdddt          j                  }|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   rW   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)r[   r   errorRuntimeErrorr]   r^   r_   rc   rd   r   FileNotFoundErrorr`   r+   )r=   rv   s     r   _ensure_docker_availabler     s    J 	
 	
 	
 	

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

.#$
 
 
D !!LL,!##%%   2   "!7  	
 	
 	
	 	 	
 	
 	
 V
 
 	
 $ 	
 	
 	
4	 	 	
 	
 	
 V
 
 	
    B 	 	
 	
 	
 	s   %B5 5BEc            #           e Zd ZdZ	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d5d	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d#Zd$edefd%Zdefd&Zd6d(ed
ede	f fd)Zedefd*            Zd+edee         fd,Zd-ed.edeeeef                  fd/Zdd0d1efd2Zd7dedefd4Z xZS )8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.
    /rootr   r   Fr8   NTr   cwdrJ   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 | _
        d| _        d| _        d| _        g | _        t                              d|	            |	4t#          |	t$                    st                              d|	           g }	t)                       g }|dk    r3t+          |          r$|                    d	t/          |          g           |dk    r)t+          |          r|                    d
| dg           t+          |          r|                    dt0          g           |dk    rZt2          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<t@          j!        "                    t@          j!        #                    |                    nd}|o0tI          |          o!t@          j!        %                    |          o| }|r>|r<t@          j!        %                    |          st          &                    d|            d | _'        d | _(        g }| j        r |            dz  |z  }t/          |dz            | _(        tA          j)        | j(        d           |                    d| j(         dg           |sS|sQt/          |dz            | _'        tA          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 ]}t]          |d&                   }|/                                rt                              d'|           G|0                                st                              d(|           w|                    d|d&          d|d)          d*g           t                              d+|d&         |d)                     |            D ]} t]          | d&                   }|/                                st                              d,|           G|                    d| d&          d| d)          d*g           t                              d-| d&         | d)                     |            D ]}!t]          |!d&                   }|/                                st                              d.|           G|                    d|!d&          d|!d)          d*g           t                              d/|!d&         |!d)                    n2# tb          $ r%}"t          &                    d0|"           Y d }"~"nd }"~"ww xY wg }#te          | j                  D ])}$|#                    d1|$ d2| j        |$          g           *g }%|rJtg                      }&|& d3|&g}%t                              d4|&           nt                              d5           ti                      pd| _5        tm          | j5        |          }'|'rt                              d6|           to          |otI          |%          |'7          }(t                              d8|            g })|pg D ]H}*t#          |*t.                    st                              d9|*           3|)                    |*           I|(|%z   |z   |z   |z   |#z   |)z   }+t                              d:|+            d;tq          j9                    j:        d d<          },tw          ty                                }-tw          |          }.d=d>d=d?|. d=d@|- g}/|| _        |,| _        |'| _        |+| _        dA|.|-dB| _
        d}0|r| =                    |.|-          }1|1|1\  }2}3d}4d }5|s| >                    |2          }5|5dCk    }4|4rt                              dD|2d dE         |5pdF|.|-           	 t          j@        | j5        dGdH|2gdddIdt~          jA        J           nG# t~          jB        t          f$ r.}"t                              dK|2d dE         |"           Y d }"~"nd }"~"ww xY wd }1|1|1\  }2}3|2| _	        |3dLk    r	 t          j@        | j5        dM|2gdddIdt~          jA        J           nT# t~          jD        t~          jB        f$ r6}"t                              dN|2d dE         |3|"           d | _	        Y d }"~"nd }"~"ww xY w| j	        r(t                              dO|2d dE         |.|-|3           d}0|0s2|'rg ndPg}6| j5        dQdRg|6dS|,|/dT||+|dUdV}7t          &                    dWdXE                    |7                      	 t          j@        |7dddYdt~          jA        J          }8nk# t~          jD        t~          jB        f$ rM}"t                              dZ|,|"           t          j@        | j5        dGdH|,gdd[t~          jA        \            d }"~"ww xY w|8jF                                        | _	        t                              d]|, d^| j	        d dE          d_           | G                                | _H        | I                                 d S )`N~r   )r   rJ   r   FzDockerEnvironment volumes: z%docker_volumes config is not a list: r   r   r   mr   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_dirz%Docker volume entry is not a string: r   z-vz:/workspaceTzDocker volume 'z' missing colon, skippingz>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.ug   Docker: image %s uses /init (s6-overlay) as entrypoint — skipping --init and mounting /run with exec.)r   zDocker volume_args: z/Ignoring non-string docker_extra_args entry: %rzDocker run_args: hermes-   --labelzhermes-agent=1zhermes-task-id=zhermes-profile=1)zhermes-agenthermes-task-idhermes-profilenoneu   Existing container %s has NetworkMode=%s but docker_network=false requests an air-gapped container — removing it and starting fresh (task=%s, profile=%s).rY   r.   rT   rU   rV   rG   z,Failed to remove mismatched container %s: %srunningstartu[   Failed to start existing container %s (state=%s): %s — falling back to a fresh container.z:Reusing container %s (task=%s, profile=%s, prior state=%s)--initr^   -d--name-wr   infinityzStarting container:  x   z<docker run failed for %s, cleaning up orphaned container: %sr{   rH   rJ   rL   zStarted container z ())Jsuper__init___persistent_persist_across_processes_task_idr   _forward_envr&   _env_container_id_labels_image_container_name_image_uses_s6_init_all_run_argsr   rm   r   r   r   r   r   r\   r   _DEFAULT_PIDS_LIMITsysplatform_storage_opt_supportedr   tools.environments.baser   r   r   r   abspath
expanduserr$   isdirrb   _workspace_dir	_home_dirmakedirstools.credential_filesr   r   r   r   is_diris_filer+   sortedr   r[   _docker_exer   r   uuiduuid4hexr4   r:   _find_reusable_container_container_network_moder]   r^   r_   r`   ra   CalledProcessErrorjoinre   _build_init_env_args_init_env_argsinit_session):selfr   r   rJ   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_mountrp   env_argsr   	user_args	user_specimage_uses_s6_initsecurity_argsvalidated_extraargall_run_argscontainer_nameprofile_name
task_label
label_argsreusedexistingrx   statemode_mismatchactual_mode	init_argsrun_cmdrv   	__class__s:                                                            r   r   zDockerEnvironment.__init__D  s_   ( #::CS'2220)A&8EE',,	,0')$&). (*;';;<<<z'4'@'@NNN7NNOOOG 	!""" 77/667  (CHH!5666A::2599:  *lll!;<<<#E** 	H  .2E!FGGG!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*   '==4H 99I5QQ 	KK?  
 -0i'
 
 

 	8;88999 $" 	( 	(Cc3'' PRUVVV""3''''   	
   	 	666777 :4:<<#3BQB#799 --E-G-GHH*733
'555777

 -#5 )  (*
 
$ # B	"44ZNNH#&.#e !&" :"&">">|"L"LK$/6$9M  $NN1 %SbS);+C)"L  
m"!-tT<H+/!%$&"'","4     '5w? m m m'UWcdgegdgWhjkllllllllm#H#&.#e%1"I%%2"!-wE+/!%$&"&","4     '9:;TU 2 2 2H("-ua  
 .2******2 % "KKT$SbS):|U   "F ,	[ 1@xjI %		 	 )	 		
 	
 	 	 	 	 $	G LLC0A0ACCDDD##'$,   1:3LM    R"A   %tT>B#'$,   
 #$ "(!4!4!6!6DKKY^YYt?QRUSURU?VYYYZZZ
 #7799 	sb   H#Z9 9
[([##[(--f g1$gg9,h& &i7,i22i7$l' 'nAn

nr   c                    t          | j                  }t          | j                  }t                      }	 ddlm} t           |                      }n# t          $ r Y nw xY wd |D             }||t          z
  z  }|rt                      ni }t          |          D ]4}t          j        |          }	|	s|                    |          }	|	r|	||<   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_passthroughc                 0    h | ]}t          |          |S rN   )r   )rQ   ks     r   	<setcomp>z9DockerEnvironment._build_init_env_args.<locals>.<setcomp>  s6     
 
 
/I!/L/L

 
 
r   r   r   )r    r  r   r  tools.env_passthroughrG  r+   r   r,   r  r   r   getr\   )r%  exec_envexplicit_forward_keyspassthrough_keysrG  _implicit_forwardforward_keys
hermes_envr   r%   r   s              r   r"  z&DockerEnvironment._build_init_env_args  sb    $(	?? #D$5 6 6%(UU	AAAAAA"#6#6#8#899 	 	 	D	
 
'
 
 
 -0ADb0bc0<D*,,,"
,'' 	& 	&CIcNNE ,"s++ & %(## 	: 	:CKK#777789999s   A 
A#"A#r   )loginrJ   
stdin_data
cmd_stringrS  rT  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   r\   r#  r   )r%  rU  rS  rJ   rT  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   )zNo such containerzis not runningzno such containeroutputc                 D    t          fd| j        D                       S )zCReturn True if the output indicates the container no longer exists.c              3       K   | ]}|v V  	d S )NrN   )rQ   pr[  s     r   	<genexpr>z7DockerEnvironment._is_container_gone.<locals>.<genexpr>D  s'      DD11;DDDDDDr   )any_NO_CONTAINER_PATTERNS)r%  r[  s    `r   _is_container_gonez$DockerEnvironment._is_container_goneB  s)    DDDD(CDDDDDDr   c                    | j         pddd         }t                              d|           d| _         | j                            dd          }| j                            dd          }|                     ||          }||\  }}|dk    r+|| _         t                              d|dd                    n	 t          j        | j	        d	|gd
d
dd
t          j
                   || _         t                              d|dd                    nL# t          j        t          j        f$ r.}t                              d|dd         |           Y d}~nd}~ww xY w| j         s| j        st                              d           dS 	 ddl}d|                                j        dd          }	| j        rg ndg}
g }| j                                        D ]!\  }}|                    d| d| g           "| j	        ddg|
d|	|d| j        | j        | j        dd}t          j        |d
d
dd
t          j
                  }|j                                        | _         |	| _        t                              d|	| j         dd                    nJ# t          j        t          j        t4          f$ r&}t                              d|           Y d}~dS d}~ww xY w	 d| _        |                                  n3# t:          $ r&}t                              d |           Y d}~dS d}~ww xY wt                              d!| j         pddd                    d
S )"a9  Recreate the container after it was removed out-of-band.

        Tries label-based reuse first; if no existing container is found,
        starts a fresh one with the same image and run-args.  Returns True
        on success, False if recreation fails (caller should surface the
        original error).
        r   NrY   u7   Container %s appears to be gone — attempting recoveryr   r   r   z&Recovery: reusing running container %sr   TrV   rG   z Recovery: restarted container %sz*Recovery: failed to start container %s: %sz8Recovery: no saved image name, cannot recreate containerFr   r   r   r   r   r   r^   r   r   r   r   r   r   z)Recovery: created fresh container %s (%s)z,Recovery: failed to create new container: %sz2Recovery: init_session failed in new container: %su(   Recovery successful — new container %s)r  r   r   r  rL  r  rm   r]   r^   r  r_   r   r`   r  r   r  r  r  r	  r!   r\   r   r
  re   r   r  ra   _snapshot_readyr$  r+   )r%  old_idr<  profile_labelr?  rs   r@  rp   _uuidnew_namerC  r=  rI  vrD  rv   s                   r   _recreate_containerz%DockerEnvironment._recreate_containerF  s&    $*CRC0Ev	
 	
 	
 " \%%&6;;
(()92>>00]KK!JC	!!%("Dc#2#hOOOO	^N)7C8'+$$(0   
 *-D&KK BCHMMMM"5z7PQ ^ ^ ^NN#OQTUXVXUXQY[\]]]]]]]]^ ! !	; WXXXu$$$$<U[[]]%6rr%:<<"&":JBB
	
 L..00 ? ?DAq%%yQ****&=>>>>$eT		 	 '	  		
 	
 (	 '	 K	 	 (	 $DtSPT$,   &,]%8%8%:%:"'/$?d0"5    1:3LgV   KQOOOuuuuu
	#(D  	 	 	LLMqQQQ55555	 	>ASAYWY[^\^[^@_```tsJ   9AD E+$EEDJ !K :KK $L   
L0
L++L0r   commandc                 0    t                      j        ||fi |}|                    dd          dk    r_|                     |                    dd                    r6| j        r/|                                 r t                      j        ||fi |}|S )a
  Execute a command, auto-recovering from dead containers.

        If the container was removed out-of-band (idle reaper, docker prune,
        OOM kill, daemon restart), detect the error and recreate the container
        transparently before retrying once.
        rc   r   r[  r   )r   executerL  rb  r  rj  )r%  rk  r   kwargsrv   rE  s        r   rm  zDockerEnvironment.execute  s     !#8888JJ|Q''1,,''

8R(@(@AA -. - '')) A(#@@@@r   c                  \   t           t           S 	 t                      pd} t          j        | dddgdddt          j                  }|j                                                                        }|d	k    rd
a d
S t          j        | ddddgdddt          j                  }|j        dk    rC|j                                        }|r%t          j        | d|gddt          j                   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?   rm   rE   z{{.Driver}}Tr{   rW   overlay2Fcreater   zsize=1mzhello-worldrF   r   rT   r   r   z Docker --storage-opt support: %s)r   r[   r]   r^   r_   re   r   lowerrc   r+   r   rb   )r?   rv   driverproberx   s        r   r  z(DockerEnvironment._storage_opt_supported  sa    &""	$ ]].hF^];#$ (  F
 ]((**0022F##"'u N?I}M#$ (  E
 1$$$|1133 =NFD,#?26)3);= = = = #'"' 	$ 	$ 	$#OOO	$7IIIs   A*C5 <A8C5 5DDrx   c                    	 t          j        | j        ddd|gddddt           j                  }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 |j        
                                }|pd	S )a  Return the container's ``HostConfig.NetworkMode`` (e.g. ``bridge``,
        ``none``, ``host``), or ``None`` when inspection fails.

        Used by the reuse path to make sure a persisted container's network
        mode still matches the operator's ``docker_network`` setting; callers
        treat ``None`` (unknown) as a mismatch when lockdown was requested,
        so a failed inspect fails closed rather than open.
        rz   rE   z{{.HostConfig.NetworkMode}}Tr{   FrG   z%docker inspect NetworkMode failed: %sNr   z*docker inspect NetworkMode returned %d: %s)r]   r^   r  r_   r`   ra   r   rb   rc   rd   r   re   )r%  rx   rv   rp   modes        r   r  z)DockerEnvironment._container_network_mode  s    	^$i = 
  $ (  FF )73 	 	 	LL@!DDD44444	 !!LL<!6=#6#6#8#8   4}""$$|ts   .1 A-A((A-r<  rf  c                    	 t          j        | j        dddddd| dd| ddgd	d	d
dt           j                  }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.
        rC   rD   r@   rA   zlabel=hermes-task-id=rB   rE   z{{.ID}}	{{.State}}Tr{   FrG   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 rN   rO   rP   s     r   rS   z>DockerEnvironment._find_reusable_container.<locals>.<listcomp>  s-    OOOBHHJJOOOOr   	rX      r   )r]   r^   r  r_   r`   ra   r   rb   rc   rd   r   re   rf   splitlenrr  )r%  r<  rf  rv   rp   linesr   r   rR   partsrs   r@  s               r   r  z*DockerEnvironment._find_reusable_container  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   := A9A44A9)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_errorsNrY   r   c                     rh	 t          j        dddgddt           j                   n?# t           j        t          f$ r&} t
                              d|            Y d } ~ nd } ~ ww xY wri	 t          j        dd	gddt           j                   d S # t           j        t          f$ r'} t
                              d
|            Y d } ~ d S d } ~ ww xY wd S )Nstopz-t10TrV   r   z%docker stop %s timed out / failed: %srT   rU   rZ   )r]   r^   r_   r`   ra   r   r   )rp   rx   r=   log_idshould_removeshould_stops    r   _do_cleanupz.DockerEnvironment.cleanup.<locals>._do_cleanup  sH    WWN#VT4F'+R(0    
 #17; W W WNN#JFTUVVVVVVVVW LLN#T4>'+R(0     
 #17; L L LNN#?KKKKKKKKKLL Ls-   '- A)A$$A)/&B C-CCr   zhermes-cleanup-)targetdaemonnamer   N)r  r   r  r  r   rmtreer  r  	threadingThreadr   _cleanup_thread)r%  r  dr  r  trx   r=   r  r  r  s         @@@@@r   cleanupzDockerEnvironment.cleanup0  s   T ) 	 # =-t~> = =A =at<<<<F  	!K MM+ 	! "&DFK M %
crc"	L 	L 	L 	L 	L 	L 	L 	L 	L 	L4 	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.
        r  NT)rJ   )r   is_aliver!  )r%  rJ   threads      r   wait_for_cleanupz"DockerEnvironment.wait_for_cleanup  sU     0$77>!2!2>4G$$$??$$$$r   )r   r   r   r   r   Fr8   NNNTNFFNT)r   )r  )__name__
__module____qualname____doc__r   r"   r#   r$   r   r    r   r"  r]   PopenrZ  ra  rb  rj  rm  staticmethodr  r   r  tupler  r  r  __classcell__)rE  s   @r   r   r   8  s0       	 	 &+ (,$!&)-%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 # # # #J ;@!$+/, , ,C ,4 ,,!Dj,4>4D, , , ,6E E E E E EPT P P P Pd s        " (D ( ( ( \(T C  HSM        D8 3 8 s 8 xX]^acf^fXgOh 8  8  8  8 t /4 u9 u9 u9t u9 u9 u9 u9n% % % % % % % % % % %r   r   )Fr  )6r  r   loggingr   r~   r   r]   r  r  pathlibr   typingr   r  r   r   tools.environments.localr   r   	getLoggerr  r   r   r	   r   __annotations__compiler   r   r   r    r&   r,   r1   r4   r:   r"   rw   rk   r[   r   r  r   r   r   r$   r   r   r   r   r   r   r   r   rN   r   r   <module>r     s       				 				      



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