
    `gjN                     <   d Z ddlZddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZ	 ddlZddlmZ ddlmZ ddlmZ ddlmZ  ej,                  e      Zej2                  Zej6                  ZdZdZeeegdf   Z ee!e"eef      gdf   Z#eegdf   Z$ee!e   gdf   Z%eg e!e"eef      f   Z&dd	ed
e!e"eef      fdZ'd
e(e   fdZ)de!e   d
efdZ*de!e   d
efdZ+de!e"eef      d
e!e   fdZ,ded
efdZ-dZ.dZ/dZ0 G d d      Z1y# e$ r dZY w xY w)a  Shared file sync manager for remote execution backends.

Tracks local file changes via mtime+size, detects deletions, and
syncs to remote environments transactionally.  Used by SSH, Modal,
and Daytona.  Docker and Singularity use bind mounts (live host FS
view) and don't need this.
    N)Path)Callable)get_hermes_home)_file_mtime_keyg      @HERMES_FORCE_FILE_SYNCcontainer_basereturnc                 *   ddl m}m}m} g } |       D ].  }|d   j	                  d| d      }|j                  |d   |f       0  ||       D ]  }|j                  |d   |d   f         ||       D ]  }|j                  |d   |d   f        |S )a[  Enumerate all files that should be synced to a remote environment.

    Combines credentials, skills, and cache into a single flat list of
    (host_path, remote_path) pairs.  Credential paths are remapped from
    the hardcoded /root/.hermes to *container_base* because the remote
    user's home may differ (e.g. /home/daytona, /home/user).
    r   )get_credential_file_mountsiter_cache_filesiter_skills_filescontainer_path/root/.hermes   	host_path)r   )tools.credential_filesr   r   r   replaceappend)r   r   r   r   filesentryremotes          O/root/.hermes/venv/lib/python3.12/site-packages/tools/environments/file_sync.pyiter_sync_filesr   5   s      $&E+- 3'(00^Q
 	eK(&12	3
 #.A DeK(%0@*ABCD!@ DeK(%0@*ABCDL    c            	         	 ddl m}  t               }	  |        }|D ]h  }t	        |t
              r|j                  d      nd}|s)	 |j                  t        t        |      j                         j                                      j |S # t        $ r t               cY S w xY w# t        $ r t               cY S w xY w# t        $ r4 |j                  t        t        |      j                                      Y w xY w)zBReturn credential files that are upload-only for remote sandboxes.r   )r   r   N)r   r   	Exceptionset
isinstancedictgetaddstrr   
expanduserresolveOSError)r   pathsmountsr   r   s        r   _credential_host_pathsr(   R   s    E eE+-  9.8.EEIIk*4		9IIc$y/446>>@AB9 L!  u  u  	9IIc$y/44678	9s4   B	 B" ?B;	BB"B87B8;:C87C8remote_pathsc                 8    ddj                  d | D              z   S )z<Build a shell ``rm -f`` command for a batch of remote paths.zrm -f  c              3   F   K   | ]  }t        j                  |        y wNshlexquote).0ps     r   	<genexpr>z$quoted_rm_command.<locals>.<genexpr>k   s     D!u{{1~D   !join)r)   s    r   quoted_rm_commandr7   i   s    chhD|DDDDr   dirsc                 8    ddj                  d | D              z   S )z>Build a shell ``mkdir -p`` command for a batch of directories.z	mkdir -p r+   c              3   F   K   | ]  }t        j                  |        y wr-   r.   )r1   ds     r   r3   z'quoted_mkdir_command.<locals>.<genexpr>p   s     !?Q%++a.!?r4   r5   )r8   s    r   quoted_mkdir_commandr<   n   s    !?$!????r   r   c           	      p    t        | D ch c]  \  }}t        j                  |       c}}      S c c}}w )zCExtract sorted unique parent directories from (host, remote) pairs.)sorted	posixpathdirname)r   _r   s      r   unique_parent_dirsrB   s   s*    eDF9$$V,DEEDs   2
pathc                     t        j                         }t        | d      5 t        fdd      D ]  }|j	                  |        	 ddd       |j                         S # 1 sw Y   |j                         S xY w)z$Return hex SHA-256 digest of a file.rbc                  &     j                  d      S )Ni   )read)fs   r   <lambda>z_sha256_file.<locals>.<lambda>|   s    !&&- r   r   N)hashlibsha256openiterupdate	hexdigest)rC   hchunkrH   s      @r   _sha256_filerR   x   si    A	dD	 Q/5 	EHHUO	 ;;= ;;=s   &A!!A9   )         l        c                   <   e Zd ZdZeddfdedededede	dz  de
dz  fd	Zd
ddeddfdZddedz  ddfdZdeddfdZdeddfdZddZ	 ddedeeeef      dz  dedz  fdZ	 ddddedeeeef      dz  dee   dz  dedz  fdZededee   defd       Zy)FileSyncManageru  Tracks local file changes and syncs to a remote environment.

    Backends instantiate this with transport callbacks (upload, delete)
    and a file-source callable.  The manager handles mtime-based change
    detection, deletion tracking, rate limiting, and transactional state.

    Not used by bind-mount backends (Docker, Singularity) — those get
    live host FS views and don't need file sync.
    Nget_files_fn	upload_fn	delete_fnsync_intervalbulk_upload_fnbulk_download_fnc                     || _         || _        || _        || _        || _        i | _        i | _        t               | _        d| _	        || _
        y )Ng        )_get_files_fn
_upload_fn_bulk_upload_fn_bulk_download_fn
_delete_fn_synced_files_pushed_hashesr   _upload_only_host_paths_last_sync_time_sync_interval)selfrY   rZ   r[   r\   r]   r^   s          r   __init__zFileSyncManager.__init__   sT     *#-!1#;=.014$&)+r   F)forcerl   r	   c                   |sJt         j                  j                  t              s't	               }|| j
                  z
  | j                  k  ry| j                         }| j                  j                  t                      |D ch c]  \  }}|	 }}}g }t        | j                        }|D ]J  \  }	}
t        |	      }|| j                  j                  |
      |k(  r3|j                  |	|
f       |||
<   L | j                  D cg c]	  }||vs| }}|s|st	               | _        yt        | j                        }t        | j                        }|rt         j#                  dt%        |             |rt         j#                  dt%        |             	 |r=| j&                  1| j'                  |       t         j#                  dt%        |             n3|D ].  \  }	}
| j)                  |	|
       t         j#                  d|	|
       0 |r'| j+                  |       t         j#                  d|       |D ]  \  }	}
t-        |	      | j                  |
<    |D ]0  }|j/                  |d       | j                  j/                  |d       2 || _        t	               | _        yc c}}w c c}w # t0        $ r.}|| _        || _        t         j3                  d|       Y d}~yd}~ww xY w)aT  Run a sync cycle: upload changed files, delete removed files.

        Rate-limited to once per ``sync_interval`` unless *force* is True
        or ``HERMES_FORCE_FILE_SYNC=1`` is set.

        Transactional: state only committed if ALL operations succeed.
        On failure, state rolls back so the next cycle retries everything.
        Nzfile_sync: uploading %d file(s)z+file_sync: deleting %d stale remote file(s)z#file_sync: bulk-uploaded %d file(s)zfile_sync: uploaded %s -> %szfile_sync: deleted %sz-file_sync: sync failed, rolled back state: %s)osenvironr    _FORCE_SYNC_ENV
_monotonicrh   ri   r`   rg   rN   r(   r   re   r   r   rf   loggerdebuglenrb   ra   rd   rR   popr   warning)rj   rl   nowcurrent_filesrA   r   current_remote_paths	to_upload	new_filesr   remote_pathfile_keyr2   	to_delete
prev_filesprev_hashesexcs                    r   synczFileSyncManager.sync   s    RZZ^^O<,CT)))D,?,??**,$$++,B,DE8EF91fFF ,.	++,	&3 	."I{&y1H!!%%k2h>i56%-Ik"	. !% 2 2T1a?S6SQT	T#-<D  $,,-
4../LL:C	NKLLFIW!	QT11=$$Y/BC	NS.7 Y*I{OOI{;LL!?KXY 	*4i@ +4 K&	;3?	3J##K0K  1a&##''401 "+D#-<D i  G UN  		Q!+D"-D NNJCPP		Qs+   J"	J(J(DJ- -	K$6$KK$hermes_homec                    | j                   y| j                  s"| j                  st        j	                  d       y|xs
 t               dz  }|j                  j                  dd       d}t        t              D ]  }	 | j                  |        y t        j                  dt        |       y# t        $ rG}|}|t        dz
  k  r/t        |   }t        j                  d|dz   ||       t        |       Y d}~d}~ww xY w)	a  Pull remote changes back to the host filesystem.

        Downloads the remote ``.hermes/`` directory as a tar archive,
        unpacks it, and applies only files that differ from what was
        originally pushed (based on SHA-256 content hashes).

        Protected against SIGINT (defers the signal until complete) and
        serialized across concurrent gateway sandboxes via file lock.
        Nu+   sync_back: no prior push state — skippingz
.sync.lockT)parentsexist_okr   z2sync_back: attempt %d failed (%s), retrying in %dsz%sync_back: all %d attempts failed: %s)rc   rf   re   rr   rs   r   parentmkdirrange_SYNC_BACK_MAX_RETRIES_sync_back_oncer   _SYNC_BACK_BACKOFFrv   _sleep)rj   r   	lock_pathlast_excattemptr   delays          r   	sync_backzFileSyncManager.sync_back   s     !!)
 ""4+=+=LLFG 5O$5E	td;%)34 	"G"$$Y/	" 	>@VX`a  "3a77.w7ENNL!S% 5M"s   ?B//	C?8=C::C?r   c                 h   t        j                         t        j                         u }g d}|rLt        j                  t        j
                        }fd}t        j                  t        j
                  |       	 | j                  |       |rN|Kt        j                  t        j
                  |       r$t        j                  t        j
                         yyyy# |rN|Kt        j                  t        j
                  |       r$t        j                  t        j
                         w w w w xY w)z>Single sync-back attempt with SIGINT protection and file lock.Nc                 V    j                  | |f       t        j                  d       y )Nz/sync_back: SIGINT deferred until sync completes)r   rr   rs   )signumframedeferred_sigints     r   _defer_sigintz6FileSyncManager._sync_back_once.<locals>._defer_sigint.  s"    &&7NOr   )	threadingcurrent_threadmain_threadsignal	getsignalSIGINT_sync_back_lockedraise_signal)rj   r   on_main_threadoriginal_handlerr   r   s        @r   r   zFileSyncManager._sync_back_once"  s    
 #113y7L7L7NN(*%//>P MM&--7	7""9-"2">fmm-=>" ''6 # #?~~"2">fmm-=>" ''6 # #?~s   =C AD1c                    t         | j                          yt        |dd      }	 t        j                  |t         j                         | j                          	 t        j                  |t         j
                         |j                          y# t        t        f$ r Y "w xY w# 	 t        j                  |t         j
                         n# t        t        f$ r Y nw xY w|j                          w xY w)z;Sync-back under file lock (serializes concurrent gateways).Nwzutf-8)encoding)	fcntl_sync_back_implrL   flockLOCK_EXLOCK_UNr%   IOErrorclose)rj   r   lock_fds      r   r   z!FileSyncManager._sync_back_lockedE  s    =  "y#8	KK/  "GU]]3 MMO W% GU]]3W% MMOsA   4B& $B B#"B#&C3($CC3CC3CC3c           	         | j                   t        d      	 t        | j                               }t        j                  d      5 }| j                  t        |j                               	 t        j                  j                  |j                        }|t        kD  r%t        j                  d|t               	 ddd       yt        j                   d      5 }t#        j$                  |j                        5 }|j'                  |d	
       ddd       d}| j(                  t+               z  }t        j,                  |      D ]  \  }}	}
|
D ]x  }t        j                  j/                  ||      }t        j                  j1                  ||      }d|z   }| j2                  j5                  |      }|t7        |      }||k(  rwd}| j9                  ||      }|-| j;                  |||      }|t        j=                  d|       | j?                  ||      rt        j=                  d|       t        j                  jA                  |      r(|&t7        |      }||k7  rt        j                  d|       t        jB                  t        j                  jE                  |      d       tG        jH                  ||       |dz  }{  |rt        jK                  d|       nt        j=                  d       ddd       ddd       y# t        $ r g }Y w xY w# t        $ r d}Y w xY w# 1 sw Y   $xY w# 1 sw Y   CxY w# 1 sw Y   yxY w)z1Download, diff, and apply remote changes to host.Nz/_sync_back_impl called without bulk_download_fnz.tar)suffixr   uB   sync_back: remote tar is %d bytes (cap %d) — skipping extractionzhermes-sync-back-)prefixdata)filter/upload_only_host_pathsz(sync_back: skipping %s (no host mapping)z2sync_back: skipping upload-only credential file %suw   sync_back: conflict on %s — host modified since push, remote also changed. Applying remote version (last-write-wins).T)r   r   z%sync_back: applied %d changed file(s)z%sync_back: no remote changes detected)&rc   RuntimeErrorlistr`   r   tempfileNamedTemporaryFiler   namern   rC   getsizer%   _SYNC_BACK_MAX_BYTESrr   rv   TemporaryDirectorytarfilerL   
extractallrg   r(   walkr6   relpathrf   r    rR   _resolve_host_path_infer_host_pathrs   _is_upload_only_host_pathexistsmakedirsr@   shutilcopy2info)rj   file_mappingtftar_sizestagingtarappliedr   dirpath	_dirnames	filenamesfnamestaged_filerelr|   pushed_hashremote_hashr   	host_hashs                      r   r   zFileSyncManager._sync_back_implV  s&   !!)PQQ	 2 2 45L ((7 O	J2""4=177??2773 ..X2 O	J O	J  ,,4GH ?JG\\"''* ;cNN76N:; 003I3KK ' 68WWW5E 2%1GY	!* 1%&(ggll7E&B ggook7C&)Ci&*&9&9&=&=k&J '2*6{*CK*k9 (*.K %)$;$;K$V	$,(,(=(= + ,7M )> )I
  )0 &$N$/!" !)99)E[\"LL T + %77>>)49P(4Y(?I(K7 &%H %0	!" BGGOOI$>N[)<1c1%2%h KK GQLL!HI?J!O	J O	J  	L	  ; ;?J ?J!O	J O	Jsx   L %M
.)L%M
M
 L>;L1G-L><M
LLL.*M
-L..M
1L;6L>>M	M

Mr|   r   c                 6    ||ng }|D ]  \  }}||k(  s|c S  y)zAFind the host path for a known remote path from the file mapping.N )rj   r|   r   mappinghostr   s         r   r   z"FileSyncManager._resolve_host_path  s5     #/":,# 	LD&$	 r   r   r   c                0   ||ng }|xs
 t               }|D ]}  \  }}| j                  ||      rt        t        |      j                        }|j                  |dz         sLt        t        |      j                        }|t        |      d }	||	z   c S  y)u  Infer a host path for a new remote file by matching path prefixes.

        Uses the existing file mapping to find a remote->host directory
        pair, then applies the same prefix substitution to the new file.
        For example, if the mapping has ``/root/.hermes/skills/a.md`` →
        ``~/.hermes/skills/a.md``, a new remote file at
        ``/root/.hermes/skills/b.md`` maps to ``~/.hermes/skills/b.md``.
        Nr   )r   r   r"   r   r   
startswithrt   )
rj   r|   r   r   r   r   r   
remote_dirhost_dirr   s
             r   r   z FileSyncManager._infer_host_path  s     #/":,!7!@35# 	)LD&--d4JKT&\001J%%j3&67tDz001$S_%56&((	) r   r   c                     	 t        t        |       j                         j                               }||v S # t        $ r( t        t        |       j                               }Y ||v S w xY wr-   )r"   r   r#   r$   r%   )r   r   resolveds      r   r   z)FileSyncManager._is_upload_only_host_path  si    	94	?557??ABH 111  	94	?5578H111	9s   06 +A'&A'r-   )r	   N)__name__
__module____qualname____doc___SYNC_INTERVAL_SECONDS
GetFilesFnUploadFnDeleteFnfloatBulkUploadFnBulkDownloadFnrk   boolr   r   r   r   r   r   r"   r   tupler   r   r   staticmethodr   r   r   r   rX   rX      s     6.226, , , 	,
 , %t+, )4/,( %* OQT OQd OQj&bTD[ &bD &bP!7 !7$ !7F4 D "ZJz IMc )-eCHo)>)EQTW[Q[ GK DHC '+E#s(O'<t'C 25SD MPRVJ0 2S 2#c( 2W[ 2 2r   rX   )r   )2r   rJ   loggingrn   r?   r/   r   r   r   r   r   timer   ImportErrorpathlibr   typingr   hermes_constantsr   tools.environments.baser   	getLoggerr   rr   sleepr   	monotonicrq   r   rp   r"   r   r   r   r   r   r   r   r   r   r(   r7   r<   rB   rR   r   r   r   rX   r   r   r   <module>r      s     	           , 3			8	$ 
 ^^
 * S#J$%eCHo./564&$,'T#YK%&b$uS#X//0
C d5c?>S :C .EDI E# E
@tCy @S @
Fd5c?3 FS	 F
s s    - S2 S2_  Es   D DD