
    )j1              	          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  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)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'Z.da/ee+         ed(<   d-d)Z0 G d* d+e          Z1d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        >/home/ubuntu/.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%   ?   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   m   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   {   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   rG   rH   rI   rK      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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_idsrf   rg   removedcidfinished_atageresults                  r   reap_orphan_containersrv      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.
    inspectrD   z{{.State.FinishedAt}}T
   FrF   z*orphan reaper docker inspect %s failed: %sNrX   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   ra   rb   rd   r   
startswithrer1   replacerf   fromisoformat
ValueError)r<   rw   ru   ro   raw_rerf   s          r   rj   rj      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   rl   shutilwhich_DOCKER_SEARCH_PATHS)overridefoundr   s      r   rZ   rZ     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=256m)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   c  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   ry   rD   z{{json .Config.Entrypoint}}TrE   rV   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^   SubprocessErrorr`   r   ra   rb   rc   r   rd   jsonloadsr   	TypeErrorr   r   r   )r<   r   ru   ro   r   
entrypointfirsts          r   _image_uses_init_entrypointr   r  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_okc                     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   rV   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)rZ   r   errorRuntimeErrorr\   r]   r^   rb   rc   r   FileNotFoundErrorr_   r*   )r<   ru   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	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d3d	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d4d(ed
ede	f 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d0Zd5dedefd2Z xZS )6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   NTr   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 | _
        d| _        d| _        d| _        g | _        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<t<          j                             t<          j        !                    |                    nd}|o0tE          |          o!t<          j        #                    |          o| }|r>|r<t<          j        #                    |          st          $                    d|            d | _%        d | _&        g }| j        r |            dz  |z  }t-          |dz            | _&        t=          j'        | j&        d           |                    d| j&         dg           |sS|sQt-          |dz            | _%        t=          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 ]}tY          |d%                   }|-                                rt                              d&|           G|.                                st                              d'|           w|                    d|d%          d|d(          d)g           t                              d*|d%         |d(                     |            D ]} tY          | d%                   }|-                                st                              d+|           G|                    d| d%          d| d(          d)g           t                              d,| d%         | d(                     |            D ]}!tY          |!d%                   }|-                                st                              d-|           G|                    d|!d%          d|!d(          d)g           t                              d.|!d%         |!d(                    n2# t^          $ r%}"t          $                    d/|"           Y d }"~"nd }"~"ww xY wg }#ta          | j                  D ])}$|#                    d0|$ d1| j        |$          g           *g }%|rJtc                      }&|& d2|&g}%t                              d3|&           nt                              d4           te                      pd| _3        ti          | j3        |          }'|'rt                              d5|           tk          |otE          |%          |'6          }(t                              d7|            g })|pg D ]H}*t#          |*t,                    st                              d8|*           3|)                    |*           I|(|%z   |z   |z   |z   |#z   |)z   }+t                              d9|+            d:tm          j7                    j8        d d;          },ts          tu                                }-ts          |          }.d<d=d<d>|. d<d?|- g}/|| _        |,| _        |'| _        |+| _        d@|.|-dA| _
        d}0|r| ;                    |.|-          }1|1|1\  }2}3|2| _	        |3dBk    r	 ty          j=        | j3        dC|2gdddDdtx          j>        E           nT# tx          j?        tx          j@        f$ r6}"t                              dF|2d dG         |3|"           d | _	        Y d }"~"nd }"~"ww xY w| j	        r(t                              dH|2d dG         |.|-|3           d}0|0s2|'rg ndIg}4| j3        dJdKg|4dL|,|/dM||+|dNdO}5t          $                    dPdQA                    |5                      	 ty          j=        |5dddRdtx          j>        E          }6nk# tx          j?        tx          j@        f$ rM}"t                              dS|,|"           ty          j=        | j3        dTdU|,gddVtx          j>        W            d }"~"ww xY w|6jB                                        | _	        t                              dX|, dY| j	        d dG          dZ           | C                                | _D        | E                                 d S )[N~r   )r   rI   r   Fz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_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runningstartrU   rF   u[   Failed to start existing container %s (state=%s): %s — falling back to a fresh container.rX   z:Reusing container %s (task=%s, profile=%s, prior state=%s)--initr]   -d--name-wsleepinfinityzStarting container:  x   z<docker run failed for %s, cleaning up orphaned container: %srS   rT   rz   rG   rI   rK   zStarted container z ())Fsuper__init___persistent_persist_across_processes_task_idr   _forward_envr%   _env_container_id_labels_image_container_name_image_uses_s6_init_all_run_argsr   rl   r   r   r   r   r[   r   sysplatform_storage_opt_supportedr   tools.environments.baser   r   r   r   abspath
expanduserr#   isdirra   _workspace_dir	_home_dirmakedirstools.credential_filesr   r   r   r   is_diris_filer*   sortedr   rZ   _docker_exer   r   uuiduuid4hexr3   r9   _find_reusable_containerr\   r]   r^   CalledProcessErrorr_   joinrd   _build_init_env_args_init_env_argsinit_session)8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_mountro   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existingrw   state	init_argsrun_cmdru   	__class__s8                                                          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*   '==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 )  (*
 
$ # 	"44ZNNH#&.#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 	sJ   H#Y0 0
Z:ZZ%,d e#-,ee#.$h i;.Ai66i;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        |          }|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_passthroughr   r   )r   r   r   r   tools.env_passthroughr8  r*   r   r+   r  r   r   getr[   )
r  exec_envexplicit_forward_keyspassthrough_keysr8  forward_keys
hermes_envr   r$   r   s
             r   r  z&DockerEnvironment._build_init_env_args  sC    $(	?? #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@  rA  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  rB  r@  rI   rA  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 )NrM   )rP   prH  s     r   	<genexpr>z7DockerEnvironment._is_container_gone.<locals>.<genexpr>  s'      DD11;DDDDDDr   )any_NO_CONTAINER_PATTERNS)r  rH  s    `r   _is_container_gonez$DockerEnvironment._is_container_gone  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   NrX   u7   Container %s appears to be gone — attempting recoveryr   r   r   z&Recovery: reusing running container %sr   TrU   rF   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   r:  r  rl   r\   r]   r  r^   r  r_   r   r   r  r  r  r   r    r[   r   r   rd   r   r   r`   _snapshot_readyr  r*   )r  old_idr/  profile_labelr2  rr   r3  ro   _uuidnew_namer4  r0  kvr5  ru   s                   r   _recreate_containerz%DockerEnvironment._recreate_container  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.
        rb   r   rH  r   )r   executer:  rO  r   rX  )r  rY  r   kwargsru   r6  s        r   r[  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>   rl   rD   z{{.Driver}}Trz   rV   overlay2Fcreater   zsize=1mzhello-worldrE   r   rS   r   r   z Docker --storage-opt support: %s)r   rZ   r\   r]   r^   rd   r   lowerrb   r*   r   ra   )r>   ru   driverproberw   s        r   r  z(DockerEnvironment._storage_opt_supported7  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r/  rS  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.
        rB   rC   r?   r@   zlabel=hermes-task-id=rA   rD   z{{.ID}}	{{.State}}Trz   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 rM   rN   rO   s     r   rR   z>DockerEnvironment._find_reusable_container.<locals>.<listcomp>  s-    OOOBHHJJOOOOr   	rW      r   )r\   r]   r  r^   r_   r`   r   ra   rb   rc   r   rd   re   splitlenr`  )r  r/  rS  ru   ro   linesr   r   rQ   partsrr   r3  s               r   r  z*DockerEnvironment._find_reusable_containerb  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_removerk  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_errorsNrX   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10TrU   r   z%docker stop %s timed out / failed: %srS   rT   rY   )r\   r]   r^   r_   r`   r   r   )ro   rw   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  rk  drt  rz  trw   r<   rq  rr  rs  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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)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)r   )r  )__name__
__module____qualname____doc__r   r!   r"   r#   r   r   r   r  r\   PopenrG  rN  rO  rX  r[  staticmethodr  r   tupler  r  r  __classcell__)r6  s   @r   r   r     s	       	 	 &+ (,$!&)-%J JJ J 	J
 J J J  $J J J #Y%J D[J J J J  !J" #J$ #'%J J J J J JXd3i    @ ;@!$+/, , ,C ,4 ,,!Dj,4>4D, , , ,6E E E E E EPT P P P Pd s        " (D ( ( ( \(T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   )Frx  )2r  r   loggingr   r}   r   r\   r   r  pathlibr   typingr   r  r   r   tools.environments.localr   	getLoggerr  r   r   r   r   __annotations__compiler   r   r   r   r%   r+   r0   r3   r9   r!   rv   rj   rZ   r   r   r   r   r#   r   r   r   r   r   r   rM   r   r   <module>r     s       				 				      



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