
    i                    R   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	Z	ddl
Z
ddlZddlZddlmZmZ ddlmZ ddlmZmZmZmZmZmZmZ ddlmZ ddlmZ dd	lmZ dDdZ 	 ddl!Z!n# e"$ r dZ!Y nw xY w ej#        e$          Z%h dZ&de'd<   dZ(dZ)dEdZ*dFdZ+dGdZ,h dZ-de'd<   e G d d                      Z.e G d d                      Z/ G d  d!          Z0 G d" d#          Z1da2d$e'd%<   dHd&Z3dIdJd*Z4dKd/Z5	 	 	 dLdMd8Z6dIdNd9Z7d: Z8dOd<Z9d=Z:dPd?Z;dQdAZ<dRdCZ=dS )Su  
Hermes Plugin System
====================

Discovers, loads, and manages plugins from four sources:

1. **Bundled plugins** – ``<repo>/plugins/<name>/`` (shipped with hermes-agent;
   ``memory/`` and ``context_engine/`` subdirs are excluded — they have their
   own discovery paths)
2. **User plugins**   – ``~/.hermes/plugins/<name>/``
3. **Project plugins** – ``./.hermes/plugins/<name>/`` (opt-in via
   ``HERMES_ENABLE_PROJECT_PLUGINS``)
4. **Pip plugins**     – packages that expose the ``hermes_agent.plugins``
   entry-point group.

Later sources override earlier ones on name collision, so a user or project
plugin with the same name as a bundled plugin replaces it.

Each directory plugin must contain a ``plugin.yaml`` manifest **and** an
``__init__.py`` with a ``register(ctx)`` function.

Lifecycle hooks
---------------
Plugins may register callbacks for any of the hooks in ``VALID_HOOKS``.
The agent core calls ``invoke_hook(name, **kwargs)`` at the appropriate
points.

Tool registration
-----------------
``PluginContext.register_tool()`` delegates to ``tools.registry.register()``
so plugin-defined tools appear alongside the built-in tools.
    )annotationsN)	dataclassfield)Path)AnyCallableDictListOptionalSetUnion)get_hermes_homeenv_var_enabled)cfg_getreturnr   c                     t          j        d          } | rt          |           S t          t                                                    j        j        dz  S )zLocate the bundled ``plugins/`` directory.

    Honours ``HERMES_BUNDLED_PLUGINS`` (set by the Nix wrapper / packaged
    installs) so read-only store paths are consulted first.  Falls back to
    the in-repo path used during development.
    HERMES_BUNDLED_PLUGINSplugins)osgetenvr   __file__resolveparent)env_overrides    7/home/ubuntu/.hermes/hermes-agent/hermes_cli/plugins.pyget_bundled_plugins_dirr   7   sM     9566L "L!!!>>!!##*1I==    >   pre_llm_callpost_llm_callpre_tool_callsubagent_stopon_session_endpost_tool_callpre_api_requeston_session_reseton_session_startpost_api_requeston_session_finalizepre_approval_requestpre_gateway_dispatchtransform_tool_resultpost_approval_responsetransform_terminal_outputzSet[str]VALID_HOOKSzhermes_agent.pluginshermes_pluginsnamestrboolc                     t          |           S )z<Return True when an env var is set to a truthy opt-in value.r   )r1   s    r   _env_enabledr5   y   s    4   r   setc                     	 ddl m}   |             }t          |ddg           }t          |t                    rt          |          nt                      S # t          $ r t                      cY S w xY w)zRead the disabled plugins list from config.yaml.

    Kept for backward compat and explicit deny-list semantics. A plugin
    name in this set will never load, even if it appears in
    ``plugins.enabled``.
    r   load_configr   disabled)default)hermes_cli.configr9   r   
isinstancelistr6   	Exception)r9   configr:   s      r   _get_disabled_pluginsrA   ~   s    11111169j"EEE *8T : :Es8}}}E   uus   AA A21A2Optional[set]c                     	 ddl m}   |             }|                    d          }t          |t                    sdS d|vrdS |                    d          }t          |t
                    sdS t          |          S # t          $ r Y dS w xY w)uL  Read the enabled-plugins allow-list from config.yaml.

    Plugins are opt-in by default — only plugins whose name appears in
    this set are loaded. Returns:

    * ``None`` — the key is missing or malformed. Callers should treat
      this as "nothing enabled yet" (the opt-in default); the first
      ``migrate_config`` run populates the key with a grandfathered set
      of currently-installed user plugins so existing setups don't
      break on upgrade.
    * ``set()`` — an empty list was explicitly set; nothing loads.
    * ``set(...)`` — the concrete allow-list.
    r   r8   r   Nenabled)r<   r9   getr=   dictr>   r6   r?   )r9   r@   plugins_cfgrD   s       r   _get_enabled_pluginsrH      s    111111jj+++t,, 	4K''4//),,'4(( 	47||   tts"   :A? A? *A? 0A? ?
BB>   backendplatform	exclusive
standalone_VALID_PLUGIN_KINDSc                      e Zd ZU dZded<   dZded<   dZded<   dZded<    ee	          Z
d	ed
<    ee	          Zded<    ee	          Zded<   dZded<   dZded<   dZded<   dZded<   dS )PluginManifestz0Parsed representation of a plugin.yaml manifest.r2   r1    versiondescriptionauthordefault_factoryz List[Union[str, Dict[str, Any]]]requires_env	List[str]provides_toolsprovides_hookssourceNOptional[str]pathrL   kindkey)__name__
__module____qualname____doc____annotations__rQ   rR   rS   r   r>   rV   rX   rY   rZ   r\   r]   r^    r   r   rO   rO      s         ::IIIGKF5:U45P5P5PLPPPP %d ; ; ;N;;;; %d ; ; ;N;;;;FD  D CMMMMMMr   rO   c                      e Zd ZU dZded<   dZded<    ee          Zded	<    ee          Z	ded
<    ee          Z
ded<   dZded<   dZded<   dS )LoadedPluginz)Runtime state for a single loaded plugin.rO   manifestNzOptional[types.ModuleType]modulerT   rW   tools_registeredhooks_registeredcommands_registeredFr3   rD   r[   error)r_   r`   ra   rb   rc   rh   r   r>   ri   rj   rk   rD   rl   rd   r   r   rf   rf      s         33)-F----"'%"="="====="'%"="="=====%*U4%@%@%@@@@@GEr   rf   c                      e Zd ZdZd9dZ	 	 	 	 	 d:d;dZd<d=dZ	 	 d>d?d#Z	 	 d@dAd%ZdBd(Z	dCd)Z
dCd*Z	 	 	 dDdEd2ZdFd5Z	 dGdHd8ZdS )IPluginContextz=Facade given to plugins so they can register tools and hooks.rg   rO   manager'PluginManager'c                "    || _         || _        d S N)rg   _manager)selfrg   ro   s      r   __init__zPluginContext.__init__   s     r   NFrP   r1   r2   toolsetschemarF   handlerr   check_fnCallable | NonerV   list | Noneis_asyncr3   rR   emojir   Nonec
                    ddl m}
 |
                    |||||||||		  	         | j        j                            |           t                              d| j        j	        |           dS )zKRegister a tool in the global registry **and** track it as plugin-provided.r   registry)	r1   rv   rw   rx   ry   rV   r|   rR   r}   zPlugin %s registered tool: %sN)
tools.registryr   registerrs   _plugin_tool_namesaddloggerdebugrg   r1   )rt   r1   rv   rw   rx   ry   rV   r|   rR   r}   r   s              r   register_toolzPluginContext.register_tool   s     	,+++++%# 	 
	
 
	
 
	
 	(,,T2224dm6H$OOOOOr   usercontentrolec                   | j         j        }|t                              d           dS |dk    r|nd| d| }t	          |dd          r|j                            |           n|j                            |           dS )	a  Inject a message into the active conversation.

        If the agent is idle (waiting for user input), this starts a new turn.
        If the agent is running, this interrupts and injects the message.

        This enables plugins (e.g. remote control viewers, messaging bridges)
        to send messages into the conversation from external sources.

        Returns True if the message was queued successfully.
        Nz@inject_message: no CLI reference (not available in gateway mode)Fr   [z] _agent_runningT)rs   _cli_refr   warninggetattr_interrupt_queueput_pending_input)rt   r   r   climsgs        r   inject_messagezPluginContext.inject_message  s     m$;NN]^^^5gg-B-B-B-B-B3(%00 	( $$S)))) ""3'''tr   helpsetup_fn
handler_fnc                    |||||| j         j        d| j        j        |<   t                              d| j         j        |           dS )a  Register a CLI subcommand (e.g. ``hermes honcho ...``).

        The *setup_fn* receives an argparse subparser and should add any
        arguments/sub-subparsers.  If *handler_fn* is provided it is set
        as the default dispatch function via ``set_defaults(func=...)``.)r1   r   rR   r   r   pluginz$Plugin %s registered CLI command: %sN)rg   r1   rs   _cli_commandsr   r   )rt   r1   r   r   r   rR   s         r   register_cli_commandz"PluginContext.register_cli_command-  sV     & $m(-
 -
#D) 	;T]=OQUVVVVVr   	args_hintc                <   |                                                                                     d                              dd          }|s't                              d| j        j                   dS 	 ddlm	}  ||          (t                              d| j        j        |           dS n# t          $ r Y nw xY w||pd	| j        j        |pd
                                d| j        j        |<   t                              d| j        j        |           dS )u  Register a slash command (e.g. ``/lcm``) available in CLI and gateway sessions.

        The handler signature is ``fn(raw_args: str) -> str | None``.
        It may also be an async callable — the gateway dispatch handles both.

        Unlike ``register_cli_command()`` (which creates ``hermes <subcommand>``
        terminal commands), this registers in-session slash commands that users
        invoke during a conversation.

        ``args_hint`` is an optional short string (e.g. ``"<file>"`` or
        ``"dias:7 formato:json"``) used by gateway adapters to surface the
        command with an argument field — for example Discord's native slash
        command picker. Plugin commands without ``args_hint`` register as
        parameterless in Discord and still accept trailing text when invoked
        as free-form chat.

        Names conflicting with built-in commands are rejected with a warning.
        / -z;Plugin '%s' tried to register a command with an empty name.Nr   )resolve_commandz^Plugin '%s' tried to register command '/%s' which conflicts with a built-in command. Skipping.zPlugin commandrP   )rx   rR   r   r   z!Plugin %s registered command: /%s)lowerstriplstripreplacer   r   rg   r1   hermes_cli.commandsr   r?   rs   _plugin_commandsr   )rt   r1   rx   rR   r   cleanr   s          r   register_commandzPluginContext.register_commandF  sH   2 

""$$++C0088cBB 	NNM"   F
	;;;;;;u%%19M&  
  2  	 	 	D	 &:*:m(#/r0022	1
 1
&u- 	8$-:LeTTTTTs   87B2 2
B?>B?	tool_nameargsc                    ddl m} d|vr(| j        j        }|rt	          |dd          nd}|||d<    |j        ||fi |S )u  Dispatch a tool call through the registry, with parent agent context.

        This is the public interface for plugin slash commands that need to call
        tools like ``delegate_task`` without reaching into framework internals.
        The parent agent (if available) is resolved automatically — plugins never
        need to access the agent directly.

        Args:
            tool_name: Registry name of the tool (e.g. ``"delegate_task"``).
            args: Tool arguments dict (same as what the model would pass).
            **kwargs: Extra keyword args forwarded to the registry dispatch.

        Returns:
            JSON string from the tool handler (same format as model tool calls).
        r   r   parent_agentagentN)r   r   rs   r   r   dispatch)rt   r   r   kwargsr   r   r   s          r   dispatch_toolzPluginContext.dispatch_tool~  su      	,+++++
 ''-(C36@GC$///DE ).~& x D;;F;;;r   c                T   | j         j        't                              d| j        j                   dS ddlm} t          ||          s't                              d| j        j                   dS || j         _        t          	                    d| j        j        |j                   dS )a%  Register a context engine to replace the built-in ContextCompressor.

        Only one context engine plugin is allowed. If a second plugin tries
        to register one, it is rejected with a warning.

        The engine must be an instance of ``agent.context_engine.ContextEngine``.
        NzyPlugin '%s' tried to register a context engine, but one is already registered. Only one context engine plugin is allowed.r   )ContextEnginezbPlugin '%s' tried to register a context engine that does not inherit from ContextEngine. Ignoring.z)Plugin '%s' registered context engine: %s)
rs   _context_enginer   r   rg   r1   agent.context_enginer   r=   info)rt   enginer   s      r   register_context_enginez%PluginContext.register_context_engine  s     =(4NNQ"  
 F666666&-00 	NN8"  
 F(.%7M	
 	
 	
 	
 	
r   c                    ddl m} ddlm} t	          ||          s't
                              d| j        j                   dS  ||           t
          	                    d| j        j        |j                   dS )a=  Register an image generation backend.

        ``provider`` must be an instance of
        :class:`agent.image_gen_provider.ImageGenProvider`. The
        ``provider.name`` attribute is what ``image_gen.provider`` in
        ``config.yaml`` matches against when routing ``image_generate``
        tool calls.
        r   )ImageGenProvider)register_providerzjPlugin '%s' tried to register an image_gen provider that does not inherit from ImageGenProvider. Ignoring.Nz-Plugin '%s' registered image_gen provider: %s)
agent.image_gen_providerr   agent.image_gen_registryr   r=   r   r   rg   r1   r   )rt   providerr   r   s       r   register_image_gen_providerz)PluginContext.register_image_gen_provider  s     	>=====>>>>>>($455 	NN?"  
 F(###;M	
 	
 	
 	
 	
r   labeladapter_factoryvalidate_configrequired_envinstall_hintentry_kwargsr   c                0   ddl m}	m}
 |                    d| j        j                    |
d||||||pg |dd|}|	                    |           | j        j        	                    |           t                              d| j        j        |           dS )	u  Register a gateway platform adapter.

        The adapter_factory receives a ``PlatformConfig`` and returns a
        ``BasePlatformAdapter`` subclass instance.  The gateway calls
        ``check_fn()`` before instantiation to verify dependencies.

        Extra keyword arguments are forwarded to ``PlatformEntry`` (e.g.
        ``setup_fn``, ``emoji``, ``allowed_users_env``, ``platform_hint``).
        Unknown keys raise TypeError from the dataclass constructor.

        Example::

            ctx.register_platform(
                name="irc",
                label="IRC",
                adapter_factory=lambda cfg: IRCAdapter(cfg),
                check_fn=lambda: True,
                emoji="💬",
                setup_fn=irc_interactive_setup,
            )
        r   )platform_registryPlatformEntryplugin_namer   )r1   r   r   ry   r   r   r   rZ   z!Plugin %s registered platform: %sNrd   )gateway.platform_registryr   r   
setdefaultrg   r1   r   rs   _plugin_platform_namesr   r   r   )rt   r1   r   r   ry   r   r   r   r   r   r   entrys               r   register_platformzPluginContext.register_platform  s    @ 	ONNNNNNNt}/ABBB 

++%+%

 

 

 

 	""5))),00666/M	
 	
 	
 	
 	
r   	hook_namecallbackc           
     b   |t           vrLt                              d| j        j        |d                    t          t                                          | j        j        	                    |g           
                    |           t                              d| j        j        |           dS )zRegister a lifecycle hook callback.

        Unknown hook names produce a warning but are still stored so
        forward-compatible plugins don't break.
        z4Plugin '%s' registered unknown hook '%s' (valid: %s), zPlugin %s registered hook: %sN)r/   r   r   rg   r1   joinsortedrs   _hooksr   appendr   )rt   r   r   s      r   register_hookzPluginContext.register_hook  s     K''NN"		&--..   	''	266==hGGG4dm6H)TTTTTr   r\   r   c                   ddl m} d|v r t          d| d| j        j         d          |r|                    |          st          d| d          |                                st          d	|           | j        j         d| }|| j        j        ||d
| j        j	        |<   t                              d| j        j        |           dS )u  Register a read-only skill provided by this plugin.

        The skill becomes resolvable as ``'<plugin_name>:<name>'`` via
        ``skill_view()``.  It does **not** enter the flat
        ``~/.hermes/skills/`` tree and is **not** listed in the system
        prompt's ``<available_skills>`` index — plugin skills are
        opt-in explicit loads only.

        Raises:
            ValueError: if *name* contains ``':'`` or invalid characters.
            FileNotFoundError: if *path* does not exist.
        r   )_NAMESPACE_RE:zSkill name 'zG' must not contain ':' (the namespace is derived from the plugin name 'z' automatically).zInvalid skill name 'z'. Must match [a-zA-Z0-9_-]+.zSKILL.md not found at )r\   r   	bare_namerR   zPlugin %s registered skill: %sN)agent.skill_utilsr   
ValueErrorrg   r1   matchexistsFileNotFoundErrorrs   _plugin_skillsr   r   )rt   r1   r\   rR   r   	qualifieds         r   register_skillzPluginContext.register_skill#  s0   $ 	433333$;;:t : :M&: : :  
  	=..t44 	JtJJJ   {{}} 	E#$CT$C$CDDD})22D22	m(&	3
 3
$Y/ 	,M		
 	
 	
 	
 	
r   )rg   rO   ro   rp   )NNFrP   rP   )r1   r2   rv   r2   rw   rF   rx   r   ry   rz   rV   r{   r|   r3   rR   r2   r}   r2   r   r~   )r   )r   r2   r   r2   r   r3   )NrP   )r1   r2   r   r2   r   r   r   rz   rR   r2   r   r~   )rP   rP   )
r1   r2   rx   r   rR   r2   r   r2   r   r~   )r   r2   r   rF   r   r2   r   r~   )NNrP   )r1   r2   r   r2   r   r   ry   r   r   rz   r   r{   r   r2   r   r   r   r~   )r   r2   r   r   r   r~   )rP   )r1   r2   r\   r   rR   r2   r   r~   )r_   r`   ra   rb   ru   r   r   r   r   r   r   r   r   r   r   rd   r   r   rn   rn      s]       GG        %)$(P P P P P>    B '+W W W W W: 4U 4U 4U 4U 4Up< < < <>
 
 
 
@
 
 
 
B ,0$(4
 4
 4
 4
 4
pU U U U. 	+
 +
 +
 +
 +
 +
 +
r   rn   c                      e Zd ZdZd/dZd0d1dZ	 d2d3dZd4dZd5dZd6dZ	d7dZ
d8dZd8d Zd9d%Zd:d'Zd;d*Zd<d-Zd=d.Zd	S )>PluginManagerz;Central manager that discovers, loads, and invokes plugins.r   r~   c                    i | _         i | _        t                      | _        t                      | _        i | _        d | _        i | _        d| _        d | _	        i | _
        d S )NF)_pluginsr   r6   r   r   r   r   r   _discoveredr   r   )rt   s    r   ru   zPluginManager.__init__X  s]    1313,/EE03#.0#13!&9;r   Fforcer3   c           	        | j         r|sdS |r| j                                         | j                                         | j                                         | j                                         | j                                         | j                                         d| _        d| _         g }t                      }|
                    |                     |dh d                     |
                    |                     |dz  d                     t                      dz  }|
                    |                     |d	                     t          d
          rCt          j                    dz  dz  }|
                    |                     |d                     |
                    |                                            t#                      }t%                      }i }|D ]}	|	||	j        p|	j        <   |                                D ]I}	|	j        p|	j        }
|
|v s	|	j        |v r>t-          |	d          }d|_        || j        |
<   t0                              d|
           \|	j        dk    r>t-          |	d          }d|_        || j        |
<   t0                              d|
           |	j        dk    r|	j        dv r|                     |	           |duo|
|v p|	j        |v }|sRt-          |	d          }d                    |
          |_        || j        |
<   t0                              d|
           4|                     |	           K|r^t0                              dt?          | j                  tA          d | j                                        D                                  dS dS )a  Scan all plugin sources and load each plugin found.

        When ``force`` is true, clear cached discovery state first so config
        changes or newly-added bundled backends become visible in long-lived
        sessions without requiring a full agent restart.
        NTbundled>   memory	platformscontext_engine)rZ   
skip_namesr   )rZ   r   r   HERMES_ENABLE_PROJECT_PLUGINSz.hermesprojectF)rg   rD   zdisabled via configzSkipping disabled plugin '%s'rK   u<   exclusive plugin — activate via <category>.provider configz8Skipping '%s' (exclusive, handled by category discovery))rI   rJ   zBnot enabled in config (run `hermes plugins enable {}` to activate)z&Skipping '%s' (not in plugins.enabled)z/Plugin discovery complete: %d found, %d enabledc              3  (   K   | ]}|j         	d V  dS )   N)rD   ).0ps     r   	<genexpr>z2PluginManager.discover_and_load.<locals>.<genexpr>  s)      CC!CACCCCCCr   )!r   r   clearr   r   r   r   r   r   r   extend_scan_directoryr   r5   r   cwd_scan_entry_pointsrA   rH   r^   r1   valuesrf   rl   r   r   r]   rZ   _load_pluginformatr   lensum)rt   r   	manifestsrepo_pluginsuser_dirproject_dirr:   rD   winnersrg   
lookup_keyloaded
is_enableds                r   discover_and_loadzPluginManager.discover_and_loadi  s7     	E 	F 	(M!!!K#))+++$$&&&!'')))%%'''#'D *,	 /00   DDD !  	
 	
 	
 	  !;I NN	
 	
 	

 #$$y0--hv-FFGGG 788 	R(**y09<KT11+i1PPQQQ 	0022333 )**&((-/! 	> 	>H5=GHL1HM22(( :	( :	(H!6J X%%()B)B%xGGG4,2j)<jIII
 }++%xGGGR  -3j)N    )++AX0X0X!!(+++ t# H7*Fhmw.F   
%xGGGXVJ''  -3j)<j   h'''' 	KKADM""CCt}3355CCCCC    	 	r   Nr\   r   rZ   r2   r   Optional[Set[str]]List[PluginManifest]c                6    |                      |||dd          S )u  Read ``plugin.yaml`` manifests from subdirectories of *path*.

        Supports two layouts, mixed freely:

        * **Flat** — ``<root>/<plugin-name>/plugin.yaml``. Key is
          ``<plugin-name>`` (e.g. ``disk-cleanup``).
        * **Category** — ``<root>/<category>/<plugin-name>/plugin.yaml``,
          where the ``<category>`` directory itself has no ``plugin.yaml``.
          Key is ``<category>/<plugin-name>`` (e.g. ``image_gen/openai``).
          Depth is capped at two segments.

        *skip_names* is an optional allow-list of names to ignore at the
        top level (kept for back-compat; the current call sites no longer
        pass it now that categories are first-class).
        rP   r   r   prefixdepth)_scan_directory_level)rt   r\   rZ   r   s       r   r   zPluginManager._scan_directory  s,    * ))&Z! * 
 
 	
r   r  r  intc               ^   g }|                                 s|S t          |                                          D ]}|                                 s|dk    r|r
|j        |v r)|dz  }|                                s|dz  }|                                r0|                     ||||          }	|	|                    |	           |dk    rt                              d|           |r| d|j         n|j        }
|	                    | 
                    ||d|
|dz                        |S )	a  Recursive implementation of :meth:`_scan_directory`.

        ``prefix`` is the category path already accumulated ("" at root,
        "image_gen" one level in). ``depth`` is the recursion depth; we
        cap at 2 so ``<root>/a/b/c/`` is ignored.
        r   zplugin.yamlz
plugin.ymlNr   z/Skipping %s (no plugin.yaml, depth cap reached)r   r  )is_dirr   iterdirr1   r   _parse_manifestr   r   r   r   r  )rt   r\   rZ   r   r  r  r  childmanifest_filerg   
sub_prefixs              r   r  z#PluginManager._scan_directory_level  s{    +-	{{}} 	DLLNN++ !	 !	E<<>> zzjzUZ:-E-E!M1M '')) 5 % 4##%% //!5&&  '$$X...
 zzNPUVVV5;KF11UZ111J**#%!) +      r   r  
plugin_dirOptional[PluginManifest]c                   	 t           t                              d|           dS t          j        |                                          pi }|                    d|j                  }|r| d|j         n|}|                    dd          }t          |t                    sd}|	                                
                                }	|	t          vrDt                              d||d                    t          t                                         d}	|	dk    rrd|vrn|d	z  }
|
                                rU	 |
                    d
          dd         }d|v sd|v rd}	t                              d|           n# t           $ r Y nw xY wt#          |t          |                    dd                    |                    dd          |                    dd          |                    dg           |                    dg           |                    dg           |t          |          |	|          S # t           $ r'}t                              d||           Y d}~dS d}~ww xY w)zParse a single ``plugin.yaml`` into a :class:`PluginManifest`.

        Returns ``None`` on parse failure (logs a warning).
        Nu'   PyYAML not installed – cannot load %sr1   r   r]   rL   zBPlugin %s: unknown kind '%s' (valid: %s); treating as 'standalone'r   __init__.pyr   )errorsi    register_memory_providerMemoryProviderrK   zAPlugin %s: detected memory provider, treating as kind='exclusive'rQ   rP   rR   rS   rV   rX   rY   )r1   rQ   rR   rS   rV   rX   rY   rZ   r\   r]   r^   zFailed to parse %s: %s)yamlr   r   	safe_load	read_textrE   r1   r=   r2   r   r   rM   r   r   r   r   r?   rO   )rt   r  r  rZ   r  datar1   r^   raw_kindr]   	init_filesource_textexcs                r   r  zPluginManager._parse_manifestE  s   ;	|H-XXXt>-"9"9";";<<BD88FJO44D39CV//jo///tCxx55Hh,, ('>>##))++D...X499V4G-H-H#I#I   $ |##d(:(:&6	##%% &/&9&9&9&K&KETE&R6+EE/;>>#.D"LL!? #  
 %    "DHHY3344 HH]B77xx"--!XXnb99#xx(8"==#xx(8"==__     	 	 	NN3]CHHH44444	sC   "H= DH= =AF  H= 
FH= FB.H= =
I.I))I.c                   g }	 t           j                                        }t          |d          r|                    t
                    }n=t          |t                    r|                    t
          g           }nd |D             }|D ]9}t          |j
        d|j        |j
                  }|                    |           :n2# t          $ r%}t                              d|           Y d}~nd}~ww xY w|S )z7Check ``importlib.metadata`` for pip-installed plugins.selectgroupc                2    g | ]}|j         t          k    |S rd   r/  ENTRY_POINTS_GROUPr   eps     r   
<listcomp>z4PluginManager._scan_entry_points.<locals>.<listcomp>  s%    PPPB=O1O1OR1O1O1Or   
entrypoint)r1   rZ   r\   r^   zEntry-point scan failed: %sN)	importlibmetadataentry_pointshasattrr-  r2  r=   rF   rE   rO   r1   valuer   r?   r   r   )rt   r  eps	group_epsr4  rg   r+  s          r   r   z PluginManager._scan_entry_points  s%   *,		=$1133CsH%% QJJ-?J@@		C&& QGG$6;;		PP#PPP	 + +)'	     ****+  	= 	= 	=LL6<<<<<<<<	= s   CC 
C7C22C7rg   rO   c                6    t                    }	 j        dv r                               }n                               }||_        t          |dd          }|(d|_        t                              dj	                   nt                     } ||            fd j        D             |_        t          d  j                                        D             d	  j                                        D             z
            |_         fd
 j        D             |_        d|_        nL# t*          $ r?}t-          |          |_        t                              dj	        |           Y d}~nd}~ww xY w| j        j        pj	        <   dS )z?Import a plugin module and call its ``register(ctx)`` function.)rg   )r   r   r   r   Nzno register() functionz&Plugin '%s' has no register() functionc                ^    g | ])}|d  j                                         D             v'|*S )c                ,    h | ]\  }}|j         D ]}|S rd   )ri   )r   r1   r   ns       r   	<setcomp>z8PluginManager._load_plugin.<locals>.<listcomp>.<setcomp>  sF     ! ! !#D!!"!3! !  ! ! ! !r   )r   items)r   trt   s     r   r5  z.PluginManager._load_plugin.<locals>.<listcomp>  s`     + + + ! !'+}':':'<'<! ! !     r   c                    h | ]	\  }}||
S rd   rd   )r   hcbss      r   rB  z-PluginManager._load_plugin.<locals>.<setcomp>  s2       "As  r   c                ,    h | ]\  }}|j         D ]}|S rd   )rj   )r   r1   r   rF  s       r   rB  z-PluginManager._load_plugin.<locals>.<setcomp>  sF       #D!!"!3      r   c                f    g | ]-}j         |                             d           j        k    +|.S )r   )r   rE   r1   )r   crg   rt   s     r   r5  z.PluginManager._load_plugin.<locals>.<listcomp>  sF     . . .,Q/33H==NN NNNr   TzFailed to load plugin '%s': %s)rf   rZ   _load_directory_module_load_entrypoint_modulerh   r   rl   r   r   r1   rn   r   ri   r>   r   rC  r   rj   r   rk   rD   r?   r2   r^   )rt   rg   r  rh   register_fnctxr+  s   ``     r   r  zPluginManager._load_plugin  s   x000,	Q"@@@44X>>55h??"FM "&*d;;K"7GWWWW#Hd33C   + + + +#6+ + +' +/ &*k&7&7&9&9  
 '+}':':'<'<  	+ +'. . . . .#4. . .* "& 	Q 	Q 	Qs88FLNN;X]CPPPPPPPP	Q 8>hl3hm444s   D"D7 7
F 5E;;F types.ModuleTypec                   t          |j                  }|dz  }|                                st          d|           t          t
          j        vr@t          j        t                    }g |_	        t          |_
        |t
          j        t          <   |j        p|j        }|                    dd                              dd          }t           d| }t          j                            ||t#          |          g          }||j        t'          d
|           t          j                            |          }	||	_
        t#          |          g|	_	        |	t
          j        |<   |j                            |	           |	S )a/  Import a directory-based plugin as ``hermes_plugins.<slug>``.

        The module slug is derived from ``manifest.key`` so category-namespaced
        plugins (``image_gen/openai``) import as
        ``hermes_plugins.image_gen__openai`` without colliding with any
        future ``tts/openai``.
        r   zNo __init__.py in r   __r   _.)submodule_search_locationsNzCannot create module spec for )r   r\   r   r   
_NS_PARENTsysmodulestypes
ModuleType__path____package__r^   r1   r   r7  utilspec_from_file_locationr2   loaderImportErrormodule_from_specexec_module)
rt   rg   r  r)  ns_pkgr^   slugmodule_namespecrh   s
             r   rK  z$PluginManager._load_directory_module  sn    (-((
.	!! 	G#$E$E$EFFF S[((%j11F FO!+F&,CK
#l+hm{{3%%--c377#,,d,,~55(+J'8 6 
 

 <4;.JyJJKKK0066(z??+#)K '''r   c                   t           j                                        }t          |d          r|                    t
                    }n=t          |t                    r|                    t
          g           }nd |D             }|D ](}|j	        |j	        k    r|
                                c S )t          d|j	         dt
           d          )z:Load a pip-installed plugin via its entry-point reference.r-  r.  c                2    g | ]}|j         t          k    |S rd   r1  r3  s     r   r5  z9PluginManager._load_entrypoint_module.<locals>.<listcomp>  s%    LLLRX9K-K-K-K-K-Kr   zEntry point 'z' not found in group '')r7  r8  r9  r:  r-  r2  r=   rF   rE   r1   loadr_  )rt   rg   r<  r=  r4  s        r   rL  z%PluginManager._load_entrypoint_module	  s     --//3!! 	M

);
<<IIT"" 	M 2B77IILLcLLLI 	! 	!Bw(-''wwyy    ( VHMVVASVVV
 
 	
r   r   r   r   	List[Any]c                *   | j                             |g           }g }|D ]r}	  |di |}||                    |           ## t          $ rC}t                              d|t          |dt          |                    |           Y d}~kd}~ww xY w|S )u   Call all registered callbacks for *hook_name*.

        Each callback is wrapped in its own try/except so a misbehaving
        plugin cannot break the core agent loop.

        Returns a list of non-``None`` return values from callbacks.

        For ``pre_llm_call``, callbacks may return a dict describing
        context to inject into the current turn's user message::

            {"context": "recalled text..."}
            "recalled text..."          # plain string, equivalent

        Context is ALWAYS injected into the user message, never the
        system prompt.  This preserves the prompt cache prefix — the
        system prompt stays identical across turns so cached tokens
        are reused.  All injected context is ephemeral — never
        persisted to session DB.
        Nz Hook '%s' callback %s raised: %sr_   rd   )r   rE   r   r?   r   r   r   repr)rt   r   r   	callbacksresultscbretr+  s           r   invoke_hookzPluginManager.invoke_hook  s    ( KOOIr22	 	 	B
bll6ll?NN3'''   6B
DHH55	        s   A
B9BBList[Dict[str, Any]]c                   g }t          | j                                                  D ]\  }}|                    |j        j        |j        j        p|j        j        |j        j        |j        j        |j        j	        |j        j
        |j        t          |j                  t          |j                  t          |j                  |j        d           |S )z7Return a list of info dicts for all discovered plugins.)r1   r^   r]   rQ   rR   rZ   rD   toolshookscommandsrl   )r   r   rC  r   rg   r1   r^   r]   rQ   rR   rZ   rD   r  ri   rj   rk   rl   )rt   resultr^   r  s       r   list_pluginszPluginManager.list_pluginsG  s    ')!$-"5"5"7"788 	 	KCMM"O0!?.F&/2F"O0%6#)?#>$o4%~ !899 !899 #F$> ? ?#\     r   qualified_nameOptional[Path]c                N    | j                             |          }|r|d         ndS )z>Return the ``Path`` to a plugin skill's SKILL.md, or ``None``.r\   N)r   rE   )rt   ry  r   s      r   find_plugin_skillzPluginManager.find_plugin_skill`  s+    #''77 %/uV}}4/r   r   rW   c                r    | dt          fd| j                                        D                       S )zCReturn sorted bare names of all skills registered by *plugin_name*.r   c              3  X   K   | ]$\  }}|                               |d          V  %dS )r   N)
startswith)r   qner  s      r   r   z3PluginManager.list_plugin_skills.<locals>.<genexpr>h  sQ       
 
A}}V$$
kN
 
 
 
 
 
r   )r   r   rC  )rt   r   r  s     @r   list_plugin_skillsz PluginManager.list_plugin_skillse  sX    """ 
 
 
 
,2244
 
 
 
 
 	
r   c                <    | j                             |d           dS )z>Remove a stale registry entry (silently ignores missing keys).N)r   pop)rt   ry  s     r   remove_plugin_skillz!PluginManager.remove_plugin_skilln  s!    55555r   r   Fr   r3   r   r~   rr   )r\   r   rZ   r2   r   r  r   r  )r\   r   rZ   r2   r   r  r  r2   r  r  r   r  )
r  r   r  r   rZ   r2   r  r2   r   r  )r   r  )rg   rO   r   r~   )rg   rO   r   rO  r   r2   r   r   r   rj  )r   rr  )ry  r2   r   rz  )r   r2   r   rW   )ry  r2   r   r~   )r_   r`   ra   rb   ru   r  r   r  r  r   r  rK  rL  rq  rx  r|  r  r  rd   r   r   r   r   U  sP       EE< < < <"E E E E E^ *.	
 
 
 
 
26 6 6 6pF F F FX   <2> 2> 2> 2>h$ $ $ $L
 
 
 
," " " "P   20 0 0 0

 
 
 
6 6 6 6 6 6r   r   zOptional[PluginManager]_plugin_managerc                 :    t           t                      a t           S )z>Return (and lazily create) the global PluginManager singleton.)r  r   rd   r   r   get_plugin_managerr  z  s     '//r   Fr   r~   c                J    t                                          |            dS )zDiscover and load all plugins.

    Default behavior is idempotent. Pass ``force=True`` to rescan plugin
    manifests and reload state in the current process.
    r   Nr  r  r  s    r   discover_pluginsr    s'     ***77777r   r   r   r   rj  c                6     t                      j        | fi |S )z|Invoke a lifecycle hook on all loaded plugins.

    Returns a list of non-``None`` return values from plugin callbacks.
    )r  rq  )r   r   s     r   rq  rq    s&    
 ,+I@@@@@r   rP   r   r   Optional[Dict[str, Any]]task_id
session_idtool_call_idr[   c                (   t          d| t          |t                    r|ni |||          }|D ]b}t          |t                    s|                    d          dk    r2|                    d          }t          |t                    r|r|c S cdS )a  Check ``pre_tool_call`` hooks for a blocking directive.

    Plugins that need to enforce policy (rate limiting, security
    restrictions, approval workflows) can return::

        {"action": "block", "message": "Reason the tool was blocked"}

    from their ``pre_tool_call`` callback.  The first valid block
    directive wins.  Invalid or irrelevant hook return values are
    silently ignored so existing observer-only hooks are unaffected.
    r!   )r   r   r  r  r  actionblockmessageN)rq  r=   rF   rE   r2   )r   r   r  r  r  hook_resultsrw  r  s           r   get_pre_tool_call_block_messager    s    $ d++3TT!  L   &$'' 	::h7****Y''gs## 	 	NNN4r   c                N    t                      }|                    |            |S )zReturn the global manager after ensuring plugin discovery has run.

    Pass ``force=True`` to rescan in the current process.
    r  r  )r   ro   s     r   _ensure_plugins_discoveredr    s+    
 !""GE***Nr   c                 (    t                      j        S )z5Return the plugin-registered context engine, or None.)r  r   rd   r   r   get_plugin_context_enginer    s    %''77r   Optional[Callable]c                f    t                      j                            |           }|r|d         ndS )zFReturn the handler for a plugin-registered slash command, or ``None``.rx   N)r  r   rE   )r1   r   s     r   get_plugin_command_handlerr    s3    &((9==dCCE$.5$.r   g      >@rw  c                    t          j                   s S 	 t          j                     n$# t          $ r t          j                   cY S w xY wi i t          j                    d fd}t          j        |dd          }|	                                 
                    t                    st          dt          d	d
          dv rd                             d          S )a  Resolve a plugin command return value, awaiting async handlers when needed.

    Sync CLI/TUI dispatch sites call plugin handlers from plain functions.
    If a handler is async, await it directly when no loop is running; if
    we're already inside an active loop, run it in a helper thread with its
    own loop so the caller still gets a concrete result synchronously. The
    threaded path is bounded by a 30s timeout so a hung async handler cannot
    wedge the terminal indefinitely.
    r   r~   c                     	 t          j                  d<   n# t          $ r} | d<   Y d } ~ nd } ~ ww xY w                                 d S #                                  w xY w)Nr;  r+  )asynciorunBaseExceptionr6   )r+  donefailureoutcomerw  s    r   _runnerz.resolve_plugin_command_result.<locals>._runner  sv    	&{622GG 	! 	! 	! GENNNNNN	! HHJJJJJDHHJJJJs%    A 
4/A 4A A#zhermes-plugin-command-awaitT)targetr1   daemon)timeoutz5Plugin command async handler did not complete within z.0fsr+  r;  r   )inspectisawaitabler  get_running_loopRuntimeErrorr  	threadingEventThreadstartwait"_PLUGIN_COMMAND_AWAIT_TIMEOUT_SECSTimeoutErrorrE   )rw  r  threadr  r  r  s   `  @@@r   resolve_plugin_command_resultr    sK    v&& # """" # # #{6"""""# !G(*G?D         *  F
 LLNNN99?9@@ 
9189 9 9
 
 	
 en;;ws   0 AADict[str, dict]c                 (    t                      j        S )u   Return the full plugin commands dict (name → {handler, description, plugin}).

    Triggers idempotent plugin discovery so callers can use plugin commands
    before any explicit discover_plugins() call.
    )r  r   rd   r   r   get_plugin_commandsr    s     &''88r   List[tuple]c                 D   t                      } | j        sg S 	 ddlm} n# t          $ r g cY S w xY wi }i }| j        D ]O}|                    |          }|s|j        }|                    |g                               |j	                   P| j
                                        D ]J\  }}|j        D ]=}|                    |          }|r$|j        |v r|                    |j        |           >Kg }	t          |          D ]}
|                    |
          }d|
                    dd                                           }|r|j        j        r|j        j        }n(d                    t          ||
                             }|	                    |
||f           |	S )zReturn plugin toolsets as ``(key, label, description)`` tuples.

    Used by the ``hermes tools`` TUI so plugin-provided toolsets appear
    alongside the built-in ones and can be toggled on/off per platform.
    r   r   u   🔌 rR  r   r   )r  r   r   r   r?   	get_entryrv   r   r   r1   r   rC  ri   r   rE   r   titlerg   rR   r   )ro   r   toolset_toolstoolset_pluginr   r   ts_namer  rw  ts_keyr   r   descs                 r   get_plugin_toolsetsr  
  s    !""G% 	+++++++   			 +-M.0N/ < <	""9-- 	]  R((//
;;;; !)//11 A Av0 	A 	AI&&y11E A-77))%-@@@	A
 F'' - -##F++:sC006688:: 	<fo1 	<?.DD99VM&$9::;;Dvud+,,,,Ms     //)r   r   )r1   r2   r   r3   )r   r6   )r   rB   )r   r   r  r  r  )rP   rP   rP   )r   r2   r   r  r  r2   r  r2   r  r2   r   r[   )r   r3   r   r   )r1   r2   r   r  )rw  r   r   r   )r   r  )r   r  )>rb   
__future__r   r  r7  importlib.metadataimportlib.utilr  loggingr   rV  r  rX  dataclassesr   r   pathlibr   typingr   r   r	   r
   r   r   r   hermes_constantsr   utilsr   r<   r   r   r$  r_  	getLoggerr_   r   r/   rc   r2  rU  r5   rA   rH   rM   rO   rf   rn   r   r  r  r  rq  r  r  r  r  r  r  r  r  rd   r   r   <module>r     s    B # " " " " "                				 



      ( ( ( ( ( ( ( (       B B B B B B B B B B B B B B B B B B , , , , , , ! ! ! ! ! ! % % % % % %
> 
> 
> 
>KKKK   DDD 
	8	$	$$ $ $ $ $ $ $L , 
! ! ! !
       D !S R R  R R R R ! ! ! ! ! ! ! !H 	  	  	  	  	  	  	  	  e
 e
 e
 e
 e
 e
 e
 e
X[6 [6 [6 [6 [6 [6 [6 [6D ,0 / / / /   8 8 8 8 8A A A A $ $ $ $ $N    8 8 8
/ / / / &* "+  +  +  + \9 9 9 9* * * * * *s   )A. .A87A8