
    (Gj                        d Z ddlmZ ddlZddlmZ ddlmZ  G d de          Z	 ej
        d          ZddZddZddZ	 dddZdS )u  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`` already
blocks these commands at *execution* time when ``_HERMES_GATEWAY=1``, and
``hermes gateway stop|restart`` refuse to self-target from inside the
gateway.  Blocking 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)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/rurouni/.hermes/hermes-agent/cron/lifecycle_guard.pyr   r   )   s        KKKKr   r   a'  (?i)(?:hermes\s+gateway\s+(?:restart|stop))|(?:launchctl\s+(?:kickstart|unload|load|stop|restart)\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)textstrreturnboolc                X    | sdS t          t                              |                     S )zCReturn True if *text* contains a gateway lifecycle command pattern.F)r   _GATEWAY_LIFECYCLE_PATTERNsearch)r   s    r   "contains_gateway_lifecycle_commandr   E   s,     u*11$77888r   script_pathr   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   
expanduseris_absolute)r   r   raws      r   _resolve_script_pathr   L   s^     100000
{


&
&
(
(C
 
?y(3..r   c                    	 t          |                                                               dd          S # t          $ r Y dS w xY w)u  Read a script file for lifecycle-pattern scanning.

    Decodes with ``errors="replace"`` so binary or non-UTF-8 content does not
    silently bypass the check — a plain text-mode read raises
    ``UnicodeDecodeError`` on such files, and swallowing that error would let
    an attacker hide the command in binary noise.  Returns an empty string
    only when the file cannot be read at all.
    zutf-8replace)errors )r   
read_bytesdecodeOSError)r   s    r   _read_script_for_scanningr'   _   sb    #K00;;==DDI E 
 
 	
    rrs   58 
AApromptOptional[str]scriptNonec                ~    | pd}|rt          |          }|r| 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#   
a  Blocked: cron job contains a gateway lifecycle command (restart/stop/kill). 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.N)r'   r   r   )r(   r*   combinedscript_texts       r   check_gateway_lifecycler0   p   sn      |H 4/77 	4"33k33H)(33 
%+
 
 	

 
r   )r   r   r   r   )r   r   r   r   )r   r   r   r   )N)r(   r)   r*   r)   r   r+   )r   
__future__r   repathlibr   typingr   
ValueErrorr   compiler   r   r   r'   r0   r   r   r   <module>r7      s   B # " " " " " 				            L L L L Lj L L L (RZ4  *9 9 9 9/ / / /&   & !
 
 
 
 
 
 
r   