
    sjT9                    *   U d Z ddlmZ ddlZddlZddlmZ ddlmZmZ ddl	m
Z
mZmZmZmZmZmZ  ej        e          Z ed           G d	 d
                      Zd  eddd           eddd           eddd           eddd           eddd           eddd           edd d!          fD             Zd"ed#<    ee          Zd$Zd%ZdJdKd-ZdLd0ZdMdNd5Z	 dMdOd7ZdPd<Z	 dMdQd>Z dRdBZ!dSdEZ"dMdTdGZ#	 dMdUdHZ$	 dMdVdIZ%dS )WuK	  Plugin capability declarations + consent state (#64228).

Unifies the scattered per-plugin trust gates (``plugins.entries.<id>.allow_*``)
into one declared, diffable **capability model** with install/update-time
consent.

**This is NOT a sandbox.** In-process Python plugins remain trusted code — a
malicious plugin can import anything, monkey-patch core, and ignore all of
this. Capabilities govern the *host API surfaces* Hermes hands out (which
registrations succeed, which ``ctx`` methods are live) and give the user an
honest consent + audit trail. Actual isolation is a separate research track.

Canonical registry
------------------
Every capability id maps 1:1 to a trust gate that **already exists** on the
enforcing surface. We deliberately do not mint capability ids without an
enforcing gate:

===========================  ==================================================
Capability id                Legacy config gate (``plugins.entries.<id>.…``)
===========================  ==================================================
``tools.override``           ``allow_tool_override``
``llm.provider_override``    ``llm.allow_provider_override``
``llm.model_override``       ``llm.allow_model_override``
``llm.agent_id_override``    ``llm.allow_agent_id_override``
``llm.profile_override``     ``llm.allow_profile_override``
``llm.task_override``        ``llm.allow_task_override``
``gateway.platform_actions`` ``allow_platform_actions``
===========================  ==================================================

The legacy ``allow_*`` keys keep working verbatim (deprecated but honored):
a gate is open when the legacy key is true **or** the capability is granted.

Consent state
-------------
Stored under the plugin's config entry::

    plugins:
      entries:
        <plugin_id>:
          granted_capabilities: [tools.override]
          capabilities_consent:
            hash: "<sha256 of the declared capability set at consent time>"
            granted_at: "2026-08-12T00:00:00+00:00"

The hash records *what the user saw* when they consented. When an update
declares capabilities whose set hash differs, the additions stay ungranted
until the user re-consents (``hermes plugins update`` surfaces the diff).

Ground rule: everything defaults OFF. Any failure to read consent state
(missing config, corrupt YAML, wrong types) means **not granted**.
    )annotationsN)	dataclass)datetimetimezone)AnyDictIterableListMappingOptionalTupleT)frozenc                  2    e Zd ZU dZded<   ded<   ded<   dS )CapabilitySpecz9One declarable capability and the legacy gate it maps to.stridzTuple[str, ...]legacy_pathdescriptionN)__name__
__module____qualname____doc____annotations__     B/home/agent/.hermes/hermes-agent/hermes_cli/plugin_capabilities.pyr   r   A   s;         CCGGG !   r   r   c                    i | ]
}|j         |S r   )r   ).0specs     r   
<dictcomp>r    N   s,     62 62 62 	GT62 62 62r   ztools.override)allow_tool_overrideuv   Replace built-in tools (e.g. shell_exec, write_file) — an override can intercept everything routed through that tool)r   r   r   zllm.provider_override)llmallow_provider_overridez^Run host-owned LLM calls against a provider other than your active one (uses your credentials)zllm.model_override)r"   allow_model_overridezLChoose which model host-owned LLM calls use (spend follows the chosen model)zllm.agent_id_override)r"   allow_agent_id_overridez/Attribute its LLM calls to a different agent idzllm.profile_override)r"   allow_profile_overridez,Run LLM calls under a different auth profilezllm.task_override)r"   allow_task_overridezDRoute its LLM calls through the host's built-in auxiliary task laneszgateway.platform_actions)allow_platform_actionszkAct on connected chat platforms as the gateway bot (add reactions, rename threads) via ctx.platform_actionszDict[str, CapabilitySpec]CAPABILITY_REGISTRYgranted_capabilitiescapabilities_consent?rawr   plugin_namer   return	List[str]c                
   | sg S t          | t          t          f          s0t                              d|t          |           j                   g S g }| D ]}t          |t                    st                              d||           4|                                }|t          v r||vr|
                    |           kt                              d||d                    t          t                                         |S )uj  Normalize a manifest ``capabilities:`` value into known capability ids.

    Unknown ids are dropped with a warning (forward compat: a plugin built
    for a newer Hermes may declare ids this build doesn't know; they can
    never be granted here, so hiding them from the consent screen is the
    fail-closed choice — the plugin must degrade gracefully).
    uF   Plugin %s: manifest 'capabilities' must be a list, got %s — ignoringz2Plugin %s: ignoring non-string capability entry %ru9   Plugin %s: unknown capability %r (known: %s) — ignoringz, )
isinstancelisttupleloggerwarningtyper   r   stripVALID_CAPABILITY_IDSappendjoinsorted)r-   r.   outitemcaps        r   parse_declared_capabilitiesr@      s     	cD%=)) Tc+	
 	
 	
 	C  $$$ 	NNDT   jjll&&&#~~

3NNKS$))F3G,H,H"I"I    Jr   capabilitiesIterable[str]c                    d                     t          t          |                               }t          j        |                    d                                                    S )z?Deterministic sha256 over a capability set (order-insensitive).
zutf-8)r;   r<   sethashlibsha256encode	hexdigest)rA   canons     r   capability_set_hashrK      sK    IIfS..//00E>%,,w//00::<<<r   	plugin_idconfigOptional[Mapping[str, Any]]dictc                   	 |}|ddl m}  |            pi }|                    d          pi                     d          pi }|                    |           pi }t          |t                    r|ni S # t
          $ r i cY S w xY w)uB   Return ``plugins.entries.<plugin_id>`` or ``{}`` — never raises.Nr   )load_configpluginsentries)hermes_cli.configrQ   getr2   rO   	Exception)rL   rM   cfgrQ   rS   entrys         r   _plugin_entryrY      s    
;555555+--%2C779%%+00;;ArI&&,""5$//7uuR7   			s   A1A4 4BB	frozensetc                    t          | |          }|                    t                    }t          |t                    st                      S t          d |D                       S )zReturn the set of capabilities the user has granted this plugin.

    Fail-closed: missing/corrupt state yields the empty set.
    c              3     K   | ]H}t          |t                    r1|                                t          v 2|                                V  Id S N)r2   r   r8   r9   r   cs     r   	<genexpr>z'granted_capabilities.<locals>.<genexpr>   s\        a"#''))/C"C"C 	
		"C"C"C"C r   )rY   rU   GRANTED_KEYr2   r3   rZ   )rL   rM   rX   r-   s       r   r*   r*      sn     )V,,E
))K
 
 Cc4   {{       r   rX   Mapping[str, Any]r   boolc                    | }|j         D ]/}t          |t                    s dS |                    |          }0t	          |          o|duS )z>True when the deprecated ``allow_*`` key for *spec* is truthy.FN)r   r2   r   rU   rc   )rX   r   nodeparts       r   _legacy_gate_setrg      sY    D   $(( 	55xx~~::*$d**r   
capabilityc                   t                               |          }|t                              d||            dS t	          | |          }|t          | dd| |iii          v rt          | |dd           dS t          ||          r3t          | |dd	|  d
d
                    |j	                   d           dS t          | |dd           dS )a  Canonical check: is *capability* live for *plugin_id*?

    True when EITHER:

    * the capability appears in ``granted_capabilities`` (consent flow), OR
    * the legacy ``allow_*`` config key is set (deprecated, still honored so
      existing configs keep working).

    Unknown capability ids and any failure to read state return ``False``
    (ground rule 4: fail closed).
    Nu9   capability check for unknown id %r (plugin %s) — deniedFrR   rS   )rM   Tr*   zlegacy key plugins.entries..z (deprecated)znot granted)
r)   rU   r5   debugrY   r*   _log_capability_decisionrg   r;   r   )rL   rh   rM   r   rX   s        r   plugin_capability_grantedrm      s     "":..D|G		
 	
 	
 u)V,,E))YU^`eTfHg<hiiiii J>TUUUtt$$  z4_)__chht?O6P6P___	
 	
 	
 tY
E=III5r   allowedevidenceNonec                J    t                               d| ||rdnd|           dS )zDAudit line for capability gate decisions (the ``checked_by`` trail).zecapability_check plugin=%s capability=%s decision=%s checked_by=plugin_capability_granted evidence=%sallowdenyN)r5   info)rL   rh   rn   ro   s       r   rl   rl     s9     KKo:'=wwvx    r   granteddeclaredc                T   ddl m}m} d t                              |          D             }d t                              |          D             } |            }|                    di           }t          |t                    si }||d<   |                    di           }	t          |	t                    si }	|	|d<   |	                    | i           }
t          |
t                    si }
|
|	| <   |
                    t                    }t          |t                    rt          |          ng }|D ]}||vr|
                    |           t          d t                              |          D                       |
t          <   t          |          t          j        t          j                                      d	          d
|
t$          <   |
t                   D ]g}t&          |         }|
}|j        dd         D ]6}|                    |i           }t          |t                    si }|||<   |}7d||j        d         <   h ||           t*                              d| d                    |
t                             pd|
t$                   d         dd                    dS )uv  Persist a consent decision for *plugin_id*.

    Writes ``granted_capabilities`` (union with any previously granted set),
    the consent record (hash of the *declared* set the user saw + UTC
    timestamp), and — so every existing enforcement site keeps working
    without changes — the corresponding legacy ``allow_*`` keys for each
    newly granted capability.
    r   )rQ   save_configc                $    g | ]}|t           v |S r   r9   r^   s     r   
<listcomp>z"record_consent.<locals>.<listcomp>+  s#    SSS!>R9R9RA9R9R9Rr   c                $    g | ]}|t           v |S r   rz   r^   s     r   r{   z"record_consent.<locals>.<listcomp>,  #    UUU11@T;T;TQ;T;T;Tr   rR   rS   c              3  V   K   | ]$}t          |t                    r|t          v  |V  %d S r]   )r2   r   r9   r^   s     r   r`   z!record_consent.<locals>.<genexpr>A  sL          a "#';";"; 	
";";";";   r   seconds)timespec)hash
granted_atNTz8capability_consent plugin=%s granted=%s declared_hash=%s,z(none)r      )rT   rQ   rx   rO   fromkeys
setdefaultr2   rU   ra   r3   r:   r<   rK   r   nowr   utc	isoformatCONSENT_KEYr)   r   r5   rt   r;   )rL   ru   rv   rQ   rx   granted_listdeclared_listrM   plugins_cfgrS   rX   previousmergedr?   r   re   rf   childs                     r   record_consentr     s    ;:::::::SSt}}W55SSSLUUh 7 7UUUM[]]F##Ir22Kk4(( ('y$$Y33Ggt$$ )!(Iy"--EeT"" #"	yy%%H)(D99AT(^^^rF  fMM#    ==((       E+
 $M22l8<00::I:NN E+ [! 	* 	*"3'$SbS) 	 	DOOD"--EeT** #"T
DD%)Tb!""K
KKB388E+.//;8k6"3B3'    r   Optional[str]c                    t          | |          }|                    t                    }t          |t                    sdS |                    d          }t          |t
                    r|r|ndS )z<Return the stored consent hash, or None when absent/corrupt.Nr   )rY   rU   r   r2   rO   r   )rL   rM   rX   consenths        r   consent_hashr   _  sh    )V,,Eii$$Ggt$$ tFA1c""2q211d2r   c                    d t                               |          D             }t          | |          fd|D             S )a  Capabilities declared by the plugin but not yet granted.

    Used both at first consent (everything is pending) and on update
    re-consent: when a new version declares capabilities the granted set
    lacks, those additions are returned and must be re-consented before
    they go live. The stored consent hash tells whether the *declared* set
    changed since the user last saw it.
    c                $    g | ]}|t           v |S r   rz   r^   s     r   r{   z(pending_capabilities.<locals>.<listcomp>v  r}   r   c                    g | ]}|v|	S r   r   )r   r_   ru   s     r   r{   z(pending_capabilities.<locals>.<listcomp>x  s#    999!(8(8A(8(8(8r   )rO   r   r*   )rL   rv   rM   r   ru   s       @r   pending_capabilitiesr   i  sN     VUh 7 7UUUM"9f55G9999}9999r   c                d    t          | |          }|dS |t          d |D                       k    S )zTrue when the declared set differs from what the user consented to.

    No stored consent at all counts as changed (never consented).
    NTc              3  ,   K   | ]}|t           v |V  d S r]   rz   r^   s     r   r`   z'declared_set_changed.<locals>.<genexpr>  s6       ) )q$8888888) )r   )r   rK   )rL   rv   rM   storeds       r   declared_set_changedr   {  sS     )V,,F~t( ) )) ) )    r   )r,   )r-   r   r.   r   r/   r0   )rA   rB   r/   r   r]   )rL   r   rM   rN   r/   rO   )rL   r   rM   rN   r/   rZ   )rX   rb   r   r   r/   rc   )rL   r   rh   r   rM   rN   r/   rc   )
rL   r   rh   r   rn   rc   ro   r   r/   rp   )rL   r   ru   rB   rv   rB   r/   rp   )rL   r   rM   rN   r/   r   )rL   r   rv   rB   rM   rN   r/   r0   )rL   r   rv   rB   rM   rN   r/   rc   )&r   
__future__r   rF   loggingdataclassesr   r   r   typingr   r   r	   r
   r   r   r   	getLoggerr   r5   r   r)   r   rZ   r9   ra   r   r@   rK   rY   r*   rg   rm   rl   r   r   r   r   r   r   r   <module>r      sN  3 3 3j # " " " " "   ! ! ! ! ! ! ' ' ' ' ' ' ' ' F F F F F F F F F F F F F F F F F F		8	$	$ $       62 62 	0M		
 	
 	
 	&:5		
 	
 	
 	#7$		
 	
 	
 	&:I	
 	
 	

 	%9F	
 	
 	

 	"6		
 	
 	
 	)3K		
 	
 	
W362 62 62  6 6 6 6p !y!455  %$! ! ! ! !H= = = =      ;?    "+ + + + +/" " " " "J   @ @ @ @F3 3 3 3 3 +/: : : : :* +/      r   