§
    øžyjŽw  ã                  óD  — 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m	Z	 ddl
mZmZmZ  ej        e¦  «        Z G d„ de¦  «        Z ej        d¦  «        Z ej        d	¦  «        ZdFd„Z eh d£¦  «        Z eh d£¦  «        ZdZdZ ed¦  «        Z eh d£¦  «        ZdZ ej        d¦  «        ZdZ dZ!ee"gee"         f         Z#dGd„Z$dHd„Z%dId „Z&dJd!„Z'dFd"„Z(dId#„Z)dKd&„Z*dLd)„Z+dd*œdMd,„Z,dNd.„Z-dOd0„Z.dPd4„Z/dQd5„Z0dd6œdRd=„Z1ddd>œdSd?„Z2dTd@„Z3dUdA„Z4	 dVdWdE„Z5dS )Xu~  Gateway lifecycle guard for cron job creation (#30719).

An agent running inside a gateway can schedule a cron job that calls
``hermes gateway restart`` (or ``launchctl kickstart ai.hermes.gateway``
or ``systemctl restart hermes-gateway``).  When the cron fires, the
gateway dies, the supervisor (launchd KeepAlive / systemd Restart=)
revives it, auto-resume picks up the offending session, and the resumed
turn re-runs the same logic â€” a SIGTERM-respawn loop every ~10 seconds
until manually broken.

This module rejects cron job specs whose prompt or script contains a
direct shell-level gateway-lifecycle command.  It is enforced at
``cron.jobs.create_job`` so it fires on every job-creation path: the
``hermes cron create`` CLI subcommand AND the agent's ``cronjob`` model
tool (which calls ``create_job`` directly, bypassing the CLI layer).

The pattern is intentionally command-shaped: it anchors on a concrete
command identifier (``hermes gateway``, ``launchctl ... hermes-gateway``,
``systemctl ... hermes-gateway``, ``pkill`` against the gateway) so it
cannot fire on prose.  A cron ``prompt`` is fed to a future LLM, not a
shell, so an over-broad substring match on English ("Kong API gateway
autoscaling and restart behavior") would produce a high false-positive
rate without preventing the actual foot-gun, which requires a real
command shape.

This is a defence-in-depth layer.  ``tools/terminal_tool.py`` blocks direct
commands and shell scripts they reference when ``_HERMES_GATEWAY=1``. It also
rejects ``launchctl submit`` in gateway sessions because launchd treats that
primitive as a persistent KeepAlive job, not a one-shot task. ``hermes gateway
stop|restart`` separately refuse to self-target from inside the gateway.
Blocking cron specs at creation time as well means the agent gets an immediate,
informative rejection instead of scheduling a job that will only fail
(silently) when it fires.
é    )ÚannotationsN)ÚPath)ÚCallableÚIteratorÚOptionalc                  ó   — e Zd ZdZdS )ÚGatewayLifecycleBlockedzARaised when a cron job spec contains a gateway-lifecycle command.N)Ú__name__Ú
__module__Ú__qualname__Ú__doc__© ó    ú:/home/ragecks/.hermes/hermes-agent/cron/lifecycle_guard.pyr	   r	   1   s   € € € € € ØKÐKÐKÐKr   r	   a8  (?i)(?:hermes\s+gateway\s+(?:restart|stop))|(?:launchctl\s+(?:kickstart|unload|load|stop|restart|submit|bootstrap)\b[^\n]*\bhermes[.\-]?gateway)|(?:systemctl\s+(?:-\S+\s+)*(?:restart|stop|start)\b[^\n]*\bhermes[.\-]?gateway)|(?:p?kill\b[^\n]*\bhermes\b[^\n]*\bgateway)|(?:p?kill\b[^\n]*\bgateway\b[^\n]*\bhermes)z\\\r?\n[ \t]*ÚtextÚstrÚreturnÚboolc                óŽ   — | sdS t                                d| ¦  «        }t          t                               |¦  «        ¦  «        S )zCReturn True if *text* contains a gateway lifecycle command pattern.FÚ )Ú_SHELL_LINE_CONTINUATIONÚsubr   Ú_GATEWAY_LIFECYCLE_PATTERNÚsearch©r   Ú
normalizeds     r   Ú"contains_gateway_lifecycle_commandr   b   sA   € àð ØˆuÝ)×-Ò-¨c°4Ñ8Ô8€JÝÕ*×1Ò1°*Ñ=Ô=Ñ>Ô>Ð>r   >   ÚshÚkshÚzshÚbashÚdash>   ú+Oú+oú-Oú-oi   é   ú;&|()>	   ÚagÚrgÚackÚgrepÚpsqlÚegrepÚfgrepÚsqlite3Ú
journalctl)ú`z$(z<(z>(z\!zB\|\s*&?\s*(?:sudo\s+)?(?:sh|bash|dash|ksh|zsh|xargs|eval|source)\b)s   ELFs   MZs   Êþº¾s   Ïúíþs   Îúíþs   þíúÎs   þíúÏi   ÚcommandúIterator[list[str]]c              #  ó€  K  — |                       dd¦  «        }|                     ¦   «         p|gD ]�}	 t          j        |dd¬¦  «        }d|_        d|_        t          |¦  «        }n# t          $ r Y ŒDw xY wg }|D ]:}|r!t          |¦  «        t          k    r	|r|V — g }Œ%| 	                    |¦  «         Œ;|r|V — ŒŽdS )zEYield shell-tokenized command segments, honoring quotes and comments.z\
Ú Tr(   ©ÚposixÚpunctuation_charsú#N)
ÚreplaceÚ
splitlinesÚshlexÚwhitespace_splitÚ
commentersÚlistÚ
ValueErrorÚsetÚ_CONTROL_CHARSÚappend)r3   r   ÚlineÚlexerÚtokensÚsegmentÚtokens          r   Ú_iter_command_segmentsrJ   š   s  è è € à—’ ¨Ñ,Ô,€JØ×%Ò%Ñ'Ô'Ð7¨J¨<ð ð ˆð
	Ý”KØØØ")ðñ ô ˆEð
 &*ˆEÔ"Ø"ˆEÔÝ˜%‘[”[ˆFˆFøÝð 	ð 	ð 	ØˆHð	øøøð  ˆØð 	"ð 	"ˆEØð �˜U™œ¥~Ò5Ð5Øð !Ø!�M�M�MØ �GØØ�NŠN˜5Ñ!Ô!Ð!Ð!Øð 	ØˆMˆMˆMøð-ð s   ³4A(Á(
A5Á4A5rH   ú	list[str]úOptional[int]c                ób   — t          | ¦  «        D ]\  }}t          j        d|¦  «        rŒ|c S dS )z?Return the executable token index after simple env assignments.z^[A-Za-z_][A-Za-z0-9_]*=N)Ú	enumerateÚreÚmatch)rH   ÚindexrI   s      r   Ú_command_token_indexrR   ¶   sC   € å! 'Ñ*Ô*ð ð ‰ˆˆuÝŒ8Ð/°Ñ7Ô7ð 	ØØˆˆˆØˆ4r   c                óæ   — t          | ¦  «        D ]`}t          |¦  «        }|€Œt          ||         ¦  «        j        dk    r.||dz   d…         }|r|d                              ¦   «         dv r dS ŒadS )aü  Detect an executed ``launchctl submit``/``bootstrap``, not quoted text.

    Label-independent by design: the label of a submitted/bootstrapped job is
    chosen by whoever writes it, so a neutral name (``ai.hermes.svc-reload-tmp``)
    defeats any label-anchored regex (#62891, second reproduction). Both verbs
    register a NEW persistent launchd job (``submit`` jobs get KeepAlive
    semantics; ``bootstrap`` loads an arbitrary plist), which is never safe to
    do from inside the gateway process.
    NÚ	launchctlé   r   >   ÚsubmitÚ	bootstrapTF)rJ   rR   r   ÚnameÚlower)r3   rH   rQ   Ú	argumentss       r   Ú!contains_launchctl_submit_commandr[   ¿   sŒ   € õ *¨'Ñ2Ô2ð ð ˆÝ$ WÑ-Ô-ˆØˆ=ØÝ�˜”ÑÔÔ$¨Ò3Ð3Ø ¨¡	  Ô,ˆIØð ˜Y qœ\×/Ò/Ñ1Ô1Ð5LÐLÐLØ�t�tøØˆ5r   c                óB  — g }d}|                       ¦   «         p| gD �]ê}t                               |¦  «        r|                     |¦  «         Œ3	 t	          j        |dd¬¦  «        }d|_        d|_        t          |¦  «        }n%# t          $ r |                     |¦  «         Y ŒŠw xY wg }g }|D ]_}|rFt          |¦  «        t          k    r.|                     |¦  «         |                     |g¦  «         g }ŒJ|                     |¦  «         Œ`|                     |¦  «         g }	|D ]´}
|
sŒt          |
¦  «        }|�‰t          |
|         ¦  «        j        t          v rh|
|dz   d…         }t          d„ |D ¦   «         ¦  «        sBd}|	                     |
d|dz   …         ¦  «         |	                     d	„ |D ¦   «         ¦  «         ŒŸ|	                     |
¦  «         Œµ|                     d
                     |	¦  «        ¦  «         �Œì|s| S d                     |¦  «        S )u   Replace data-sink executables' arguments with a neutral placeholder.

    The lifecycle regex is command-shaped, but it cannot tell an EXECUTED
    ``systemctl restart hermes-gateway`` from the same characters appearing
    as *data* â€” a grep/rg pattern, a journalctl filter, a SQL string literal
    passed to sqlite3/psql. Those diagnostics commands were being rejected
    (false positives blocking legitimate cron prompts), e.g.::

        grep -c 'systemctl restart hermes-gateway' /var/log/syslog
        sqlite3 db "SELECT msg FROM log WHERE msg LIKE '%systemctl restart hermes-gateway%'"

    This masker shell-tokenizes each line and, for command segments whose
    executable is a known data sink (``_DATA_SINK_EXECUTABLES``), replaces
    every argument with ``arg``. The caller then re-runs the lifecycle regex
    on the masked text: a match that survives masking sits OUTSIDE any data
    argument and is a real command.

    Strictly fail-closed: masking is skipped (leaving the original,
    regex-matching text in place) whenever the line pipes into a shell or
    interpreter, any argument carries an execution-capable marker
    (substitution, sqlite3 ``.``-commands, psql ``\!``), or the line cannot
    be tokenized at all. Masking can therefore only ever ALLOW a command the
    plain regex would have blocked â€” never block one it would have allowed â€”
    so it runs solely as a second-pass exemption check.
    FTr(   r7   r:   NrU   c              3  ó‚   ‡K  — | ]9Š‰                      d ¦  «        pt          ˆfd„t          D ¦   «         ¦  «        V — Œ:dS )ú.c              3  ó    •K  — | ]}|‰v V — Œ	d S ©Nr   )Ú.0ÚmarkerÚarguments     €r   ú	<genexpr>z6_mask_data_sink_arguments.<locals>.<genexpr>.<genexpr>  s(   øè è € ÐUÐU°&˜6 XÐ-ÐUÐUÐUÐUÐUÐUr   N)Ú
startswithÚanyÚ_UNSAFE_DATA_ARG_MARKERS)ra   rc   s    @r   rd   z,_mask_data_sink_arguments.<locals>.<genexpr>  sn   øè è € ð ð ð !ð ×'Ò'¨Ñ,Ô,ð VÝÐUÐUÐUÐUÕ<TÐUÑUÔUÑUÔUðð ð ð ð ð r   c              3  ó   K  — | ]}d V — ŒdS )ÚargNr   )ra   Ú_s     r   rd   z,_mask_data_sink_arguments.<locals>.<genexpr>  s"   è è € Ð"<Ð"<¨Q 5Ð"<Ð"<Ð"<Ð"<Ð"<Ð"<r   r   Ú
)r<   Ú_PIPE_TO_INTERPRETERr   rD   r=   r>   r?   r@   rA   rB   rC   rR   r   rX   Ú_DATA_SINK_EXECUTABLESrf   ÚextendÚjoin)r   Ú	lines_outÚchangedrE   rF   rG   ÚsegmentsÚcurrentrI   ÚrebuiltrH   rQ   rZ   s                r   Ú_mask_data_sink_argumentsru   Ô   s~  € ð4 €IØ€GØ—’Ñ!Ô!Ð+ d Vð ),ñ ),ˆÝ×&Ò& tÑ,Ô,ð 	Ø×Ò˜TÑ"Ô"Ð"Øð	Ý”K ¨DÀGÐLÑLÔLˆEØ%)ˆEÔ"Ø"ˆEÔÝ˜%‘[”[ˆFˆFøÝð 	ð 	ð 	Ø×Ò˜TÑ"Ô"Ð"ØˆHð	øøøð %'ˆØˆØð 	"ð 	"ˆEØð �˜U™œ¥~Ò5Ð5Ø—’ Ñ(Ô(Ð(Ø—’  Ñ(Ô(Ð(Ø�ØØ�NŠN˜5Ñ!Ô!Ð!Ð!Ø�Š˜Ñ Ô Ð àˆØð 	$ð 	$ˆGØð ØÝ(¨Ñ1Ô1ˆEØÐ ¥T¨'°%¬.Ñ%9Ô%9Ô%>ÕBXÐ%XÐ%XØ# E¨A¡I K KÔ0�	Ýð ð ð %.ðñ ô ñ ô ð ð
 #�GØ—N’N 7¨;¨U°Q©Y¨;Ô#7Ñ8Ô8Ð8Ø—N’NÐ"<Ð"<°)Ð"<Ñ"<Ô"<Ñ<Ô<Ð<ØØ�NŠN˜7Ñ#Ô#Ð#Ð#Ø×Ò˜Ÿš 'Ñ*Ô*Ñ+Ô+Ð+Ñ+Øð ØˆØ�9Š9�YÑÔÐs   Á4BÂB'Â&B'c                ó’   — t          | ¦  «        sdS t                               d| ¦  «        }t          t          |¦  «        ¦  «        S )uO  Lifecycle-regex scan that exempts matches living inside data arguments.

    Two-pass: the cheap regex first (the overwhelmingly common no-match case
    pays nothing extra); on a raw match, re-scan with data-sink arguments
    masked out. Only a match that survives masking â€” i.e. one in actual
    command position â€” blocks.
    Fr   )r   r   r   ru   r   s     r   Ú+_lifecycle_command_scan_with_data_exemptionrw     sE   € õ .¨dÑ3Ô3ð ØˆuÝ)×-Ò-¨c°4Ñ8Ô8€JÝ-Õ.GÈ
Ñ.SÔ.SÑTÔTÐTr   c                ó>   — t          | ¦  «        pt          | ¦  «        S )zCPure-string direct scans: lifecycle regex (data-exempted) + submit.)rw   r[   )r3   s    r   Ú_direct_lifecycle_scanry   -  s'   € å6Øñô ð 4å	*¨7Ñ	3Ô	3ð4r   Ú	candidateúOptional[Path]c                ó’   — | rd| v rdS 	 t          | ¦  «                             ¦   «         S # t          t          t          f$ r Y dS w xY w)u—  Sanitize a tokenized path candidate at the ingestion boundary.

    Candidate tokens come from shlex-splitting arbitrary command text â€”
    including text recursively decoded from binaries or remote reads â€” so
    they can carry NUL bytes or other junk no real filesystem path can
    contain. Every OS-facing ``Path`` operation downstream (``expanduser``,
    ``os.open``, ``resolve``) raises a *different* exception for the same
    junk (``ValueError: embedded null byte``, ``RuntimeError: Could not
    determine home directory`` when HOME is unset under launchd, OSError
    for over-long paths). Rejecting here â€” once, before any OS call â€” is
    the whole-class fix; catching per-syscall was the whack-a-mole that
    produced #76762, #77703, #77780, and #78256.

    Returns ``None`` for candidates that cannot be a real path (nothing to
    scan), otherwise the ``expanduser()``-expanded ``Path``.
    ú N)r   Ú
expanduserrA   ÚRuntimeErrorÚOSError)rz   s    r   Ú_expand_candidate_pathr�   4  sa   € ð" ð ˜ )Ð+Ð+ØˆtðÝ�I‰Œ×)Ò)Ñ+Ô+Ð+øÝ�¥gÐ.ð ð ð Øˆtˆtðøøøs   Š + «AÁAÚcwdúOptional[str]c                óÄ   — t          | ¦  «        }|€d S |                     ¦   «         s8	 t          |pt          j        ¦   «         ¦  «        |z  }n# t          $ r Y d S w xY w|S r`   )r�   Úis_absoluter   r‚   r€   )rz   r‚   Úpaths      r   Ú_resolve_terminal_script_pathr‡   M  sz   € Ý! )Ñ,Ô,€DØ€|ØˆtØ×ÒÑÔð ð	Ý˜Ð)�tœx™zœzÑ*Ô*¨TÑ1ˆDˆDøÝð 	ð 	ð 	à�4�4ð	øøøð €Ks   ©%A Á
AÁA©r‚   úIterator[Path]c             #  óÞ  K  — t          | ¦  «        D �]Y}t          |¦  «        }|€Œ||         }t          |¦  «        j        }|dv r6t	          |¦  «        |dz   k    rt          ||dz            |¦  «        }|�|V — Œk|t          v r¡||dz   d…         }d}|t	          |¦  «        k     rD||         }	|	dk    r|dz  }n0|	dv rn+|	t          v r|dz  }Œ;|	                     d¦  «        r|dz  }ŒV	 |t	          |¦  «        k     r&||         dvrt          ||         |¦  «        }|�|V — �Œ| 	                    d	¦  «        r/d	|v s| 
                    d
¦  «        rt          ||¦  «        }|�|V — �Œ[dS )z9Yield scripts executed directly or through a POSIX shell.N>   Úsourcer^   rU   r   z--¾   ú-cú	--commandé   ú-ú/)z.shz.bashz.zsh)rJ   rR   r   rX   Úlenr‡   Ú_SHELL_EXECUTABLESÚ_SHELL_OPTIONS_WITH_VALUESre   ÚstripÚendswith)
r3   r‚   rH   rQ   Ú
executableÚexecutable_nameÚresolvedrZ   Ú	arg_indexrc   s
             r   Ú_iter_referenced_shell_scriptsr›   Z  sé  è è € õ *¨'Ñ2Ô2ð 1#ñ 1#ˆÝ$ WÑ-Ô-ˆØˆ=ØØ˜U”^ˆ
Ý˜zÑ*Ô*Ô/ˆà˜oÐ-Ð-Ý�7‰|Œ|˜e a™iÒ'Ð'Ý8¸ÀÈÁÔ9KÈSÑQÔQ�ØÐ'Ø"�N�N�NØàÕ0Ð0Ð0Ø ¨¡	  Ô,ˆIØˆIØ�c )™nœnÒ,Ð,Ø$ YÔ/�Ø˜tÒ#Ð#Ø ‘N�IØØÐ2Ð2Ð2ØØÕ9Ð9Ð9Ø ‘N�IØØ×&Ò& sÑ+Ô+ð Ø ‘N�IØØØ�3˜y™>œ>Ò)Ð)¨i¸	Ô.Bð Kð /ð /õ 9¸À9Ô9MÈsÑSÔS�ØÐ'Ø"�N�N�NÙð ×Ò˜CÑ Ô ð 	#Ø�jÐ Ð  J×$7Ò$7Ð8PÑ$QÔ$QÐ Ý8¸ÀSÑIÔI�ØÐ'Ø"�N�N�Nùðc1#ð 1#r   úIterator[str]c              #  ó  K  — t          | ¦  «        D ]r}t          |¦  «        }|�!t          ||         ¦  «        j        t          vrŒ5||dz   d…         }t          |dd…         ¦  «        D ]\  }}|dv r||dz            V —  nŒŒsdS )zDYield code passed through ``sh|bash|... -c`` for recursive scanning.NrU   éÿÿÿÿrŒ   )rJ   rR   r   rX   r“   rN   )r3   rH   rQ   rZ   rš   rc   s         r   Ú_iter_shell_command_payloadsrŸ   ”  s¸   è è € å)¨'Ñ2Ô2ð ð ˆÝ$ WÑ-Ô-ˆØˆ=�D ¨¤Ñ0Ô0Ô5Õ=OÐOÐOØØ˜E A™I˜K˜KÔ(ˆ	Ý#,¨Y°s¸°s¬^Ñ#<Ô#<ð 	ð 	ÑˆI�xØÐ.Ð.Ð.Ø 	¨A¡Ô.Ð.Ð.Ð.Ø�ð /øðð r   Úscript_pathc                óœ   — 	 t          | ¦  «        }|�(|                     ¦   «         rt          |j        ¦  «        S n# t          $ r Y nw xY wdS )zHReturn the directory *script_path* resolves to, handling relative names.N)Ú_resolve_script_pathr…   r   ÚparentÚ	Exception)r    r†   s     r   Ú_resolve_script_directoryr¥   ¡  sa   € ðÝ# KÑ0Ô0ˆØÐ × 0Ò 0Ñ 2Ô 2ÐÝ�t”{Ñ#Ô#Ð#øøÝð ð ð Øˆðøøøàˆ4s   ‚8< ¼
A	ÁA	r†   r   útuple[Optional[str], bool]c                ó²  — t           j        t          t           dd¦  «        z  }	 t          j        | |¦  «        }n# t          t
          f$ r Y dS w xY w	 t          j        |¦  «        }t          j        |j	        ¦  «        s	 t          j
        |¦  «         dS t          j        |t          ¦  «        }|                     t          ¦  «        sd|v r	 t          j
        |¦  «         dS t          |¦  «        t           k    rMt          j        |t           dz   t          |¦  «        z
  ¦  «        }|sn||z  }t          |¦  «        t           k    °Mn%# t          $ r Y t          j
        |¦  «         dS w xY wt          j
        |¦  «         n# t          j
        |¦  «         w xY wd|v rdS t          |¦  «        t           k    rdS |                     dd¬	¦  «        d
fS )zAReturn ``(text, unsafe)`` using bounded, regular-file-only reads.Ú
O_NONBLOCKr   ©NF©NTó    rU   úutf-8r;   ©ÚerrorsF)ÚosÚO_RDONLYÚgetattrÚopenr€   rA   ÚfstatÚstatÚS_ISREGÚst_modeÚcloseÚreadÚ_BINARY_SNIFF_BYTESre   Ú_BINARY_MAGIC_PREFIXESr’   Ú_MAX_REFERENCED_SCRIPT_BYTESÚdecode)r†   ÚflagsÚ
descriptorÚmetadataÚdataÚchunks         r   Ú_read_referenced_scriptrÂ   ¬  sô  € åŒK�'¥" l°AÑ6Ô6Ñ6€EðÝ”W˜T 5Ñ)Ô)ˆ
ˆ
øÝ•ZÐ ð ð ð ð ˆ{ˆ{ðøøøðÝ”8˜JÑ'Ô'ˆÝŒ|˜HÔ,Ñ-Ô-ð 	Øõ* 	Œ�ÑÔÐÐÐõ Œw�zÕ#6Ñ7Ô7ˆØ�?Š?Õ1Ñ2Ô2ð 	°gÀ°o°oØõ 	Œ�ÑÔÐÐÐõ �$‰iŒiÕ7Ò7Ð7Ý”GØÕ8¸1Ñ<½sÀ4¹y¼yÑHñô ˆEð ð ØØ�E‰MˆDõ �$‰iŒiÕ7Ò7Ð7øøõ ð ð ð Øå
Œ�ÑÔÐÐÐðøøøõ 	Œ�ÑÔÐÐø�Œ�ÑÔÐÐøøøð �$€€Øˆ{Ý
ˆ4�y„yÕ/Ò/Ð/ØˆzØ�;Š;�w yˆ;Ñ1Ô1°5Ð8Ð8sF   ¥; »AÁAÁ-E Â8E Ã'A%E ÅF Å
E/ÅF Å.E/Å/F ÆFc                ó|   — | sdS d| v rdS t          |                      dd¬¦  «        ¦  «        t          k    rdS | dfS )u¿  Apply the local-read contract to text from a ``read_remote_script`` callback.

    The recursion boundary must not trust its callbacks: any backend (SSH,
    Modal, Daytona, or a future one) can hand back raw binary bytes decoded
    as text, or arbitrarily large output. Mirror
    ``_read_referenced_script``'s semantics exactly â€” NUL bytes mean binary
    (nothing to scan, checked first, #77703), oversized text fails closed
    like an oversized local file (#76762) â€” so remote and local reads can
    never diverge again. The size check re-encodes to compare *bytes*
    (matching the local read and the ``head -c`` wire bound): a >1 MiB
    multibyte file truncated at the byte cap decodes to fewer characters
    than bytes, and a character-count check would scan the truncated text
    instead of failing closed. Enforced here rather than inside each
    callback so the guarantee holds for every callback, not just the ones
    we hardened.
    r©   r}   r¬   r;   r­   rª   F)r’   Úencoder»   )r   s    r   Ú_sanitize_remote_script_textrÅ   Þ  sR   € ð" ð Øˆ{Ø�€~€~Øˆ{Ý
ˆ4�;Š;�w yˆ;Ñ1Ô1Ñ2Ô2Õ5QÒQÐQØˆzØ�ˆ;Ðr   )Úread_remote_scriptÚdepthÚintÚvisitedú	set[Path]rÆ   úOptional[_ReadRemoteScriptFn]c               ó^  — t          | ¦  «        rdS |t          k    rdS t          | ¦  «        D ]}t          |||dz   ||¬¦  «        r dS Œt	          | |¬¦  «        D ]Ð}	 |                     d¬¦  «        }n# t          t          f$ r |}Y nw xY w||v rŒ8|                     |¦  «         t          |¦  «        \  }}	|	r dS |€/|�-t           |t          |¦  «        ¦  «        ¦  «        \  }}	|	r dS |sŒ˜t          t          |¦  «        ¦  «        p|}
t          ||
|dz   ||¬¦  «        r dS ŒÑdS )NTrU   ©r‚   rÇ   rÉ   rÆ   rˆ   F)Ústrict)ry   Ú_MAX_REFERENCED_SCRIPT_DEPTHrŸ   Ú_contains_unsafe_gateway_actionr›   Úresolver€   rA   ÚaddrÂ   rÅ   r   r¥   )r3   r‚   rÇ   rÉ   rÆ   Úpayloadr    r™   Úscript_textÚunsafeÚ
script_dirs              r   rÐ   rÐ   ø  sÏ  € õ ˜gÑ&Ô&ð ØˆtØÕ,Ò,Ð,Øˆtå/°Ñ8Ô8ð ð ˆÝ*ØØØ˜!‘)ØØ1ð
ñ 
ô 
ð 	ð �4�4ð	õ 6°gÀ3ÐGÑGÔGð $ð $ˆð	#Ø"×*Ò*°%Ð*Ñ8Ô8ˆHˆHøÝ�Ð$ð 	#ð 	#ð 	#ð #ˆHˆHˆHð		#øøøð
 �wÐÐØØ�Š�HÑÔÐÝ5°kÑBÔBÑˆ�VØð 	Ø�4�4ØÐÐ#5Ð#Aõ
 #?Ø"Ð"¥3 {Ñ#3Ô#3Ñ4Ô4ñ#ô #ÑˆK˜ð ð Ø�t�tØð 	Øõ /­s°8©}¬}Ñ=Ô=ÐDÀˆ
Ý*ØØØ˜!‘)ØØ1ð
ñ 
ô 
ð 	ð �4�4ð	ð ˆ5s   ÁA6Á6BÂB)r‚   rÆ   c               ó  — 	 t          | |dt          ¦   «         |¬¦  «        S # t          $ r_ t                               dd¬¦  «         	 t          | ¦  «        cY S # t          $ r# t          | ¦  «        pt          | ¦  «        cY cY S w xY ww xY w)uF  Detect lifecycle/submit commands, including bounded nested scripts.

    Total by construction: this function returns a verdict for *every*
    input and never raises. The direct scans below are pure string
    operations; the referenced-script walk touches the filesystem, remote
    backends, and shlex on arbitrary decoded bytes, so it is best-effort
    defense-in-depth â€” any unexpected failure inside it is logged and
    treated as "walk found nothing" rather than crashing the caller.

    This is the contract #76762 established ("a guarded path must never
    crash the guard") enforced at the boundary instead of per-syscall: a
    guard crash propagates out of ``tools/terminal_tool.py`` and breaks
    every terminal command until the gateway restarts (#77780, #78256),
    which is strictly worse than either verdict.
    r   rÍ   zRlifecycle guard referenced-script walk failed; falling back to direct-scan verdictT)Úexc_info)rÐ   rB   r¤   ÚloggerÚwarningry   r   r[   )r3   r‚   rÆ   s      r   Ú7contains_gateway_lifecycle_command_or_referenced_scriptrÛ   7  sà   € ð*<å.ØØØÝ‘E”EØ1ð
ñ 
ô 
ð 	
øõ ð <ð <ð <Ý�Šð2àð 	ñ 	
ô 	
ð 	
ð	<Ý)¨'Ñ2Ô2Ð2Ð2Ð2øÝð 	<ð 	<ð 	<õ 6Øñô ð <å2°7Ñ;Ô;ð<ð <ð <ð <ð <ð		<øøøð<øøøs-   ‚" ¢&BÁ	AÁBÁ(BÂBÂBÂBc                ó²   — ddl m} t          | ¦  «        }|€dS |                     ¦   «         r|S 	  |¦   «         dz  |z  S # t          t
          f$ r Y dS w xY w)uI  Resolve a cron ``script`` value the same way the scheduler does.

    The scheduler (``cron.scheduler``) resolves a bare/relative script path
    under ``<HERMES_HOME>/scripts/`` and only accepts absolute paths as-is.
    We MUST mirror that here so the guard scans the file that will actually
    run â€” otherwise a job whose script lives at the scheduler's real location
    (``~/.hermes/scripts/restart.sh``) but is passed as the bare name
    ``restart.sh`` would read as a nonexistent relative path and silently
    scan prompt-only content, letting the command through.

    Returns ``None`` for values that cannot be a real path (NUL bytes,
    unexpandable ``~``) â€” the same ingestion contract as
    ``_expand_candidate_path``; such a value can never name a file the
    scheduler would execute, so there is nothing to scan.
    r   )Úget_hermes_homeNÚscripts)Úhermes_constantsrÝ   r�   r…   r   r€   )r    rÝ   Úraws      r   r¢   r¢   i  sŠ   € ð  1Ð0Ð0Ð0Ð0Ð0å
  Ñ
-Ô
-€CØ
€{ØˆtØ
‡‚ÑÔð Øˆ
ðØˆÑ Ô  9Ñ,¨sÑ2Ð2øÝ�'Ð"ð ð ð ð ˆtˆtð	øøøs   ±A ÁAÁAc                ó\   — t          | ¦  «        }|€dS t          |¦  «        \  }}|rdS |pdS )a  Read a cron script with the bounded terminal-script scanner.

    Non-regular or oversized inputs fail closed by returning a lifecycle-shaped
    sentinel, while missing/unreadable/unresolvable paths remain empty so
    ordinary scheduler path validation can report them.
    Nr6   zhermes gateway restart)r¢   rÂ   )r    r™   rÔ   rÕ   s       r   Ú_read_script_for_scanningrâ   ‰  sG   € õ $ KÑ0Ô0€HØÐØˆrÝ1°(Ñ;Ô;Ñ€K�Øð (Ø'Ð'ØÐ˜"Ðr   ÚpromptÚscriptÚNonec                ó  — | pd}d}|r6t          |¦  «        }|duo
|j        dk    }t          |¦  «        }|r|› d|› �}|rt          |¦  «        }n$|rt	          |¦  «        nd}t          ||¬¦  «        }|rt          d¦  «        ‚dS )aE  Raise ``GatewayLifecycleBlocked`` if *prompt* or *script* contains a
    gateway-lifecycle command pattern.

    ``prompt`` is scanned directly.  ``script``, when supplied, is read from
    disk and concatenated for the scan.  Both are considered together so a
    job cannot slip through by splitting the command across the prompt and
    the script.

    Callers should let the exception propagate when they want the create to
    fail with a ``ValueError``-shaped error (the agent's ``cronjob`` tool
    surfaces this as a tool error; the CLI prints it in red and exits 1).
    r6   FNz.pyrk   rˆ   a  Blocked: cron job contains a gateway lifecycle command or persistent launchctl submit operation. This is blocked to prevent agent-driven SIGTERM-respawn loops under launchd/systemd supervision (#30719). Run `hermes gateway restart` from a shell outside the running gateway instead.)r¢   Úsuffixrâ   rw   r¥   rÛ   r	   )rã   rä   ÚcombinedÚpython_scriptÚresolved_scriptrÔ   rÕ   rÖ   s           r   Úcheck_gateway_lifecyclerë   ™  sÝ   € ð  ˆ|˜€HØ€MØð 4Ý.¨vÑ6Ô6ˆØ'¨tÐ3ÐW¸Ô8NÐRWÒ8WˆÝ/°Ñ7Ô7ˆØð 	4Ø"Ð3Ð3 kÐ3Ð3ˆHàð 
õ =¸XÑFÔFˆˆà:@ÐJÕ.¨vÑ6Ô6Ð6Àdˆ
ÝHØØð
ñ 
ô 
ˆð ð 
Ý%ð+ñ
ô 
ð 	
ð
ð 
r   )r   r   r   r   )r3   r   r   r4   )rH   rK   r   rL   )r3   r   r   r   )r   r   r   r   )rz   r   r   r{   )rz   r   r‚   rƒ   r   r{   )r3   r   r‚   rƒ   r   r‰   )r3   r   r   rœ   )r    r   r   rƒ   )r†   r   r   r¦   )r   rƒ   r   r¦   )r3   r   r‚   rƒ   rÇ   rÈ   rÉ   rÊ   rÆ   rË   r   r   )r3   r   r‚   rƒ   rÆ   rË   r   r   )r    r   r   r{   )r    r   r   r   r`   )rã   rƒ   rä   rƒ   r   rå   )6r   Ú
__future__r   Úloggingr¯   rO   r=   r´   Úpathlibr   Útypingr   r   r   Ú	getLoggerr
   rÙ   rA   r	   Úcompiler   r   r   Ú	frozensetr“   r”   r»   rÏ   rC   rm   rg   rl   rº   r¹   r   Ú_ReadRemoteScriptFnrJ   rR   r[   ru   rw   ry   r�   r‡   r›   rŸ   r¥   rÂ   rÅ   rÐ   rÛ   r¢   râ   rë   r   r   r   ú<module>rô      sv  ðð!ð !ðF #Ð "Ð "Ð "Ð "Ð "à €€€Ø 	€	€	€	Ø 	€	€	€	Ø €€€Ø €€€Ø Ð Ð Ð Ð Ð Ø /Ð /Ð /Ð /Ð /Ð /Ð /Ð /Ð /Ð /à	ˆÔ	˜8Ñ	$Ô	$€ðLð Lð Lð Lð L˜jñ Lô Lð Lð (˜RœZð4ñô Ð ðN &˜2œ:Ð&6Ñ7Ô7Ð ð?ð ?ð ?ð ?ð �YÐCÐCÐCÑDÔDÐ Ø&˜YÐ'?Ð'?Ð'?Ñ@Ô@Ð Ø*Ð Ø Ð Ø�˜7Ñ#Ô#€ð #˜ØRÐRÐRñô Ð ð :Ð ð "�r”zØIñô Ð ðÐ ð Ð ð
  ˜u h¨s¤mÐ3Ô4Ð ðð ð ð ð8ð ð ð ðð ð ð ð*H ð H ð H ð H ðVUð Uð Uð Uð4ð 4ð 4ð 4ðð ð ð ð2
ð 
ð 
ð 
ð  ð7#ð 7#ð 7#ð 7#ð 7#ð 7#ðt
ð 
ð 
ð 
ðð ð ð ð/9ð /9ð /9ð /9ðdð ð ð ð@ 9=ð<ð <ð <ð <ð <ð <ðD Ø8<ð	-<ð -<ð -<ð -<ð -<ð -<ðdð ð ð ð@ð ð ð ð$ !ð1
ð 1
ð 1
ð 1
ð 1
ð 1
ð 1
r   