
    PL
j]              	      &   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dlmZ ddlmZmZmZ  ej        e          Zi ddd	d
dddddddddddddddddddddd d!d"d#d$d%d&d'd(d)d*d+d,d-d.d/d0Zd1ed2<    ej        d3          Z G d4 d5e          Z e
d67           G d8 d9                      Zd_d<Zd`d?Zdad@ZdadAZd`dBZd`dCZ dDdEdbdJZ!dcdLZ"dcdMZ#d6dNdddQZ$dedRZ%dfdTZ&dgdVZ'dWdNdhdYZ(dWdNdid^Z)dS )ju	  
Lazy dependency installer for opt-in Hermes Agent backends.

Many Hermes features (Mistral TTS, ElevenLabs TTS, Honcho memory, Bedrock,
Slack, Matrix, etc.) require Python packages that not every user needs. The
historical approach was to bundle them all under ``pyproject.toml`` extras
(``hermes-agent[all]``) and install them eagerly at setup time. That has
two problems:

1. **Fragility.** When one extra's transitive dependency becomes
   unavailable on PyPI (quarantined for malware, yanked, broken upload),
   the *entire* ``[all]`` resolve fails and fresh installs silently fall
   back to a stripped tier — losing 10+ unrelated extras at once.

2. **Bloat.** A user who only ever talks to one provider pulls hundreds
   of packages they will never import.

The lazy-install pattern fixes both. Backends call :func:`ensure` at the
top of their first-import path. If the deps are missing, ``ensure`` checks
the ``security.allow_lazy_installs`` config flag (default true) and runs
a venv-scoped pip install. If the user has explicitly disabled lazy
installs, ``ensure`` raises :class:`FeatureUnavailable` with a clear
remediation hint pointing at ``hermes tools`` or the manual pip command.

Security model:

* **Venv-scoped only.** Installs target ``sys.executable`` in the active
  venv. We never touch the system Python.
* **PyPI by package name only.** Specs may be ``"package>=1.0,<2"`` etc.
  We do NOT support ``--index-url`` overrides, ``git+https://``, file:
  paths, or any other input that could be hijacked by a malicious config.
* **Allowlist.** Only specs that appear in :data:`LAZY_DEPS` can be
  installed via this path. A typo in feature name doesn't get the user
  install-anything semantics.
* **Opt-out.** Setting ``security.allow_lazy_installs: false`` in
  ``config.yaml`` disables runtime installs. Users in restricted networks
  or strict security postures can pin themselves to whatever was installed
  at setup time.
* **Offline detection.** If the install fails (offline, mirror down,
  PyPI 404 / quarantine), we surface the failure as
  :class:`FeatureUnavailable` with the actual pip stderr — no silent
  retries, no caching of bad state.

Adding a new backend:

1. Add an entry to :data:`LAZY_DEPS` with the package specs.
2. At the top of the backend module's import path, call
   ``ensure("feature.name")`` inside a try/except that converts
   :class:`FeatureUnavailable` to a useful runtime error.
    )annotationsN)	dataclass)Path)AnyCallableOptionalzprovider.anthropic)zanthropic==0.87.0zprovider.bedrock)zboto3==1.42.89z
search.exa)zexa-py==2.10.2zsearch.firecrawl)zfirecrawl-py==4.17.0zsearch.parallel)zparallel-web==0.4.2ztts.edge)zedge-tts==7.2.7ztts.elevenlabs)zelevenlabs==1.59.0zstt.faster_whisper)zfaster-whisper==1.2.1zsounddevice==0.5.5znumpy==2.4.3z	image.fal)zfal-client==0.13.1zmemory.honcho)zhoncho-ai==2.0.1zmemory.hindsight)zhindsight-client==0.6.1zplatform.telegram)z#python-telegram-bot[webhooks]==22.6zplatform.discord)zdiscord.py[voice]==2.7.1zbrotlicffi==1.2.0.1zplatform.slack)zslack-bolt==1.27.0zslack-sdk==3.40.1zaiohttp==3.13.4zplatform.matrix)zmautrix[encryption]==0.21.0zMarkdown==3.10.2zaiosqlite==0.22.1zasyncpg==0.31.0zaiohttp-socks==0.11.0zplatform.dingtalk)zdingtalk-stream==0.24.3zalibabacloud-dingtalk==2.2.42qrcode==7.4.2zplatform.feishu)zlark-oapi==1.5.3r	   )zmodal==1.3.4)zdaytona==0.155.0)zvercel==0.5.7)z!google-api-python-client==2.194.0zgoogle-auth-oauthlib==1.3.1zgoogle-auth-httplib2==0.3.1)zyoutube-transcript-api==1.2.4)zagent-client-protocol==0.9.0)zfastapi==0.133.1zuvicorn[standard]==0.41.0)zterminal.modalzterminal.daytonazterminal.vercelzskill.google_workspacezskill.youtubeztool.acpztool.dashboardzdict[str, tuple[str, ...]]	LAZY_DEPSz]^[A-Za-z0-9_][A-Za-z0-9_.\-]*(?:\[[A-Za-z0-9_,\-]+\])?(?:[<>=!~]=?[A-Za-z0-9_.\-+,*<>=!~]+)?$c                  ,     e Zd ZdZd
 fdZdd	Z xZS )FeatureUnavailablezA lazily-installable feature is missing and cannot be made available.

    Either the deps were never installed and the user has disabled lazy
    installs, or the install attempt failed.
    featurestrmissingtuple[str, ...]reasonc                    || _         || _        || _        t                                          |                                            d S N)r   r   r   super__init___format)selfr   r   r   	__class__s       3/home/kuhnn/.hermes/hermes-agent/tools/lazy_deps.pyr   zFeatureUnavailable.__init__   s>    (((((    returnc           	     |    d                     d | j        D                       }d| j        d| j         d| d| d	S )N c              3  4   K   | ]}t          |          V  d S r   repr.0ss     r   	<genexpr>z-FeatureUnavailable._format.<locals>.<genexpr>   s(      ;;T!WW;;;;;;r   zFeature z unavailable: z%. To enable manually: uv pip install z  (or: pip install z).)joinr   r   r   )r   	spec_lists     r   r   zFeatureUnavailable._format   si    HH;;dl;;;;;	.t| . .T[ . .2;. . ). . .	
r   )r   r   r   r   r   r   )r   r   )__name__
__module____qualname____doc__r   r   __classcell__)r   s   @r   r   r      s[         ) ) ) ) ) )
 
 
 
 
 
 
 
r   r   T)frozenc                  .    e Zd ZU ded<   ded<   ded<   dS )_InstallResultboolsuccessr   stdoutstderrN)r'   r(   r)   __annotations__ r   r   r.   r.      s+         MMMKKKKKKKKr   r.   r   r/   c                 
   t           j                            d          dk    rdS 	 ddlm}   |             }n# t
          $ r Y dS w xY w|                    d          pi }|                    dd          }t          |          S )	a  Return the ``security.allow_lazy_installs`` config flag.

    Defaults to True. If config is unreadable we fail open (allow), because
    refusing to install would lock people out of their own backends; the
    decision to block is an explicit user opt-in.
    HERMES_DISABLE_LAZY_INSTALLS1Fr   )load_configTsecurityallow_lazy_installs)osenvirongethermes_cli.configr8   	Exceptionr/   )r8   cfgsecvals       r   _allow_lazy_installsrC      s     
z~~455<<u111111kmm   tt
''*


#C
'''
.
.C99s   8 
AAspecr   c                      rt                     dk    rdS t           fddD                       rdS                      d          sd v sd v rdS t          t                                                   S )zCReject pip specs that contain URLs, paths, or shell metacharacters.   Fc              3      K   | ]}|v V  	d S r   r4   )r"   chrD   s     r   r$   z _spec_is_safe.<locals>.<genexpr>   s'      
R
R"2:
R
R
R
R
R
Rr   )	;|&`$
	\)-/.z://@)lenany
startswithr/   
_SAFE_SPECmatch)rD   s   `r   _spec_is_safer[      s     3t99s??u

R
R
R
R Q
R
R
RRR u'' 5D==C4KKu
  &&'''r   c                ^    t          j        d|           }|r|                    d          n| S )u   Extract the bare package name from a pip spec.

    ``"slack-bolt>=1.18.0,<2"`` → ``"slack-bolt"``
    ``"mautrix[encryption]>=0.20"`` → ``"mautrix"``
    z^([A-Za-z0-9_][A-Za-z0-9_.\-]*)   )rerZ   grouprD   ms     r   _pkg_name_from_specrb      s/     	3T::A$1771:::$r   c                l    t          j        d|           }|sdS | |                                d         S )u   Extract just the version-specifier portion of a pip spec.

    ``"honcho-ai==2.0.1"`` → ``"==2.0.1"``
    ``"mautrix[encryption]>=0.20,<1"`` → ``">=0.20,<1"``
    ``"package"`` → ``""`` (no version constraint)
    z6^[A-Za-z0-9_][A-Za-z0-9_.\-]*(?:\[[A-Za-z0-9_,\-]+\])? N)r^   rZ   endr`   s     r   _specifier_from_specrf      s9     	JDQQA r>r   c                b   t          |           }	 ddlm}m} n# t          $ r Y dS w xY w	  ||          }n# |$ r Y dS t
          $ r Y dS w xY wt          |           }|sdS 	 ddlm}m	} ddl
m}m}	 n# t          $ r Y dS w xY w	  |	|           ||          v S # ||t
          f$ r Y dS w xY w)a^  Is ``spec`` already satisfied in the current env?

    Checks both presence AND version. If the package is installed at a
    version outside the spec's range, returns False so the caller will
    upgrade/downgrade to the pinned version. This is what makes
    ``hermes update`` propagate pin bumps in :data:`LAZY_DEPS` to already-
    installed backends instead of silently leaving stale versions in place.

    If ``packaging`` is unavailable for any reason (it's a transitive of
    pip so this should never happen), we fall back to a presence-only check
    so we err on the side of "don't churn".
    r   PackageNotFoundErrorversionFT)InvalidSpecifierSpecifierSet)InvalidVersionVersion)rb   importlib.metadatari   rj   ImportErrorr?   rf   packaging.specifiersrk   rl   packaging.versionrm   rn   )
rD   pkgri   rj   	installed	spec_tailrk   rl   rm   rn   s
             r   _is_satisfiedrv     se    d
#
#CDDDDDDDDD   uuGCLL		   uu   uu %T**I tGGGGGGGG=========   ttwy!!\\)%<%<<<ni8   ttsE    
((8 A 	AA$A5 5
BBB B.-B.c                    t          |           }	 ddlm}m} n# t          $ r Y dS w xY w	  ||           dS # |$ r Y dS t
          $ r Y dS w xY w)zCheap presence-only check (package name installed at any version).

    Used by :func:`active_features` to detect backends the user has
    previously activated, regardless of whether the version pin moved.
    r   rh   FT)rb   ro   ri   rj   rp   r?   )rD   rs   ri   rj   s       r   _is_presentrx   :  s     d
#
#CDDDDDDDDD   uut   uu   uus!    
((9 A	AAi,  )timeoutspecsr   ry   intc                  | st          ddd          S t          t          j                  j        j        }i t
          j        dt          |          i}t          j	        d          }|r	 t          j        |ddg| dd||          }|j        dk    rt          d|j        pd|j        pd          S t                              d	|j                   n># t          j        t$          f$ r%}t                              d
|           Y d}~nd}~ww xY wt          j        ddg}	 t          j        |dgz   ddd          }|j        dk    rt%          d          n# t          j        t$          f$ rk 	 t          j        t          j        ddddgdddd           n># t          j        t          j        f$ r }t          ddd|           cY d}~cY S d}~ww xY wY nw xY w	 t          j        |dg| z   dd|          }t          |j        dk    |j        pd|j        pd          S # t          j        $ r}t          ddd|           cY d}~S d}~wt(          $ r}t          ddd|           cY d}~S d}~ww xY w)u   Install ``specs`` into the active venv using uv → pip → ensurepip ladder.

    Mirrors the strategy in ``hermes_cli.tools_config._pip_install`` but
    kept independent here so this module has no CLI dependency.
    Trd   VIRTUAL_ENVuvpipinstall)capture_outputtextry   envr   zuv pip install failed: %szuv invocation failed: %sNz-mz	--version   )r   r   ry   zpip not in venv	ensurepipz	--upgradez--default-pipx   )r   r   ry   checkFz(pip not available and ensurepip failed: zpip install timed out: pip install failed: )r.   r   sys
executableparentr;   r<   r   shutilwhich
subprocessrun
returncoder1   r2   loggerdebugTimeoutExpiredFileNotFoundErrorCalledProcessErrorr?   )	rz   ry   	venv_rootuv_envuv_binrepip_cmdprobes	            r   _venv_pip_installr   N  si     ,dB+++S^$$+2I:
:M3y>>::F \$F 
8		8	2E2#$V  A |q  %dAHNAHNKKKLL4ah????)+<= 	8 	8 	8LL3Q77777777	8 ~tU+GR{m#dB
 
 
 q  #$5666 !%'89 R R R	RN{KQ#$4     -z/HI 	R 	R 	R!%"PQ"P"PR R R R R R R R R R	R	 R	ENy)5))dG
 
 
 ala/RRPPP$ H H HeR)F1)F)FGGGGGGGG E E EeR)C)C)CDDDDDDDDEs   -AC 5 C D,DD$6E G2(FGG6G	G
GGGG AH& &I95II9I9I4.I94I9r   c                R    | t           vrt          d|           t           |          S )z=Return the registered specs for a feature, or raise KeyError.zUnknown lazy feature: )r
   KeyErrorr   s    r   feature_specsr     s/    i;;;<<<Wr   c                N    t          d t          |           D                       S )zCReturn the subset of specs for ``feature`` not currently installed.c              3  8   K   | ]}t          |          |V  d S r   )rv   r!   s     r   r$   z"feature_missing.<locals>.<genexpr>  s/      KKq-:J:JKKKKKKKr   )tupler   r   s    r   feature_missingr     s'    KKM'22KKKKKKr   promptr   Nonec               x   | t           vrt          | dd| d          t          |           }|sdS |D ]%}t          |          st          | |d|          &t	                      st          | |d          |rt
          j                                        rt
          j                                        rd	                    |          }	 t          d| d	| d
                                                                          }n# t          t          f$ r d}Y nw xY w|r|dvrt          | |d          t                              dd	                    |          |            t#          |          }|j        sD|j        p|j        pd                                }|r
|dd         }t          | |d|pd           	 ddlm} t-          |d          r|                                 n# t0          $ r Y nw xY wt          |           }	|	rt          | |	d          t                              d|            dS )u  Make sure all packages for ``feature`` are importable.

    If they're missing, attempts to install them in the active venv. Raises
    :class:`FeatureUnavailable` if the user has disabled lazy installs or
    if the install attempt fails.

    ``prompt``: when True (default) and stdin is a TTY, asks the user to
    confirm before installing. Non-interactive callers (gateway, cron,
    batch) get prompt=False and skip the confirmation — config flag is
    the gate in that case.
    r4   zfeature z not in LAZY_DEPS allowlistNz refusing to install unsafe spec z;lazy installs disabled (security.allow_lazy_installs=false)z, z	
Feature z requires: z)
Install into the active venv now? [Y/n] n>   yyeszuser declined install at promptz!Lazy-installing %s for feature %rr   rd   i0r   zno error outputr   _cache_clearzWinstall reported success but packages still not importable (may require Python restart)z$Lazy install complete for feature %r)r
   r   r   r[   rC   r   stdinisattyr1   r%   inputstriplowerEOFErrorKeyboardInterruptr   infor   r0   r2   ro   metadatahasattrr   r?   )
r   r   r   rD   r&   answerresultsnippet_mdstill_missings
             r   ensurer     s    i RJGJJJ
 
 	
 g&&G    T"" 	$;4;;  	  !! 
 WI
 
 	

  #)""$$ ):):)<)< IIg&&		<W < <9 < < <  eggeegg F +, 	 	 	FFF	 	fL00$"C   KK3SXXg5F5FPPPw''F> 

 =7FM7R>>@@ 	&effoG WA7#?.?AA
 
 	
((((((3'' 	    $G,,M 
 ]+
 
 	
 KK6@@@@@s$   :D DD*G- -
G:9G:c                8    | t           vrdS t          |            S )z8Return True if the feature's deps are already satisfied.F)r
   r   r   s    r   is_availabler     s#    iuw''''r   Optional[str]c                v    | t           vrdS t           |          }dd                    d |D                       z   S )zFReturn the ``pip install`` command a user could run manually, or None.Nzuv pip install r   c              3  4   K   | ]}t          |          V  d S r   r   r!   s     r   r$   z*feature_install_command.<locals>.<genexpr>  s(      '?'?AQ'?'?'?'?'?'?r   )r
   r%   )r   rz   s     r   feature_install_commandr     sB    itgEsxx'?'?'?'?'?????r   	list[str]c                     g } t                                           D ]3\  }}t          d |D                       r|                     |           4| S )a  Return the list of features the user has ever lazy-installed.

    A feature counts as "active" if at least one of its declared packages
    is currently installed in the venv (presence check, ignoring version).
    Features the user has never enabled stay quiet.

    Used by ``hermes update`` to figure out which lazy backends need a
    refresh pass when pins move in :data:`LAZY_DEPS`.
    c              3  4   K   | ]}t          |          V  d S r   )rx   r!   s     r   r$   z"active_features.<locals>.<genexpr>  s(      --!{1~~------r   )r
   itemsrW   append)activer   rz   s      r   active_featuresr     s]     F#//++ # #--u----- 	#MM'"""Mr   Fdict[str, str]c                d   i }t                      D ]}t          |          }|sd||<   	 t          ||            d||<   1# t          $ rG}dt	          |          v sdt	          |          v rd|j         ||<   nd|j         ||<   Y d}~}d}~wt          $ r}d| ||<   Y d}~d}~ww xY w|S )	uj  Re-run ``ensure`` for every feature the user has previously activated.

    Returns a ``{feature: status}`` map where status is one of:
        ``"current"``  — pins already satisfied, no install run
        ``"refreshed"`` — pins were stale, reinstall succeeded
        ``"failed: <reason>"`` — install attempt failed; caller decides
                                  whether to surface it (we don't raise)
        ``"skipped: <reason>"`` — gated off (config flag, user decline)

    Intended for ``hermes update``. Never raises; lazy-install failures
    here must not block the rest of the update flow.
    currentr   	refreshedzlazy installs disableddeclinedz	skipped: zfailed: N)r   r   r   r   r   r   r?   )r   resultsr   r   r   s        r   refresh_active_featuresr     s    !G"$$ . .!'** 	(GG	.76*****GG! 	9 	9 	9 (3q6611Z3q665I5I#9qx#9#9  #8ah#8#8  	. 	. 	.-!~~GG	.Ns#   A
B-=BB-B((B-importerCallable[[], dict[str, Any]]target_globalsdictc                   	 t          | |           n# t          t          f$ r Y dS w xY w	  |            }n# t          $ r Y dS w xY w|                    |           dS )a  Ensure a feature is installed, then rebind names into the caller's globals.

    Combines :func:`ensure` with a post-install import step that rebinds
    module-level names.  This eliminates the error-prone pattern of manually
    listing every global that needs updating after lazy-install.

    ``importer`` is a zero-arg callable that returns a dict of
    ``{name: value}`` for all symbols the caller needs rebound.  It is called
    only after :func:`ensure` succeeds (or if the packages are already
    installed).

    Returns True on success, False if deps couldn't be installed or imported.

    Example usage in a platform adapter::

        def check_slack_requirements() -> bool:
            if SLACK_AVAILABLE:
                return True
            def _import():
                from slack_bolt.async_app import AsyncApp
                from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler
                from slack_sdk.web.async_client import AsyncWebClient
                import aiohttp
                return {
                    "AsyncApp": AsyncApp,
                    "AsyncSocketModeHandler": AsyncSocketModeHandler,
                    "AsyncWebClient": AsyncWebClient,
                    "aiohttp": aiohttp,
                    "SLACK_AVAILABLE": True,
                }
            return ensure_and_bind("platform.slack", _import, globals(), prompt=False)
    r   FT)r   r   r?   rp   update)r   r   r   r   bindingss        r   ensure_and_bindr   .  s    Nwv&&&&&	*   uu8::   uu (###4s    ))
8 
AA)r   r/   )rD   r   r   r/   )rD   r   r   r   )rz   r   ry   r{   r   r.   )r   r   r   r   )r   r   r   r/   r   r   )r   r   r   r/   )r   r   r   r   )r   r   )r   r/   r   r   )
r   r   r   r   r   r   r   r/   r   r/   )*r*   
__future__r   loggingr;   r^   r   r   r   dataclassesr   pathlibr   typingr   r   r   	getLoggerr'   r   r
   r3   compilerY   RuntimeErrorr   r.   rC   r[   rb   rf   rv   rx   r   r   r   r   r   r   r   r   r   r4   r   r   <module>r      s,  1 1 1f # " " " " "  				 				      



 ! ! ! ! ! !       * * * * * * * * * *		8	$	$[) 0	[) +[) %[) 1[) /[). $/[)0 -1[)6  7[)D (E[)J *K[)L 4M[)R AS[)^ K_[)`  a[)j  k[)x  y[)B  C[)N (-)
 8 2o[) [) [)	 [ [ [ [B RZ	 

 
 
 
 
 
 
 
, $          &( ( ( (% % % %   ) ) ) )X   ( AD 6E 6E 6E 6E 6E 6E|   L L L L
 ,0 QA QA QA QA QA QAh( ( ( (@ @ @ @   " /4      N 2 2 2 2 2 2 2 2r   