Ë
    ÿ`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y)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y)ÚGatewayLifecycleBlockedzARaised when a cron job spec contains a gateway-lifecycle command.N)Ú__name__Ú
__module__Ú__qualname__Ú__doc__© ó    úG/root/.hermes/venv/lib/python3.12/site-packages/cron/lifecycle_guard.pyr   r   )   s   „ Ú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)c                óD   — | syt        t        j                  | «      «      S )zCReturn True if *text* contains a gateway lifecycle command pattern.F)ÚboolÚ_GATEWAY_LIFECYCLE_PATTERNÚsearch)Útexts    r   Ú"contains_gateway_lifecycle_commandr   E   s   € áØÜÔ*×1Ñ1°$Ó7Ó8Ð8r   c                ó~   — ddl m} t        | «      j                  «       }|j	                  «       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)Úscript_pathr   Úraws      r   Ú_resolve_script_pathr   L   s<   € õ 1ä
ˆ{Ó
×
&Ñ
&Ó
(€CØ
‡ÔØˆ
ÙÓ˜yÑ(¨3Ñ.Ð.r   c                óv   — 	 t        | «      j                  «       j                  dd¬«      S # t        $ r Y y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%   _   sG   € ðÜ# KÓ0×;Ñ;Ó=×DÑDØ˜Ið Eó 
ð 	
øô ò Ùðús   ‚), ¬	8·8c                óh   — | xs d}|rt        |«      }|r|› d|› }t        |«      rt        d«      ‚y)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   )ÚpromptÚscriptÚcombinedÚscript_texts       r   Úcheck_gateway_lifecycler,   p   sL   € ð  Š|˜€HÙÜ/°Ó7ˆÙØ"˜ 2 k ]Ð3ˆHä)¨(Ô3Ü%ð+ó
ð 	
ð 4r   )r   ÚstrÚreturnr   )r   r-   r.   r   )r   r-   r.   r-   )N)r(   úOptional[str]r)   r/   r.   ÚNone)r   Ú
__future__r   ÚreÚpathlibr   Útypingr   Ú
ValueErrorr   Úcompiler   r   r   r%   r,   r   r   r   ú<module>r7      sr   ðñõB #ã 	Ý Ý ôL˜jô Lð (˜RŸZ™Zð4óÐ ó*9ó/ó&ð& !ð
Øð
àð
ð 
ô
r   