
    `gj                       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 m!Z!m"Z" 	 ddl#Z#dd
l%m&Z& ddl'm(Z(  ejR                  e*      Z+dZ,dZ-dZ.dZ/ e0       Z1de2d<    ejf                         Z4 ejf                         Z5e G d d             Z6dd	 	 	 	 	 d3dZ7d4dZ8d5dZ9d6dZ:	 	 	 	 	 	 	 	 d7dZ;h dZ<d8dZ=d9dZ>d:dZ?d;dZ@d<d ZAd=d!ZBd>d"ZCd?d#ZDd@d$ZEedAd%       ZF	 	 	 	 	 	 	 	 dBd&ZGdCd'ZHdDd(ZIdEd)ZJd*ZKd+e2d,<   dFd-ZL	 	 	 	 	 	 dGd.ZMdHd/ZNdId0ZOdJd1ZP	 	 	 	 	 	 dKd2ZQy# e$$ r dZ#Y w xY w)Lu  
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>

Per-event ``extra`` keys
~~~~~~~~~~~~~~~~~~~~~~~~

The ``extra`` object contains every kwarg that is **not** one of the
top-level payload keys (``tool_name``, ``args``, ``session_id``,
``parent_session_id``).  The tables below list the ``extra`` keys
emitted by each built-in hook site.

``post_tool_call`` (emitted from ``model_tools.py``)::

    result          – tool return value (serialised string)
    status          – "ok" | "error" | "blocked"
    error_type      – error category (e.g. "ValueError"), or None
    error_message   – human-readable error text, or None
    duration_ms     – wall-clock time in milliseconds
    task_id         – current task id (empty string if none)
    tool_call_id    – provider tool-call id
    turn_id         – current turn id
    api_request_id  – current API request id
    middleware_trace – list of dicts from tool middleware chain

``pre_tool_call`` (emitted from ``model_tools.py``)::

    task_id         – current task id (empty string if none)
    tool_call_id    – provider tool-call id
    turn_id         – current turn id
    api_request_id  – current API request id
    middleware_trace – list of dicts from tool middleware chain

``on_session_start`` (emitted from ``agent/conversation_loop.py``)::

    model           – model name (e.g. "claude-sonnet-4-20250514")
    platform        – platform identifier (e.g. "cli", "whatsapp")

``on_session_end`` (emitted from ``agent/turn_finalizer.py``)::

    task_id         – current task id
    turn_id         – current turn id
    completed       – bool, True when the turn produced a final response
    interrupted     – bool, True when the user interrupted
    model           – model name
    platform        – platform identifier

``subagent_stop`` (emitted from ``tools/delegate_tool.py``)::

    parent_turn_id  – parent agent's current turn id
    child_session_id – child (subagent) session id
    child_role      – role string of the child agent
    child_summary   – summary of the child's work
    child_status    – exit status string (e.g. "success", "error")
    duration_ms     – wall-clock time of the child run in milliseconds
    )annotationsN)contextmanager)	dataclassfield)datetimetimezone)Path)AnyCallableDictIteratorListOptionalSetTuple)
IS_WINDOWSwindows_hide_flags)get_hermes_home)atomic_replace<   i,  zshell-hooks-allowlist.jsonzBlocked by shell hook.z#Set[Tuple[str, Optional[str], str]]_registeredc                  p    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y)ShellHookSpeczAParsed and validated representation of a single ``hooks:`` entry.streventcommandNOptional[str]matcherinttimeoutF)defaultreprzOptional[re.Pattern]compiled_matcherc                v   t        | j                  t              r%| j                  j                         }|r|nd | _        | j                  r&	 t	        j
                  | j                        | _        y y # t        j                  $ r2}t        j                  d| j                  |       d | _        Y d }~y d }~ww xY w)NuF   shell hook matcher %r is invalid (%s) — treating as literal equality)

isinstancer   r   striprecompiler#   errorloggerwarning)selfstrippedexcs      D/root/.hermes/venv/lib/python3.12/site-packages/agent/shell_hooks.py__post_init__zShellHookSpec.__post_init__   s     dllC(||))+H'/8TDL<<-(*

4<<(@%  88 -'(,c )-%%-s   $A3 3B8(B33B8c                    | j                   sy|y| j                  | j                  j                  |      d uS || j                   k(  S )NTF)r   r#   	fullmatch)r,   	tool_names     r/   matches_toolzShellHookSpec.matches_tool   sN    ||  ,((229=TII DLL((    returnNone)r3   r   r7   bool)__name__
__module____qualname____doc____annotations__r   DEFAULT_TIMEOUT_SECONDSr    r   r#   r0   r4    r5   r/   r   r      s@    KJL!G]!*GS*-24e-L*L-"	)r5   r   Faccept_hooksc          	         t        | t              sg S ddlm}  |d      rt        j                  d       g S t        | |      }t        | j                  d            }|sg S g }ddl	m
}  |       }|D ]}  }|j                  |j                  |j                  f}	t        5  |	t        v r
	 ddd       @t!        |j                  |j                        }
ddd       
sNt#        |j                  |j                  |      s,t        j%                  d	|j                  |j                         t        5  |	t        v r
	 ddd       |j&                  j)                  |j                  g       j+                  t-        |             t        j/                  |	       |j+                  |       t        j                  d
|j                  |j                  |j                  |j0                         ddd        |S # 1 sw Y   %xY w# 1 sw Y   xY w)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.
    r   )env_var_enabledHERMES_SAFE_MODEu6   HERMES_SAFE_MODE=1 — shell-hook registration skippedhooks)get_plugin_managerNrA   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utilsrD   r*   info_resolve_effective_accept_parse_hooks_blockgethermes_cli.pluginsrG   r   r   r   _registered_lockr   _is_allowlisted_prompt_and_recordr+   _hooks
setdefaultappend_make_callbackaddr    )cfgrB   rD   effective_acceptspecs
registeredrG   managerspeckeyalready_allowlisteds              r/   register_from_configr_      s   * c4 	
 &)*LM	0lCswww/0E	&(J 6 "G  zz4<<6 	Lk!	L 	L #2$**dll"K	L
 #%

DLL7G ' JJ  		k!		 		 NN%%djj"5<<^D=QROOC d#KKK

DLL$,,		 		)> ;	L 	L$		 		s%    
G63 G61
HB%H6H 	H	c                Z    t        | t              sg S t        | j                  d            S )zReturn the parsed ``ShellHookSpec`` entries from config without
    registering anything.  Used by ``hermes hooks list`` and ``doctor``.rF   )r%   rH   rL   rM   )rW   s    r/   iter_configured_hooksra     s'     c4 	cggg.//r5   c                 b    t         5  t        j                          ddd       y# 1 sw Y   yxY w)z-Clear the idempotence set.  Test-only helper.N)rO   r   clearr@   r5   r/   reset_for_testsrd   '  s&    	   s   %.c           
     >   ddl m} t        | t              sg S g }| j	                         D ]  \  }}|dv r||vrot        j                  t        |      |dd      }|rt        j                  d||d          n/t        j                  d|d	j                  t        |                   ~|t        |t              s+t        j                  d
|t        |      j                         t        |      D ]&  \  }}t!        |||      }||j#                  |       (  |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)output_spill   g333333?)ncutoffu;   unknown hook event %r in hooks: config — did you mean %r?z2unknown hook event %r in hooks: config (valid: %s)z, z3hooks.%s must be a list of hook definitions; got %s)rN   rf   r%   rH   itemsdifflibget_close_matchesr   r*   r+   joinsortedlisttyper:   	enumerate_parse_single_entryrT   )		hooks_cfgrf   rY   
event_nameentries
suggestionirawr\   s	            r/   rL   rL   1  s%    /i&	!#E(0 ##
G **[( 22J#J Q
1
 H		&*= > ?'4(NNEDM22 ( 	#FAs&z1c:DT"	#A##J Lr5   c                Z   t        |t              s,t        j                  d| |t	        |      j
                         y |j                  d      }t        |t              r|j                         st        j                  d| |       y |j                  d      }|)t        |t              st        j                  d| |       d }|| dvrt        j                  d| |||        d }|j                  dt              }	 t        |      }|d
k  r"t        j                  d| |t               t        }|t        kD  r#t        j                  d| ||t               t        }t        | |j                         ||      S # t        t        f$ r& t        j                  d	| ||t               t        }Y w xY w)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 %dsrh   z3hooks.%s[%d].timeout must be >=1; using default %dsz2hooks.%s[%d].timeout=%ds exceeds max %ds; clamping)r   r   r   r    )r%   rH   r*   r+   rq   r:   rM   r   r&   r?   r   	TypeError
ValueErrorMAX_TIMEOUT_SECONDSr   )r   indexry   r   r   timeout_rawr    s          r/   rs   rs   f  s    c4 I5$s),,	
 ggi Ggs#7==?A5	
 ggi G:gs#;C5	
 u,OOE 5'5		
 '')%<=K*k" {A51	
 *$$@5'#6	
 &	 + z" *M5+'>	
 **s   :E5 52F*)F*>   argsr3   
session_idparent_session_idc           	     \   ddddddd}	 t        j                  t        j                  j	                  | j
                              }|sd	|d<   |S t        j                         }t        rd
t               ini }	 t        j                  |f|d| j                  ddd|}|j(                  |d<   |j*                  xs d|d<   |j,                  xs d|d<   t        t        j                         |z
  d      |d<   |S # t        $ r!}d| j
                  d| |d<   |cY d}~S d}~ww xY w# t        j                  $ r. 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)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 commandcreationflagsT)inputcapture_outputr    textshellr      r   zcommand not foundzcommand not executabler   r   r   )shlexsplitospath
expanduserr   r   time	monotonicr   r   
subprocessrunr    TimeoutExpiredroundFileNotFoundErrorPermissionError	Exceptionr   r   r   r   )r\   
stdin_jsonresultargvr.   t0_popen_kwargsprocs           r/   _spawnr     s    F{{277--dll;< )w		B?I_&8&:;rM~~
LL
 
.  ??F<{{(bF8{{(bF8 %dnn&6&;Q ?FMK  $T\\$44GuMw& $$ "{$)$..*:R*?$C ! -w 2w c(wsM   <C: 9&D' :	D$DD$D$'>F+'F+8F+	F+F& F+&F+c                z     d fd}d j                    d j                   d|_        |j                  |_        |S )z>Build the closure that ``invoke_hook()`` will call per firing.c            	        j                   dv r!j                  | j                  d            sy t        t	        j                   |             }|d   r0t
        j                  dj                   j                  |d          y |d   r0t
        j                  d|d   j                   j                         y |d   j                         }|r/t
        j                  d	j                   j                  |d d
        |d   dk7  r3t
        j                  d|d   j                   j                  |d d
        t        j                   |d         S )Nr{   r3   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   r4   rM   r   _serialize_payloadr*   r+   r   r&   debug_parse_response)kwargsrr   r\   s      r/   	_callbackz!_make_callback.<locals>._callback  s(   ::<<$$VZZ%<=4+DJJ?@W:NN=

DLL!G* [>NNH#$djj$,, 8""$LL=

DLL&#, \?aNNG,T\\6$3< tzz1X;77r5   zshell_hook[:])r   r
   r7   Optional[Dict[str, Any]])r   r   r:   r<   )r\   r   s   ` r/   rU   rU     s>    "8H 'tzzl!DLL>CI&//Ir5   c                   |j                         D ci c]  \  }}|t        vs|| }}}	 t        t        j                               }| |j                  d      t        |j                  d      t              r|j                  d      nd|j                  d      xs |j                  d      xs d||d}t        j                  |dt        	      S c c}}w # t
        $ r d}Y w xY w)
zrRender the stdin JSON payload.  Unserialisable values are
    stringified via ``default=str`` rather than dropped.r   r3   r   Nr   r   )hook_event_namer3   
tool_inputr   cwdextraF)ensure_asciir!   )rk   _TOP_LEVEL_PAYLOAD_KEYSr   r	   r   OSErrorrM   r%   rH   jsondumps)r   r   kvextrasr   payloads          r/   r   r     s      &||~Rtq!:Q1QadRFR$((*o !ZZ,,6vzz&7I4,Pfjj(VZjj.W&**=P2QWUWG ::gE3?? S  s   CCC C)(C)c                B    | xs |}t        |t              r|r|S t        S )a  Return a validated string block message, falling back to the default.

    Accepts two candidate fields (primary wins over secondary) so callers
    can express field-priority differences between the two hook wire formats
    without duplicating the type-check logic.
    )r%   r   _DEFAULT_BLOCK_MESSAGE)primary	secondaryry   s      r/   _block_messager   +  s%     
YCS#&33J4JJr5   c                   |xs dj                         }|sy	 t        j                  |      }t        |t              sy| dk(  r|j                  d      dk(  r-dt        |j                  d      |j                  d	            d
S |j                  d      dk(  r-dt        |j                  d	      |j                  d            d
S y| dk(  rt        |j                  d      xs |j                  d      xs d      j                         j                         }|dv rW|j                  d      xs |j                  d	      }t        |t              r#|j                         rd|j                         d
S y|j                  d      }t        |t              r|j                         rd|iS y# t        j                  $ r t        j                  d| |dd        Y yw xY w)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   r|   actionblockmessagereason)r   r   decision
pre_verify>   r   continuer   context)r&   r   loadsJSONDecodeErrorr*   r+   r%   rH   rM   r   r   lower)r   r   datar   r   r   s         r/   r   r   6  s     l!!#Fzz&! dD!88H(%.)ATVZV^V^_gVh2ijj88J7*%.(ASUYU]U]^gUh2ijj TXXh'E488J+?E2FLLNTTV**hhy)?TXXh-?G'3'GMMO",IIhhy!G'3GMMO7##A  A6$3<	
 s   F+ +-GGc                 $    t               t        z  S )z/Path to the per-user shell-hook allowlist file.)r   ALLOWLIST_FILENAMEr@   r5   r/   allowlist_pathr   s  s    111r5   c                 "   	 t        j                  t               j                               } t        | t              sdg iS | j                  d      }t        |t              sg | d<   | S # t        t         j
                  t        f$ r dg icY S w xY w)z<Return the parsed allowlist, or an empty skeleton if absent.	approvals)r   r   r   	read_textr   r   r   r%   rH   rM   rp   )ry   r   s     r/   load_allowlistr   x  s    !jj)3356 c4 R  $Ii&KJ t33W= !R  !s   +A) )"BBc                H   t               }	 |j                  j                  dd       t        j                  |j
                   ddt        |j                              \  }}	 t        j                  |d      5 }|j                  t        j                  | dd             d	d	d	       t        ||       y	# 1 sw Y   xY w# t        $ r' 	 t        j                  |        # t        $ r Y  w xY ww xY w# t        $ r!}t         j#                  d
||       Y d	}~y	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''ffXQ<CM
H		2s# ErDdCDE8Q'E E  			(#   	  
B s	
 	

sf   AC7 %C ;(B8#C 8C=C 	C4C$#C4$	C0-C4/C00C44C7 7	D! DD!c                d     t               }t         fd|j                  dg       D              S )Nc              3     K   | ]@  }t        |t              xr* |j                  d       k(  xr |j                  d      k(   B yw)r   r   N)r%   rH   rM   ).0er   r   s     r/   	<genexpr>z"_is_allowlisted.<locals>.<genexpr>  sR        	1d 	(EE'Ne#	(EE)'	(s   AA	r   )r   anyrM   )r   r   r   s   `` r/   rP   rP     s2    D  +r*	  r5   c               #    K   t               } | j                  j                  dd       | j                  | j                  dz         }t
        )t        5  t               }| t        |       ddd       yt        |dd      5 }t        j                  |j                         t
        j                         	 t               }| t        |       	 t        j                  |j                         t
        j                         ddd       y# 1 sw Y   yxY w# t        t        f$ r Y &w xY w# 	 t        j                  |j                         t
        j                         w # t        t        f$ r Y w w xY wxY w# 1 sw Y   yxY ww)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+zutf-8)encoding)r   r   r   with_suffixr   fcntl_allowlist_write_lockr   r   openflockfilenoLOCK_EXLOCK_UNr   IOError)r   	lock_pathr   lock_fhs       r/   _locked_update_approvalsr    s7     	AHHNN4$N/ahh01I}" 	!!#DJ4 	! 		i	0 
GGNN$emm4	!#DJ4 GNN,emm<
 
	! 	 W% GNN,emm<W% 
 
s   AFD.F3E<9D/2D	FDFD,)E<+D,,E</E912E$#E9$E6	3E95E6	6E99E<<FFc               b   |r$t        | |       t        j                  d| |       yt        j                  j                         syt        d|  d| d       	 t        d      j                         j                         }|dv rt        | |       yy# t        t        f$ r t                Y yw xY w)	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*   rJ   sysstdinisattyprintr   r&   r   EOFErrorKeyboardInterrupt)r   r   rB   answers       r/   rQ   rQ     s     (w	
 99	w y ! 	!89??AGGI
 ( '( s   'B B.-B.c                :   | |t               t        |      d}t               5 }|j                  dg       D cg c]<  }t	        |t
              r(|j                  d      | k(  r|j                  d      |k(  s|> c}|gz   |d<   d d d        y c c}w # 1 sw Y   y xY w)N)r   r   approved_atscript_mtime_at_approvalr   r   r   )_utc_now_isoscript_mtime_isor  rM   r%   rH   )r   r   entryr   r   s        r/   r  r    s    #~$4W$=	E 
"	# txxR0
1d#EE'Ne+EE)$/	 
 G[ 
 s   BAB9
BBBc                     t        j                  t        j                        j	                         j                  dd      S )Ntz+00:00Z)r   nowr   utc	isoformatreplacer@   r5   r/   r  r  
  s+    <<8<<(224<<XsKKr5   c                <   t               5 }t        |j                  dg             }|j                  dg       D cg c](  }t        |t              r|j                  d      | k(  s|* c}|d<   t        |d         }ddd       ||z
  S c c}w # 1 sw Y   z
  S xY w)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   r   N)r  lenrM   r%   rH   )r   r   beforer   afters        r/   revoker&    s     
"	# 'tTXXk2./xxR0
q$'AEE),<,G 
[ D%&' E>
' E>s   0B-B	(B	BB)z.shz.bashz.zshz.fishz.pyz.pywz.rbz.plz.luaz.jsz.mjsz.cjsz.tszTuple[str, ...]_SCRIPT_EXTENSIONSc                    	 t        j                  |       }|s| S |D ])  }|j                         j	                  t
              s'|c S  |D ]  }d|v s|j                  d      s|c S  |d   S # t        $ r | cY S w xY w)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K  $;$//#.K 8O  s   A. .A<;A<c                4   |ryt         j                  j                  dd      j                         j	                         }|dv ry| j                  dd      }t        |t              r|S t        |t              r |j                         j	                         dv S y)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onr
  truehooks_auto_acceptF)r   environrM   r&   r   r%   r9   r   )rW   accept_hooks_argenvcfg_vals       r/   rK   rK   B  s     
**...
3
9
9
;
A
A
CC
((gg)51G'4 '3}}$$&*DDDr5   c                    t               j                  dg       D ]@  }t        |t              s|j                  d      | k(  s)|j                  d      |k(  s>|c S  y)z2Return the allowlist record for this pair, if any.r   r   r   N)r   rM   r%   rH   )r   r   r   s      r/   allowlist_entry_forr;  ]  sV    !!+r2 q$g%'i G+H r5   c                <   t        |       }|sy	 t        j                  j                  |      }t	        j
                  t        j                  j                  |      t        j                        j                         j                  dd      S # t        $ r Y y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  i  s~      (D77%%d+%%GGX&8<<

)+ggh,	-  s   A>B 	BBc                   t        |       }|syt        j                  j                  |      }t        j                  j	                  |      sy	 t        j                  |       }t        |      xr |d   |k(  }|rt        j                  nt        j                  }t        j                  ||      S # t        $ r Y yw xY w)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   r9   X_OKR_OKaccess)r   r   r?  r   is_bare_invocationrequireds         r/   script_is_executablerG  x  s      (Dww!!$'H77>>(#{{7# d7Q4,rww"''H99Xx((	  s   B1 1	B=<B=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   )r\   r   r   r   s       r/   run_oncerJ    s?     $DJJ7JD*%F&tzz6(3CDF8Mr5   )rW   r   rB   r9   r7   List[ShellHookSpec])rW   r   r7   rK  r6   )rt   r
   r7   rK  )r   r   r   r   ry   r
   r7   zOptional[ShellHookSpec])r\   r   r   r   r7   Dict[str, Any])r\   r   r7   z'Callable[..., Optional[Dict[str, Any]]])r   r   r   rL  r7   r   )r   r
   r   r
   r7   r   )r   r   r   r   r7   r   )r7   r	   )r7   rL  )r   rL  r7   r8   )r   r   r   r   r7   r9   )r7   zIterator[Dict[str, Any]])r   r   r   r   rB   r9   r7   r9   )r   r   r   r   r7   r8   )r7   r   )r   r   r7   r   )r   r   r7   r   )rW   rL  r7  r9   r7   r9   )r   r   r   r   r7   r   )r   r   r7   r   )r   r   r7   r9   )r\   r   r   rL  r7   rL  )Rr=   
__future__r   rl   r   loggingr   r'   r   r   r  r   	threadingr   
contextlibr   dataclassesr   r   r   r   pathlibr	   typingr
   r   r   r   r   r   r   r   hermes_cli._subprocess_compatr   r   r   ImportErrorhermes_constantsr   rI   r   	getLoggerr:   r*   r?   r   r   r   setr   r>   LockrO   r   r   r_   ra   rd   rL   rs   r   r   rU   r   r   r   r   r   r   rP   r  rQ   r  r  r&  r'  r/  rK   r;  r  rG  rJ  r@   r5   r/   <module>rZ     s.  gR #    	 	   
    % ( '  L L L H -  			8	$  1 1  4750 8!9>>#  '	(  #) #) #)Z Q	!Q Q 	Qh02j@@@!$@@N S 9x)X@&K6z2

@  B###/3#	#L$L"' O 6	+/	6	).
!/]  Es   /E/ /E:9E: