
    epjG                       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mZ ddl	m
Z
mZmZ  G d de          Z ej        d          Z ej        d	          Zd;dZ eh d          Z eh d          ZdZdZ ed          Ze
egee         f         Zd<dZd=dZd>dZd?d Zdd!d@d#ZdAd%Z dBd'Z!dCd*Z"dd+dDd2Z#ddd3dEd4Z$dFd5Z%dGd6Z&	 dHdId:Z'dS )Ju~  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/thesage/.hermes/hermes-agent/cron/lifecycle_guard.pyr	   r	   .   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   _   sA     u)--c488J*11*==>>>r   >   shkshzshbashdash>   +O+o-O-oi      ;&|()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)r(   r   linelexertokenssegmenttokens          r   _iter_command_segmentsr>   s   s     ,,J%%''7J<  
	K")  E
 &*E""E%[[FF 	 	 	H	   	" 	"E U~55 !!MMM GNN5!!!! 	MMM- s   4A((
A54A5r<   	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)r<   indexr=   s      r   _command_token_indexrF      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)r>   rF   r   namelower)r(   r<   rE   	argumentss       r   !contains_launchctl_submit_commandrO      s     *'22  $W--=$33	,I Yq\//115LLLtt5r   	candidatecwdOptional[str]r   c                    t          |                                           }|                                s%t          |pt          j                              |z  }|S N)r   
expanduseris_absoluterQ   )rP   rQ   paths      r   _resolve_terminal_script_pathrX      sO    	??%%''D .C%48::&&-Kr   rQ   Iterator[Path]c             #    K   t          |           D ]M}t          |          }|||         }t          |          j        }|dv r2t	          |          |dz   k    rt          ||dz            |          V  g|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  OdS )z9Yield scripts executed directly or through a POSIX shell.N>   source.rI   r   z--   -c	--command   -/)z.shz.bashz.zsh)r>   rF   r   rL   lenrX   _SHELL_EXECUTABLES_SHELL_OPTIONS_WITH_VALUES
startswithstripendswith)	r(   rQ   r<   rE   
executableexecutable_namerN   	arg_indexarguments	            r   _iter_referenced_shell_scriptsrn      s      *'22 +E +E$W--=U^
z**/o--7||eai''3GEAI4FLLLLL000	,IIc)nn,,$Y/t##NI222999NI&&s++ NI3y>>))i	.B K / / 4Ii4H#NNNNN C   	Ej  J$7$78P$Q$Q 3JDDDDDW+E +E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.NrI   r^   )r>   rF   r   rL   re   rB   )r(   r<   rE   rN   rl   rm   s         r   _iter_shell_command_payloadsrr      s      )'22  $W--=D005=OOOEAIKK(	#,Yss^#<#< 	 	Ix...	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_pathrV   r   parent	Exception)rs   rW   s     r   _resolve_script_directoryrx      sb    #K00 	$t{###	$   4s   6: 
AArW   tuple[Optional[str], bool]c                x   t           j        t          t           dd          z  }	 t          j        | |          }n# t          $ r Y dS w xY w	 t          j        |          }t          j        |j                  s	 t          j	        |           dS t          j
        |t          dz             }n%# 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)NTrI       zutf-8r/   )errorsF)osO_RDONLYgetattropenOSErrorfstatstatS_ISREGst_modecloseread_MAX_REFERENCED_SCRIPT_BYTESrd   decode)rW   flags
descriptormetadatadatas        r   _read_referenced_scriptr      sY   K'"lA666EWT5))

   {{8J''|H,-- 	 		 wz#?!#CDD   
 	 	 ${
4yy///z;;wy;11588s?   ; 
A	A	-B/ B/ .C* /
C9C* CC* *D )read_remote_scriptdepthintvisited	set[Path]r   Optional[_ReadRemoteScriptFn]c               V   t          |           s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          |                    }|st          t          |                    p|}
|rt	          ||
|dz   ||          r dS dS )NTrI   rQ   r   r   r   rY   F)strict)r   rO   _MAX_REFERENCED_SCRIPT_DEPTHrr   _contains_unsafe_gateway_actionrn   resolver   r5   addr   r   rx   )r(   rQ   r   r   r   payloadrs   resolvedscript_textunsafe
script_dirs              r   r   r      s    *'22 6W7 7  t,,,t/88  *!)1
 
 
 	 44	 6g3GGG  	#"**%*88HH$ 	# 	# 	# #HHH		#
 wH5kBBV 	44#5#A,,S-=-=>>K 	 /s8}}==D
 	:!)1
 
 
 	 445s   .BBB)rQ   r   c               B    t          | |dt                      |          S )zCDetect lifecycle/submit commands, including bounded nested scripts.r   r   )r   r6   )r(   rQ   r   s      r   7contains_gateway_lifecycle_command_or_referenced_scriptr   Z  s.     +-   r   c                    ddl m} t          |                                           }|                                r|S  |            dz  |z  S )uE  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.
    r   )get_hermes_homescripts)hermes_constantsr   r   rU   rV   )rs   r   raws      r   ru   ru   l  s^     100000
{


&
&
(
(C
 
?y(3..r   c                P    t          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 paths remain empty so ordinary scheduler
    path validation can report them.
    zhermes gateway restartr+   )r   ru   )rs   r   r   s      r   _read_script_for_scanningr     s8     22F{2S2STTK (''"r   promptscriptNonec                   | pd}d}|r0t          |          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).
    r+   Fz.py
NrY   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.)ru   suffixr   r   rx   r   r	   )r   r   combinedpython_scriptr   r   r   s          r   check_gateway_lifecycler     s      |HM 4,V44;uD/77 	4"33k33H 
 4H==:@J.v666d
H
 
 
  
%+
 
 	

 
r   )r   r   r   r   )r(   r   r   r)   )r<   r?   r   r@   )r(   r   r   r   )rP   r   rQ   rR   r   r   )r(   r   rQ   rR   r   rZ   )r(   r   r   ro   )rs   r   r   rR   )rW   r   r   ry   )r(   r   rQ   rR   r   r   r   r   r   r   r   r   )r(   r   rQ   rR   r   r   r   r   )rs   r   r   r   )rs   r   r   r   rT   )r   rR   r   rR   r   r   )(r   
__future__r   r~   rC   r1   r   pathlibr   typingr   r   r   r5   r	   compiler   r   r   	frozensetre   rf   r   r   r7   r   _ReadRemoteScriptFnr>   rF   rO   rX   rn   rr   rx   r   r   r   ru   r   r   r   r   r   <module>r      s  ! !F # " " " " " 				 				         / / / / / / / / / /L L L L Lj L L L (RZ4  N &2:&677 ? ? ? ? YCCCDD &Y'?'?'?@@ *   7##
 uhsm34    8      *    1E 1E 1E 1E 1E 1Eh
 
 
 
   9 9 9 9L 9=7 7 7 7 7 7z 8<	     $/ / / /&
 
 
 
 !0
 0
 0
 0
 0
 0
 0
r   