
    sj[                       U d Z ddlm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Zded<   dd	d
dddddZded<   dad4dZd5dZd6dZd7dZd8d Zd4d!Zd9d"Zd:d$Zd4d%Zd;d'Zd<d)Z	 	 d=d>d.Zd/Zd<d0Zd?d1Zd@d3ZdS )Au  Dependency-light venv recovery that runs BEFORE hermes_cli.main's imports.

The ``hermes`` console entry point is ``hermes_cli.main:main``.  Importing
``hermes_cli.main`` pulls in third-party packages at module level (``dotenv``
via ``hermes_cli.env_loader``, ``yaml`` via ``hermes_cli.config``, ...).  In
the exact failure state the update-recovery markers exist for — a failed lazy
backend refresh or interrupted core install that wiped a core package's
import files (#57828) — a normal launch crashes *while importing main.py*,
before ``_recover_from_interrupted_install()`` can run.  The marker system is
unreachable precisely when it is needed most.

This module is deliberately **stdlib-only** so importing it can never fail on
a corrupted venv.  ``hermes_cli.main`` imports and calls
:func:`recover_if_needed` at the very top of its module body, before any
third-party import.

Scope: this early pass only repairs enough for ``hermes_cli.main`` to become
importable again (force-reinstall of the known-fragile core packages, using
the pins from pyproject.toml).  It NEVER clears the recovery markers — the
full, confirmed marker lifecycle stays with ``_recover_from_interrupted_install()``
in main.py, which runs right after import succeeds.
    )annotationsN)Path))yaml
SafeDumper)dotenvload_dotenv)clickCommand)certificontents)richprint)cryptography__version__)jwtencodeztuple[tuple[str, str], ...]LAZY_REFRESH_IMPORT_PROBESPyYAMLzpython-dotenvr	   r   r   r   PyJWT)r   r   r	   r   r   r   r   zdict[str, str]LAZY_REFRESH_REPAIR_PACKAGESFreturnboolc                     t           S )zCWhether this updater already completed its deferred native install.)_UPDATE_RETRY_RECOVERED     >/home/agent/.hermes/hermes-agent/hermes_cli/_early_recovery.py$_should_skip_external_secret_sourcesr   D   s    ""r   r   c                 b    t          t                                                    j        j        S )N)r   __file__resolveparentr   r   r   _project_rootr#   I   s     >>!!##*11r   pidintc                   | dk    rdS t           j        dk    r%	 ddl}d}|                    dd          }|j        |j        |j        g|j        _        |j        |j        _	        |j        |j        g|j
        _        |j        |j
        _	        |j        g|j        _        |j        |j        _	        |                    |d|           }|s|                                d	k    S 	 |
                    |d          d
k    |                    |           S # |                    |           w xY w# t          $ r Y dS w xY w	 t          j        | d           n)# t           $ r Y dS t"          $ r Y dS t$          $ r Y dS w xY wdS )a  Best-effort stdlib-only process liveness probe.

    ``os.kill(pid, 0)`` is not a no-op on Windows, so use the Win32 process
    handle API there.  An access-denied result is conservatively live: racing
    an elevated updater is worse than postponing recovery for one launch.
    r   Fwin32Ni   kernel32T)use_last_error   i  )sysplatformctypesWinDLLc_ulongc_intOpenProcessargtypesc_void_prestypeWaitForSingleObjectCloseHandleget_last_error	ExceptionoskillProcessLookupErrorPermissionErrorOSError)r$   r-   synchronizer(   handles        r   _pid_is_runningr@   M   s    axxu
|w	MMM$K}}Z}EEH-H )
 ,2?H (5;_fn4UH(139>H(0-3_,=H )+1<H ())+ucBBF 4,,..!33-33FA>>#E$$V,,,,$$V,,,, 	 	 	44	
Q   uu   tt   uu4sH   CD. %D >D. D++D. .
D<;D< E 
E<#	E</	E<;E<markerc                t   	 |                      dd          }n# t          $ r Y dS w xY w|                                D ]x}|                    d          \  }}}|r[|                                dk    rC	 t          t          |                                                    c S # t          $ r Y  dS w xY wydS )z?True when a legacy update marker names a process still running.utf-8replaceencodingerrorsF=r$   )	read_textr=   
splitlines	partitionstripr@   r%   
ValueError)rA   bodylinekey	separatorvalues         r   _marker_owner_is_liverS   z   s    CC   uu!!   $s 3 3Y 	--&s5;;=='9'9:::::   uuu5s    
((5-B%%
B43B4packages	list[str]project_rootc                   |dz  }|                                 s| S 	 ddl}t          |d          5 }|                    |                              di                               dg           pg }ddd           n# 1 swxY w Y   n# t
          $ r | cY S w xY wi |D ]}|                    dd          d                                         }|}d	D ]$}	|	|v r|                    |	d          d         } n%|                                                    d
d          d                                                                         }
|
r||
<   fd| D             S )u  Map bare package names to their pinned specs from pyproject.toml.

    Stdlib-only (tomllib + naive requirement-head parsing — ``packaging`` may
    itself be broken in the failure state this module exists for).  Unknown
    packages fall back to their bare name.
    pyproject.tomlr   Nrbprojectdependencies;   )z==z>=z<=z~=><z!=[c                `    g | ]*}                     |                                |          +S r   )getlower).0pkgname_to_specs     r   
<listcomp>z!_pinned_specs.<locals>.<listcomp>   s1    CCC3LSYY[[#..CCCr   )	is_filetomllibopenloadrb   r8   splitrL   rc   )rT   rV   	pyprojectri   fraw_depsspecheadbareoprP   rf   s              @r   _pinned_specsrt      s    //I )T"" 	Xa||A**9b99==nbQQWUWH	X 	X 	X 	X 	X 	X 	X 	X 	X 	X 	X 	X 	X 	X 	X    $&L 	% 	%zz#q!!!$**,,: 	 	BTzzzz"a((+  jjll  a((+113399;; 	% $LCCCC(CCCCs5   B
 A A>2B
 >BB
 BB
 
BBc                     	 ddl } t          |                                           }|                                 p|                                j        dk     S # t          $ r Y dS w xY w)u  True when certifi imports but its ``cacert.pem`` is missing/corrupt.

    A brew Python upgrade or an interrupted venv rebuild can leave certifi's
    distribution metadata (and even the module) intact while the bundled
    ``cacert.pem`` is gone or a dangling symlink — every TLS connection then
    fails with an opaque ``Could not find a suitable TLS CA certificate
    bundle`` from deep inside httpx/requests (#29866). An attribute probe
    alone passes in that state, so validate the bundle path itself.
    r   Ni   T)r   r   whererh   statst_sizer8   )r   bundles     r   _certifi_bundle_brokenrz      sq    	gmmoo&&>>###Cv{{}}'<t'CC    tts   AA 
A'&A'c                 f   g } t           D ]\  }}	 t          j        |          }t          ||          st	          | d|           |dk    rt                      rt	          d          b# t          $ r8 t                              |          }|r|| vr| 	                    |           Y w xY w| S )a  Import-probe the fragile core packages in THIS process.

    Returns repair package names (deduped, probe order) for modules that fail
    to import or lack their sentinel attribute.  Failed imports leave nothing
    in ``sys.modules``, so a post-repair retry in the same process works.

    certifi additionally gets a bundle-file check: the module can import
    cleanly while ``cacert.pem`` is missing (#29866).
    z	 missing r   z%certifi cacert.pem missing or corrupt)
r   	importlibimport_modulehasattrImportErrorrz   r8   r   rb   append)brokenmod_nameattrmodre   s        r   _probe_broken_packagesr      s     F4 
# 
#$		#)(33C3%% @!X">">">">???9$$)?)A)A$!"IJJJ 	# 	# 	#.228<<C #s&((c"""	# Ms   AA,,?B.-B.
str | Nonec                 L   t           j        dk    rdnd} t          j                    dz  dz  | z  t          j                    dz  dz  | z  t          j                    dz  dz  | z  g}|D ]'}|                                rt          |          c S (t          j        |           S )an  Locate a ``uv`` binary without importing third-party modules.

    uv-managed base interpreters carry an ``EXTERNALLY-MANAGED`` marker, so
    the stdlib ``pip`` fallback below refuses to touch them.  In that state
    the only sanctioned installer is uv itself, which Hermes already vendors
    (``~/.hermes/bin/uv.exe``) or the user has on PATH.  Stdlib-only.
    r'   zuv.exeuvz.hermesbinz.localz.cargo)r+   r,   r   homerh   strshutilwhich)exe
candidatespaths      r   _find_uv_binaryr      s     lg--((4C	i%'#-	h&,	h&,J
   <<>> 	t99	<r   c                     	 ddl } t          |                     d                    }|dz                                  rdS |j        dz                                  rdS n# t
          $ r Y nw xY wdS )a  True when ``sys.executable`` is a uv/standalone-builds managed install.

    Those interpreters ship an ``EXTERNALLY-MANAGED`` marker next to their
    stdlib (PEP 668), so ``python -m pip install`` aborts with
    ``externally-managed-environment``.  The early repair must then go
    through uv (or explicitly override pip) or the reinstall no-ops and the
    venv stays broken (#83569).
    r   NstdlibzEXTERNALLY-MANAGEDTF)	sysconfigr   get_pathexistsr"   r8   )r   r   s     r   '_base_interpreter_is_externally_managedr      s    
i((2233))1133 	4M0088:: 	4	   5s   =A  A   
A-,A-specsc           	     d   t                      }|rt                      }|ri t          j        dt	          |dz            i}|                    dd           |                    dd           	 t          j        |dddg| |d	d	d
d|          }|j        dk    rd	S |j	        p|j
        pddd         }|rt          |t          j	                   dS # t          $ r)}t          d| t          j	                   Y d}~dS d}~ww xY wt          dt          j	                   	 t          j        t          j        ddddg|d	           n# t          $ r Y nw xY wt          j        ddddg}|r|                    d           |                    |            	 t          j        ||d	d	d
d          }n6# t          $ r)}t          d| t          j	                   Y d}~dS d}~ww xY w|j        dk    r7|j	        p|j
        pddd         }|rt          |t          j	                   dS d	S )u  ``uv pip`` (or stdlib ``pip``) force-reinstall of the given specs.

    Streams nothing to stdout (``hermes acp`` speaks JSON-RPC on stdout);
    output is captured and replayed to stderr only on failure.  Never raises.

    Two installer paths, in priority order:

    1. ``uv pip install`` with ``VIRTUAL_ENV`` pointed at the project venv —
       required when the base interpreter is uv-managed (Windows git checkouts
       install exactly this way: uv's Python declares PEP 668
       ``EXTERNALLY-MANAGED`` and plain ``python -m pip`` refuses to run).
    2. ``sys.executable -m pip`` as before, for self-contained venvs whose
       interpreter carries no PEP 668 marker.
    VIRTUAL_ENVvenv
PYTHONHOMEN
PYTHONPATHpipinstallz--force-reinstallTrC   rD   )cwdcapture_outputtextrF   rG   envr    i0fileFu*     ✗ Early venv repair could not run uv: uw     ⚠ Base interpreter is externally managed and no uv binary was found; retrying repair via pip with PEP 668 override.z-m	ensurepipz	--upgradez--default-pip)r   r   z--break-system-packages)r   r   r   rF   rG   u+     ✗ Early venv repair could not run pip: )r   r   r9   environr   pop
subprocessrun
returncodestderrstdoutr   r+   r8   
executabler   extend)	r   rV   externally_managedr   r   resulttailexcpip_cmds	            r   _run_repair_installr     s    ABB 
 	KRZKL64I0J0JKKCGGL$'''GGL$'''#	+>GG$#'	   $))4<<"effE 1$SZ0000u   H3HHszZZZZuuuuu
 	D	
 	
 	
 	
^T;_M	
 	
 	
 	
 	

    ~tUI7JKG 20111NN5			
 
 
    ACAA
SSSSuuuuu A44"eff= 	)$SZ((((u4sH   1,C 5C 
D	 DD	(&E 
EEF8 8
G+G&&G+rootc                    dt           j        v o3| t          t                                                    j        j        k    S )uE  True when running under pytest AND ``root`` is this module's own
    checkout — the one whose venv is executing the suite right now.

    Lifecycle tests spawn real subprocesses that import ``hermes_cli.main``
    with recovery armed; ``PYTEST_CURRENT_TEST`` rides the inherited env into
    those children. Without this guard, a genuinely-broken dev venv gets a
    REAL ``ensurepip`` + ``pip install --force-reinstall`` from inside a
    running test suite. Tests that sandbox ``project_root`` to a tmp_path are
    unaffected (same posture as ``managed_scope._under_pytest``).PYTEST_CURRENT_TEST)r9   r   r   r    r!   r"   r   s    r   _pytest_owns_live_checkoutr   Q  s8     	+ 	;DNN**,,3::r   Path | Noneargvlist[str] | NoneNonec                8   	 |t           j        dd         n|}| t                      n| }t          |          rdS |dz  }|dz  }|                                s|                                sdS |dz                                  sdS |                                r+t          |          rdS t          ||          }|rd|v rdadS d|v rdS t                      }|sdS |dz  }	 t          j        |t          j        t          j        z  t          j        z            }	t          j        |	t          j                     d	                                           t          j        |	           ns# t&          $ r[ 	 t)          j                    |                                j        z
  d
k    r|                                 n# t0          $ r Y nw xY wY dS t0          $ r Y nw xY w	 t3          ||          }
t5          dd                    |           t           j                   t;          |
|          r*t                      st5          dt           j                   nZt5          dt           j                   t5          dt           j         dd                    |
          z   t           j                   	 |                                 dS # t0          $ r Y dS w xY w# 	 |                                 w # t0          $ r Y w w xY wxY w# t>          $ r Y dS w xY w)u1  Repair wiped core packages so ``hermes_cli.main`` can import at all.

    Fast path (no marker present) is two ``lstat`` calls.  Only acts when a
    recovery marker from a prior ``hermes update`` exists AND an import probe
    confirms a core package is actually broken.  Markers are intentionally
    NOT cleared here — ``_recover_from_interrupted_install()`` in main.py owns
    the confirmed marker lifecycle and runs immediately after import succeeds.

    Never raises: on any failure the import of main.py proceeds and surfaces
    the real error.
    Nr]   z.update-incompletez.lazy-refresh-incompleterX   updateT.update-incomplete.lock
  uQ   ⚠ Core package(s) broken by an interrupted update — repairing before launch: z, r   u     ✓ Core packages repaired.u9     ✗ Automatic repair incomplete. Recover manually with:z    z" -m pip install --force-reinstall  ) r+   r   r#   r   r   rh   rS   _complete_pending_core_installr   r   r9   rj   O_CREATO_EXCLO_WRONLYwritegetpidr   closeFileExistsErrortimerw   st_mtimeunlinkr=   rt   r   joinr   r   r   r8   )rV   r   argsr   core_markerlazy_marker	completedr   	lock_pathfdr   s              r   recover_if_neededr   a  s   "[#|sx||"."6}L%d++ 	F1177!!## 	K,>,>,@,@ 	F ''0022 	F"  	$[11 6t[III /X--*.'F
 tF')) 	F 44		BJ$:R[$HIIBHRBIKK+++2244555HRLLLL 	 	 	9;;!1!1!::TAA$$&&&   FF 	 	 	D		!&$//E@,0IIf,=,=@ @Z   
 #5$// 8N8P8P 5CJGGGGGO    M3>MMMhhuoo&     """""     """"      s   9L 2L 1L 
#L /L 	L L !L 'BE0 /L 0
G ;AG G 
GG GG L 	G L G  L $CK! :K 
KL KL !L#K87L8
LLLLL 
LL   c                
   | dz  }	 t          j        |t           j        t           j        z  t           j        z            }t          j        |t          j                     d                                           t          j        |           dS # t          $ r[ 	 t          j
                    |                                j        z
  dk    r|                                 n# t          $ r Y nw xY wY dS t          $ r Y dS w xY w)z?Single-flight claim on the shared recovery lock.  Never raises.r   r   Tr   F)r9   rj   r   r   r   r   r   r   r   r   r   rw   r   r   r=   )r   r   r   s      r   _claim_recovery_lockr     s
   00IWY
RY 6 DEE
	'''..00111
t   	y{{Y^^--66==  """ 	 	 	D	uu    tts=   BB 
DAC"!D"
C/,D.C//D5	DDc                X    	 | dz                                    dS # t          $ r Y dS w xY w)z0Best-effort release of the shared recovery lock.r   N)r   r=   r   s    r   _release_recovery_lockr     sG    	)	)1133333   s    
))r   c           	        	 ddl m} d}	 |                    dd                                          }|rUddl}	 t          |                    |                              dd                    }n# t          t          f$ r d}Y nw xY wn# t          $ r d}Y nw xY w|t          k    r!t          d| d	t          j        
           dS t          |           sdS 	 t          dt          j        
           |                    |            n# t"          $ rs}|                    |          }t          d| dt           d| t          j        
           t          dt          j        
           Y d}~t'          |            dS d}~ww xY w	 t'          |            n# t'          |            w xY w	 |                                 n# t          $ r Y nw xY wt          dt          j        
           dS # t"          $ r Y dS w xY w)u  Run the pending core install BEFORE main.py can import native modules.

    ``recover_if_needed`` invokes this when ``.update-incomplete`` exists —
    a prior ``hermes update`` (or the self-lock preflight, #83569) left the
    dependency sync deliberately unfinished.  Completing it here matters on
    Windows: the deferral exists precisely because the process that wrote the
    marker had a native venv extension mapped; this process, running before
    ``hermes_cli.main``'s third-party imports, maps nothing yet, so the
    installer can replace ``.pyd`` files without hitting the lock.

    Marker lifecycle: cleared on success; kept (attempts counter bumped) on
    failure for the next launch or main.py's post-import recovery.  An
    attempts ceiling caps automatic retries so a persistent installer
    failure does not block every launch (``hermes acp`` included).

    Never raises: any failure leaves the marker for the post-import path and
    returns ``False``.  Returns ``True`` only after the install succeeds.
    r   )_install_repairrC   rD   rE   Nattemptsu:   ⚠ Pending interrupted-update install has already failed uJ    times in the early pass — leaving it for the post-import recovery path.r   Fu   ⚠ A previous `hermes update` was interrupted mid-install — finishing dependency installation now (before any native extensions load)...u;     ✗ Early interrupted-install completion failed (attempt /z): z]  The next launch will retry; hermes will keep working from the current venv in the meantime.u:     ✓ Dependency installation completed in the early pass.T)
hermes_clir   rI   rL   jsonr%   loadsrb   rM   AttributeErrorr=    _EARLY_CORE_INSTALL_MAX_ATTEMPTSr   r+   r   r   run_core_installr8   bump_marker_attemptsr   r   )r   r   irr   raw_jsonr   new_attemptss           r   r   r     s   &B444444
 
	'''KKQQSSC !$$$$!"5;;s#3#3#7#7
A#F#FGGHH"N3 ! ! ! HHH! 	 	 	HHH	 777-- - - Z	    5#D)) 	5	)& Z	    %%%% 	 	 	22;??LMM M"BM MGJM MZ   
 4Z   
 555"4(((((	 & #4(((("4((((	     	 	 	D	H	
 	
 	
 	
 t   uus   G5 /B 6A2 1B 2BB BB G5 BG5 B-G5 
G5 0D F 
F
AF0F 4G5 F

F G5 F//G5 3G G5 
GG5 GG5 5
HH)r   r   )r   r   )r$   r%   r   r   )rA   r   r   r   )rT   rU   rV   r   r   rU   )r   rU   )r   r   )r   rU   rV   r   r   r   )r   r   r   r   )NN)rV   r   r   r   r   r   )r   r   r   r   )r   r   r   r   r   r   )__doc__
__future__r   r|   r9   r   r   r+   r   pathlibr   r   __annotations__r   r   r   r#   r@   rS   rt   rz   r   r   r   r   r   r   r   r   r   r   r   r   r   <module>r      s4    . # " " " " "     				      



       ;      "0 0         # # # #
2 2 2 2* * * *Z    D D D D@   ,   2   (   .J J J JZ   " !%!l l l l lj $%     *   U U U U U Ur   