
    is                       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ZddlmZ ddlmZm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mZ 	 ddl Z n# e!$ r dZ Y nw xY wdd	l"m#Z# dd
l$m%Z%  ej&        e'          Z(dZ)dZ*dZ+ e,            Z-de.d<    ej/                    Z0 ej/                    Z1e G d d                      Z2dddKdZ3dLdZ4dMdZ5dNd Z6dOd'Z7h d(Z8dPd,Z9dQd.Z:dRd0Z;dSd2Z<dTd4Z=dUd5Z>dVd7Z?dWd9Z@edXd;            ZAdYd<ZBdZd=ZCd[d>ZDd\d?ZEd@ZFdAe.dB<   d]dCZGd^dEZHd_dFZId`dHZJdadIZKdbdJZLdS )cu#  
Shell-script hooks bridge.

Reads the ``hooks:`` block from ``cli-config.yaml``, prompts the user for
consent on first use of each ``(event, command)`` pair, and registers
callbacks on the existing plugin hook manager so every existing
``invoke_hook()`` site dispatches to the configured shell scripts — with
zero changes to call sites.

Design notes
------------
* Python plugins and shell hooks compose naturally: both flow through
  :func:`hermes_cli.plugins.invoke_hook` and its aggregators.  Python
  plugins are registered first (via ``discover_and_load()``) so their
  block decisions win ties over shell-hook blocks.
* Subprocess execution uses ``shlex.split(os.path.expanduser(command))``
  with ``shell=False`` — no shell injection footguns.  Users that need
  pipes/redirection wrap their logic in a script.
* First-use consent is gated by the allowlist under
  ``~/.hermes/shell-hooks-allowlist.json``.  Non-TTY callers must pass
  ``accept_hooks=True`` (resolved from ``--accept-hooks``,
  ``HERMES_ACCEPT_HOOKS``, or ``hooks_auto_accept: true`` in config)
  for registration to succeed without a prompt.
* Registration is idempotent — safe to invoke from both the CLI entry
  point (``hermes_cli/main.py``) and the gateway entry point
  (``gateway/run.py``).

Wire protocol
-------------
**stdin** (JSON, piped to the script)::

    {
        "hook_event_name": "pre_tool_call",
        "tool_name":       "terminal",
        "tool_input":      {"command": "rm -rf /"},
        "session_id":      "sess_abc123",
        "cwd":             "/home/user/project",
        "extra":           {...}   # event-specific kwargs
    }

**stdout** (JSON, optional — anything else is ignored)::

    # Block a pre_tool_call (either shape accepted; normalised internally):
    {"decision": "block", "reason":  "Forbidden command"}   # Claude-Code-style
    {"action":   "block", "message": "Forbidden command"}   # Hermes-canonical

    # Inject context for pre_llm_call:
    {"context": "Today is Friday"}

    # Silent no-op:
    <empty or any non-matching JSON object>
    )annotationsN)contextmanager)	dataclassfield)datetimetimezone)Path)AnyCallableDictIteratorListOptionalSetTuple)get_hermes_home)atomic_replace<   i,  zshell-hooks-allowlist.jsonz#Set[Tuple[str, Optional[str], str]]_registeredc                  x    e Zd ZU dZded<   ded<   dZded<   eZded	<    edd
          Z	ded<   ddZ
ddZdS )ShellHookSpeczAParsed and validated representation of a single ``hooks:`` entry.streventcommandNOptional[str]matcherinttimeoutF)defaultreprzOptional[re.Pattern]compiled_matcherreturnNonec                \   t          | j        t                    r$| j                                        }|r|nd | _        | j        rf	 t	          j        | j                  | _        d S # t          j        $ r3}t          	                    d| j        |           d | _        Y d }~d S d }~ww xY wd S )NuF   shell hook matcher %r is invalid (%s) — treating as literal equality)

isinstancer   r   striprecompiler!   errorloggerwarning)selfstrippedexcs      6/home/ubuntu/.hermes/hermes-agent/agent/shell_hooks.py__post_init__zShellHookSpec.__post_init__s   s     dlC(( 	:|))++H'/988TDL< 	--(*
4<(@(@%%%8 - - -'(,c   )-%%%%%%%-	- 	-s   A' 'B)6(B$$B)	tool_nameboolc                x    | j         sdS |dS | j        | j                            |          d uS || j         k    S )NTF)r   r!   	fullmatch)r,   r1   s     r/   matches_toolzShellHookSpec.matches_tool   sP    | 	45 ,(229==TII DL((    r"   r#   )r1   r   r"   r2   )__name__
__module____qualname____doc____annotations__r   DEFAULT_TIMEOUT_SECONDSr   r   r!   r0   r5    r6   r/   r   r   i   s         KKJJJLLL!G!!!!*G****-2U4e-L-L-LLLLL- - - -"	) 	) 	) 	) 	) 	)r6   r   Faccept_hookscfgOptional[Dict[str, Any]]r@   r2   r"   List[ShellHookSpec]c          	        t          | t                    sg S t          | |          }t          |                     d                    }|sg S g }ddlm}  |            }|D ]}|j        |j        |j	        f}t          5  |t          v r	 ddd           5t          |j        |j	                  }	ddd           n# 1 swxY w Y   |	sCt          |j        |j	        |          s't                              d|j        |j	                   t          5  |t          v r	 ddd           |j                            |j        g                               t'          |                     t                              |           |                    |           t                              d|j        |j	        |j        |j                   ddd           n# 1 swxY w Y   |S )u*  Register every configured shell hook on the plugin manager.

    ``cfg`` is the full parsed config dict (``hermes_cli.config.load_config``
    output).  The ``hooks:`` key is read out of it.  Missing, empty, or
    non-dict ``hooks`` is treated as zero configured hooks.

    ``accept_hooks=True`` skips the TTY consent prompt — the caller is
    promising that the user has opted in via a flag, env var, or config
    setting.  ``HERMES_ACCEPT_HOOKS=1`` and ``hooks_auto_accept: true`` are
    also honored inside this function so either CLI or gateway call sites
    pick them up.

    Returns the list of :class:`ShellHookSpec` entries that ended up wired
    up on the plugin manager.  Skipped entries (unknown events, malformed,
    not allowlisted, already registered) are logged but not returned.
    hooksr   )get_plugin_managerNr?   u   shell hook for %s (%s) not allowlisted — skipped. Use --accept-hooks / HERMES_ACCEPT_HOOKS=1 / hooks_auto_accept: true, or approve at the TTY prompt next run.z9shell hook registered: %s -> %s (matcher=%s, timeout=%ds))r%   dict_resolve_effective_accept_parse_hooks_blockgethermes_cli.pluginsrF   r   r   r   _registered_lockr   _is_allowlisted_prompt_and_recordr*   r+   _hooks
setdefaultappend_make_callbackaddinfor   )
rA   r@   effective_acceptspecs
registeredrF   managerspeckeyalready_allowlisteds
             r/   register_from_configr\      s   * c4   	0lCCswww//00E 	&(J 655555  ""G   z4<6 	L 	Lk!!	L 	L 	L 	L 	L 	L 	L #2$*dl"K"K	L 	L 	L 	L 	L 	L 	L 	L 	L 	L 	L 	L 	L 	L 	L
 # 	%
DL7G   
 ' J    		 		k!!		 		 		 		 		 		 		 N%%dj"55<<^D=Q=QRRROOC   d###KKK
DL$,  		 		 		 		 		 		 		 		 		 		 		 		 		 		 		 s1    B=B==C	C	G+B!GG	G	c                t    t          | t                    sg S t          |                     d                    S )zReturn the parsed ``ShellHookSpec`` entries from config without
    registering anything.  Used by ``hermes hooks list`` and ``doctor``.rE   )r%   rG   rI   rJ   )rA   s    r/   iter_configured_hooksr^      s5     c4   	cggg..///r6   r#   c                 x    t           5  t                                           ddd           dS # 1 swxY w Y   dS )z-Clear the idempotence set.  Test-only helper.N)rL   r   clearr>   r6   r/   reset_for_testsra      s~    	                   s   /33	hooks_cfgr
   c           
        ddl m} t          | t                    sg S g }|                                 D ]\  }}||vrt          j        t          |          |dd          }|r#t          	                    d||d                    n<t          	                    d|d
                    t          |                               |t          |t                    s/t          	                    d
|t          |          j                   t          |          D ]-\  }}t!          |||          }||                    |           .|S )u   Normalise the ``hooks:`` dict into a flat list of ``ShellHookSpec``.

    Malformed entries warn-and-skip — we never raise from config parsing
    because a broken hook must not crash the agent.
    r   )VALID_HOOKS   g333333?)ncutoffu;   unknown hook event %r in hooks: config — did you mean %r?z2unknown hook event %r in hooks: config (valid: %s)z, Nz3hooks.%s must be a list of hook definitions; got %s)rK   rd   r%   rG   itemsdifflibget_close_matchesr   r*   r+   joinsortedlisttyper8   	enumerate_parse_single_entryrQ   )	rb   rd   rV   
event_nameentries
suggestionirawrY   s	            r/   rI   rI      s    /.....i&& 	!#E(00 # #
G[(( 2J#  J  	Q
1   
 H		&*=*= > >   ?'4(( 	NNEDMM2   (( 	# 	#FAs&z1c::DT"""	#
 Lr6   r   r   indexr   ru   Optional[ShellHookSpec]c                   t          |t                    s1t                              d| |t	          |          j                   d S |                    d          }t          |t                    r|                                st                              d| |           d S |                    d          }|3t          |t                    st                              d| |           d }|$| dvr t                              d| |||            d }|                    dt                    }	 t          |          }nA# t          t          f$ r- t                              d	| ||t                     t          }Y nw xY w|d
k     r)t                              d| |t                     t          }|t          k    r*t                              d| ||t                     t          }t          | |                                ||          S )Nz;hooks.%s[%d] must be a mapping with a 'command' key; got %sr   z3hooks.%s[%d] is missing a non-empty 'command' fieldr   z5hooks.%s[%d].matcher must be a string regex; ignoringpre_tool_callpost_tool_callu   hooks.%s[%d].matcher=%r will be ignored at runtime — the matcher field is only honored for pre_tool_call / post_tool_call.  The hook will fire on every %s event.r   z?hooks.%s[%d].timeout must be an int (got %r); using default %dsre   z3hooks.%s[%d].timeout must be >=1; using default %dsz2hooks.%s[%d].timeout=%ds exceeds max %ds; clamping)r   r   r   r   )r%   rG   r*   r+   rn   r8   rJ   r   r&   r=   r   	TypeError
ValueErrorMAX_TIMEOUT_SECONDSr   )r   rv   ru   r   r   timeout_rawr   s          r/   rp   rp   !  s1    c4   I5$s)),	
 	
 	
 tggi  Ggs## 7==?? A5	
 	
 	
 tggi  G:gs#;#;C5	
 	
 	
 u,OOOE 5'5		
 	
 	
 '')%<==K*k""z" * * *M5+'>	
 	
 	
 ** {{A51	
 	
 	
 *$$$@5'#6	
 	
 	
 &	   s   /D? ?;E=<E=>   argsr1   
session_idparent_session_idrY   
stdin_jsonDict[str, Any]c                    ddddddd}	 t          j        t          j                            | j                            }n)# t          $ r}d| j        d| |d<   |cY d}~S d}~ww xY w|sd	|d<   |S t          j                    }	 t          j
        ||d
| j        d
d          }n# t          j        $ r1 d
|d<   t          t          j                    |z
  d          |d<   |cY S t          $ r
 d|d<   |cY S t          $ r
 d|d<   |cY S t           $ r}t#          |          |d<   |cY d}~S d}~ww xY w|j        |d<   |j        pd|d<   |j        pd|d<   t          t          j                    |z
  d          |d<   |S )u  Run ``spec.command`` as a subprocess with ``stdin_json`` on stdin.

    Returns a diagnostic dict with the same keys for every outcome
    (``returncode``, ``stdout``, ``stderr``, ``timed_out``,
    ``elapsed_seconds``, ``error``).  This is the single place the
    subprocess is actually invoked — both the live callback path
    (:func:`_make_callback`) and the CLI test helper (:func:`run_once`)
    go through it.
    N Fg        )
returncodestdoutstderr	timed_outelapsed_secondsr)   zcommand z cannot be parsed: r)   zempty commandT)inputcapture_outputr   textshellr      r   zcommand not foundzcommand not executabler   r   r   )shlexsplitospath
expanduserr   r}   time	monotonic
subprocessrunr   TimeoutExpiredroundFileNotFoundErrorPermissionError	Exceptionr   r   r   r   )rY   r   resultargvr.   t0procs          r/   _spawnr   k  s     F{27--dl;;<<   MT\MMMMw  )w			B~L
 
 
 $   "{$)$.*:*:R*?$C$C !   -w   2w   c((w  ?F<{(bF8{(bF8 %dn&6&6&;Q ? ?FMsM   6A 
A(A#A(#A(B( (=D2'D29D2	D2D-'D2-D2'Callable[..., Optional[Dict[str, Any]]]c                \     d	 fd}d j          d j         d|_        |j        |_        |S )
z>Build the closure that ``invoke_hook()`` will call per firing.kwargsr
   r"   rB   c            	        j         dv r*                    |                     d                    sd S t          t	          j         |                     }|d         r/t
                              dj         j        |d                    d S |d         r/t
                              d|d         j         j                   d S |d                                         }|r/t
          	                    d	j         j        |d d
                    |d         dk    r6t
                              d|d         j         j        |d d
                    t          j         |d                   S )Nry   r1   r)   z+shell hook failed (event=%s command=%s): %sr   z6shell hook timed out after %.2fs (event=%s command=%s)r   r   z+shell hook stderr (event=%s command=%s): %si  r   r   z5shell hook exited %d (event=%s command=%s); stderr=%sr   )r   r5   rJ   r   _serialize_payloadr*   r+   r   r&   debug_parse_response)r   rr   rY   s      r/   	_callbackz!_make_callback.<locals>._callback  sp   :<<<$$VZZ%<%<== t4+DJ??@@W: 	NN=
DL!G*   4[> 	NNH#$dj$,   48""$$ 	LL=
DL&#,   \?aNNG,T\6$3$<   tz1X;777r6   zshell_hook[:])r   r
   r"   rB   )r   r   r8   r:   )rY   r   s   ` r/   rR   rR     sU    "8 "8 "8 "8 "8 "8H DtzCCDLCCCI&/Ir6   r   c                   d |                                 D             }	 t          t          j                              }n# t          $ r d}Y nw xY w| |                    d          t          |                    d          t                    r|                    d          nd|                    d          p|                    d          pd||d}t          j	        |d	t          
          S )zrRender the stdin JSON payload.  Unserialisable values are
    stringified via ``default=str`` rather than dropped.c                ,    i | ]\  }}|t           v||S r>   )_TOP_LEVEL_PAYLOAD_KEYS).0kvs      r/   
<dictcomp>z&_serialize_payload.<locals>.<dictcomp>  s)    RRRtq!:Q1Q1Qa1Q1Q1Qr6   r   r1   r   Nr   r   )hook_event_namer1   
tool_inputr   cwdextraF)ensure_asciir   )
rh   r   r	   r   OSErrorrJ   r%   rG   jsondumps)r   r   extrasr   payloads        r/   r   r     s     SRv||~~RRRF$(**oo    !ZZ,,,6vzz&7I7I4,P,PZfjj(((VZjj..W&**=P2Q2QWUW G :gE3????s    A AAr   c                    |pd                                 }|sdS 	 t          j        |          }n:# t          j        $ r( t                              d| |dd                    Y dS w xY wt          |t                    sdS | dk    r|                    d          dk    rH|                    d          p|                    d	          pd}t          |t                    r|rd|d
S |                    d          dk    rH|                    d	          p|                    d          pd}t          |t                    r|rd|d
S dS |                    d          }t          |t                    r|                                 rd|iS dS )u  Translate stdout JSON into a Hermes wire-shape dict.

    For ``pre_tool_call`` the Claude-Code-style ``{"decision": "block",
    "reason": "..."}`` payload is translated into the canonical Hermes
    ``{"action": "block", "message": "..."}`` shape expected by
    :func:`hermes_cli.plugins.get_pre_tool_call_block_message`.  This is
    the single most important correctness invariant in this module —
    skipping the translation silently breaks every ``pre_tool_call``
    block directive.

    For ``pre_llm_call``, ``{"context": "..."}`` is passed through
    unchanged to match the existing plugin-hook contract.

    Anything else returns ``None``.
    r   Nz3shell hook stdout was not valid JSON (event=%s): %s   rz   actionblockmessagereason)r   r   decisioncontext)
r&   r   loadsJSONDecodeErrorr*   r+   r%   rG   rJ   r   )r   r   datar   r   s        r/   r   r     s     l!!##F tz&!!   A6$3$<	
 	
 	
 tt dD!! t88H((hhy))ETXXh-?-?E2G'3'' ?G ?")g>>>88J7**hhx((EDHHY,?,?E2G'3'' ?G ?")g>>>thhy!!G'3 $GMMOO $7##4s   1 3A('A(r	   c                 .    t                      t          z  S )z/Path to the per-user shell-hook allowlist file.)r   ALLOWLIST_FILENAMEr>   r6   r/   allowlist_pathr     s    111r6   c                 J   	 t          j        t                                                                } n&# t          t           j        t          f$ r dg icY S w xY wt          | t                    sdg iS | 	                    d          }t          |t                    sg | d<   | S )z<Return the parsed allowlist, or an empty skeleton if absent.	approvals)r   r   r   	read_textr   r   r   r%   rG   rJ   rm   )ru   r   s     r/   load_allowlistr     s    !j))335566t3W= ! ! !R    !c4   !R  $$Ii&& KJs   25  AAr   c                f   t                      }	 |j                            dd           t          j        |j         ddt          |j                            \  }}	 t          j        |d          5 }|	                    t          j        | dd                     d	d	d	           n# 1 swxY w Y   t          ||           d	S # t          $ r( 	 t          j        |           n# t          $ r Y nw xY w w xY w# t          $ r'}t                               d
||           Y d	}~d	S d	}~ww xY w)a;  Atomically persist the allowlist via per-process ``mkstemp`` +
    ``os.replace``.  Cross-process read-modify-write races are handled
    by :func:`_locked_update_approvals` (``fcntl.flock``).  On OSError
    the failure is logged; the in-process hook still registers but
    the approval won't survive across runs.Tparentsexist_ok.z.tmp)prefixsuffixdirw   )indent	sort_keysNzFailed to persist shell hook allowlist to %s: %s. The approval is in-memory for this run, but the next startup will re-prompt (or skip registration on non-TTY runs without --accept-hooks / HERMES_ACCEPT_HOOKS).)r   parentmkdirtempfilemkstempnamer   r   fdopenwriter   r   r   r   unlinkr   r*   r+   )r   pfdtmp_pathfhr.   s         r/   save_allowlistr   -  s    	A
	td333'f<<<CMM
 
 
H		2s## ErDdCCCDDDE E E E E E E E E E E E E E E8Q''''' 	 	 		(####   	  
 
 
B s	
 	
 	
 	
 	
 	
 	
 	
 	

sr   AC? !C
 6+B-!C
 -B11C
 4B15C
 

C<C*)C<*
C74C<6C77C<<C? ?
D0	D++D0r   c                     t                      }t           fd|                    dg           D                       S )Nc              3     K   | ]K}t          |t                    o1|                    d           k    o|                    d          k    V  LdS )r   r   Nr%   rG   rJ   r   er   r   s     r/   	<genexpr>z"_is_allowlisted.<locals>.<genexpr>O  ss          	1d 	(EE'NNe#	(EE)'     r6   r   )r   anyrJ   )r   r   r   s   `` r/   rM   rM   M  s\    D      +r**	     r6   Iterator[Dict[str, Any]]c               #    K   t                      } | j                            dd           |                     | j        dz             }t
          Bt          5  t                      }|V  t          |           ddd           n# 1 swxY w Y   dS t          |d          5 }t          j
        |                                t
          j                   	 t                      }|V  t          |           t          j
        |                                t
          j                   n6# t          j
        |                                t
          j                   w xY w	 ddd           dS # 1 swxY w Y   dS )u  Serialise read-modify-write on the allowlist across processes.

    Holds an exclusive ``flock`` on a sibling lock file for the duration
    of the update so concurrent ``_record_approval``/``revoke`` callers
    cannot clobber each other's changes (the race Codex reproduced with
    20–50 simultaneous writers).  Falls back to an in-process lock on
    platforms without ``fcntl``.
    Tr   z.lockNza+)r   r   r   with_suffixr   fcntl_allowlist_write_lockr   r   openflockfilenoLOCK_EXLOCK_UN)r   	lock_pathr   lock_fhs       r/   _locked_update_approvalsr   W  s      	AHNN4$N///ah011I}" 	! 	!!##DJJJ4   	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 		i		 9'GNN$$em444	9!##DJJJ4   K((%-8888EK((%-888889 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9s<   "BB
B
#2E-!D)72E-)3EE--E14E1c                  |r.t          | |           t                              d| |           dS t          j                                        sdS t          d|  d| d           	 t          d                                          	                                }n&# t          t          f$ r t                       Y dS w xY w|dv rt          | |           dS dS )	zDecide whether to approve an unseen ``(event, command)`` pair.
    Returns ``True`` iff the approval was granted and recorded.
    zDshell hook auto-approved via --accept-hooks / env / config: %s -> %sTFuf   
⚠ Hermes is about to register a shell hook that will run a
  command on your behalf.

    Event:   z
    Command: zU

  Commands run with your full user credentials.  Only approve
  commands you trust.zAllow this hook to run? [y/N]: )yyes)_record_approvalr*   rT   sysstdinisattyprintr   r&   lowerEOFErrorKeyboardInterrupt)r   r   r@   answers       r/   rN   rN   v  s     (((w	
 	
 	
 t9 u		!	! 	!  	! 	! 	!  899??AAGGII'(   uu (((t5s   (3B B?>B?c                      t                      t                    d}t                      5 } fd|                    dg           D             |gz   |d<   d d d            d S # 1 swxY w Y   d S )N)r   r   approved_atscript_mtime_at_approvalc                    g | ]K}t          |t                    r2|                    d           k    r|                    d          k    I|LS )r   r   r   r   s     r/   
<listcomp>z$_record_approval.<locals>.<listcomp>  se     
 
 
1d##
 EE'NNe++EE)$$//	  0//r6   r   )_utc_now_isoscript_mtime_isor   rJ   )r   r   entryr   s   ``  r/   r  r    s    #~~$4W$=$=	 E 
"	#	# t
 
 
 
 
xxR00
 
 
 G[                 s   +A))A-0A-c                     t          j        t          j                                                                      dd          S )Ntz+00:00Z)r   nowr   utc	isoformatreplacer>   r6   r/   r  r    s3    <8<(((2244<<XsKKKr6   c                    t                      5 }t          |                    dg                     } fd|                    dg           D             |d<   t          |d                   }ddd           n# 1 swxY w Y   ||z
  S )u   Remove every allowlist entry matching ``command``.

    Returns the number of entries removed.  Does not unregister any
    callbacks that are already live on the plugin manager in the current
    process — restart the CLI / gateway to drop them.
    r   c                p    g | ]2}t          |t                    r|                    d           k    0|3S )r   r   )r   r   r   s     r/   r  zrevoke.<locals>.<listcomp>  sL     
 
 
q$''
,-EE),<,<,G,G ,G,G,Gr6   N)r   lenrJ   )r   r   beforeafters   `   r/   revoker!    s     
"	#	# 'tTXXk2..//
 
 
 
xxR00
 
 
[ D%&&' ' ' ' ' ' ' ' ' ' ' ' ' ' ' E>s   AA::A>A>)z.shz.bashz.zshz.fishz.pyz.pywz.rbz.plz.luaz.jsz.mjsz.cjsz.tszTuple[str, ...]_SCRIPT_EXTENSIONSc                   	 t          j        |           }n# t          $ r | cY S w xY w|s| S |D ]2}|                                                    t
                    r|c S 3|D ]}d|v s|                    d          r|c S  |d         S )a4  Return the script path from ``command`` for doctor / drift checks.

    Prefers a token ending in a known script extension, then a token
    containing ``/`` or leading ``~``, then the first token.  Handles
    ``python3 /path/hook.py``, ``/usr/bin/env bash hook.sh``, and the
    common bare-path form.
    /~r   )r   r   r}   r  endswithr"  
startswith)r   partsparts      r/   _command_script_pathr*    s    G$$      ::<<  !344 	KKK	  $;;$//#..;KKK 8Os    &&accept_hooks_argc                v   |rdS t           j                            dd                                                                          }|dv rdS |                     dd          }t          |t                    r|S t          |t                    r(|                                                                dv S dS )a  Combine all three opt-in channels into a single boolean.

    Precedence (any truthy source flips us on):
      1. ``--accept-hooks`` flag (CLI) / explicit argument
      2. ``HERMES_ACCEPT_HOOKS`` env var
      3. ``hooks_auto_accept: true`` in ``cli-config.yaml``
    THERMES_ACCEPT_HOOKSr   )1truer  onhooks_auto_acceptF)r   environrJ   r&   r  r%   r2   r   )rA   r+  envcfg_vals       r/   rH   rH     s      t
*...
3
3
9
9
;
;
A
A
C
CC
(((tgg)511G'4   '3 E}}$$&&*DDD5r6   c                    t                                          dg           D ]M}t          |t                    r6|                    d          | k    r|                    d          |k    r|c S NdS )z2Return the allowlist record for this pair, if any.r   r   r   N)r   rJ   r%   rG   )r   r   r   s      r/   allowlist_entry_forr6    st    !!+r22  q$	g%''i  G++HHH4r6   r   c                P   t          |           }|sdS 	 t          j                            |          }t	          j        t          j                            |          t          j                  	                                
                    dd          S # t          $ r Y dS w xY w)zUISO-8601 mtime of the resolved script path, or ``None`` if the
    script is missing.Nr  r  r  )r*  r   r   r   r   fromtimestampgetmtimer   r  r  r  r   )r   r   expandeds      r/   r  r    s      ((D t7%%d++%GX&&8<
 
 

)++ggh,,	-    tts   BB 
B%$B%c                   t          |           }|sdS t          j                            |          }t          j                            |          sdS 	 t          j        |           }n# t          $ r Y dS w xY wt          |          o|d         |k    }|rt          j	        nt          j
        }t          j        ||          S )u  Return ``True`` iff ``command`` is runnable as configured.

    For a bare invocation (``/path/hook.sh``) the script itself must be
    executable.  For interpreter-prefixed commands (``python3
    /path/hook.py``, ``/usr/bin/env bash hook.sh``) the script just has
    to be readable — the interpreter doesn't care about the ``X_OK``
    bit.  Mirrors what ``_spawn`` would actually do at runtime.Fr   )r*  r   r   r   isfiler   r   r}   r2   X_OKR_OKaccess)r   r   r:  r   is_bare_invocationrequireds         r/   script_is_executablerB    s      ((D uw!!$''H7>>(## u{7##   uud7Q4,9rww"'H9Xx(((s   A* *
A87A8c                    t          | j        |          }t          | |          }t          | j        |d                   |d<   |S )uU  Fire a single shell-hook invocation with a synthetic payload.
    Used by ``hermes hooks test`` and ``hermes hooks doctor``.

    ``kwargs`` is the same dict that :func:`hermes_cli.plugins.invoke_hook`
    would pass at runtime.  It is routed through :func:`_serialize_payload`
    so the synthetic stdin exactly matches what a real hook firing would
    produce — otherwise scripts tested via ``hermes hooks test`` could
    diverge silently from production behaviour.

    Returns the :func:`_spawn` diagnostic dict plus a ``parsed`` field
    holding the canonical Hermes-wire-shape response.r   parsed)r   r   r   r   )rY   r   r   r   s       r/   run_oncerE  3  sD     $DJ77JD*%%F&tz6(3CDDF8Mr6   )rA   rB   r@   r2   r"   rC   )rA   rB   r"   rC   r7   )rb   r
   r"   rC   )r   r   rv   r   ru   r
   r"   rw   )rY   r   r   r   r"   r   )rY   r   r"   r   )r   r   r   r   r"   r   )r   r   r   r   r"   rB   )r"   r	   )r"   r   )r   r   r"   r#   )r   r   r   r   r"   r2   )r"   r   )r   r   r   r   r@   r2   r"   r2   )r   r   r   r   r"   r#   )r"   r   )r   r   r"   r   )r   r   r"   r   )rA   r   r+  r2   r"   r2   )r   r   r   r   r"   rB   )r   r   r"   r   )r   r   r"   r2   )rY   r   r   r   r"   r   )Mr;   
__future__r   ri   r   loggingr   r'   r   r   r  r   	threadingr   
contextlibr   dataclassesr   r   r   r   pathlibr	   typingr
   r   r   r   r   r   r   r   r   ImportErrorhermes_constantsr   utilsr   	getLoggerr8   r*   r=   r~   r   setr   r<   LockrL   r   r   r\   r^   ra   rI   rp   r   r   rR   r   r   r   r   r   rM   r   rN   r  r  r!  r"  r*  rH   r6  r  rB  rE  r>   r6   r/   <module>rS     sZ  3 3 3j # " " " " "    				 				      



       % % % % % % ( ( ( ( ( ( ( ( ' ' ' ' ' ' ' '       L L L L L L L L L L L L L L L L L L L LLLLL   EEE - , , , , ,            		8	$	$  1  47355 8 8 8 8!9>##  '	((  #) #) #) #) #) #) #) #)Z H H H H H HV0 0 0 0   - - - -`@ @ @ @N SRR 7 7 7 7t) ) ) )X@ @ @ @&/ / / /l2 2 2 2
   
 
 
 
@    9 9 9 9<# # # #L   $L L L L   "'        6   6	 	 	 	   ) ) ) ).     s   'A, ,A65A6