Ë
    ãß²jT1  ã                   ót  — d 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  ej                  d«      Z ed«      Zded	efd
„Zded	edz  fd„Zd*de	ded	efd„Zdeded	efd„Z ej0                  dej2                  ej4                  z  «      Z ej0                  d«      Z ej0                  d«      Zded	efd„Z ed„ dD «       «      Z ejB                  dfd„ dfd„ dffZ"ded	efd„Z#ded	edz  fd„Z$d e	d!ed"ed#ed	e
e   f
d$„Z%ded e	d%ed&ede
e   d'ed	e
e   fd(„Z&ded&ede
e   d'ed	e
e   f
d)„Z'y)+að  Pre-execution guards for the terminal tool.

Pure functions that decide whether a command may run at all: workdir
validation, the foreground long-lived/background-operator guidance, the
supervised-gateway lifecycle block, and the Windows self-repo git guard.
Each ``*_block`` helper returns a finished JSON error string, or None when
the command may proceed. Split out of tools/terminal_tool.py; the origin
module re-imports every public helper so ``tools.terminal_tool.<name>``
keeps resolving.
é    N)ÚPath)ÚAnyÚOptional)Ústrip_inert_heredoc_bodiesztools.terminal_toolz/\:_-.~ +@=,ÚchÚreturnc                 ót   — | rt        | «      dk  st        | «      dk(  ry| j                  «       xs | t        v S )Né    é   F)ÚordÚisalnumÚ_WORKDIR_SAFE_ASCII_CHARS)r   s    ú?/home/ubuntu/.hermes/hermes-agent/tools/terminal_tool_guards.pyÚ_is_safe_workdir_charr       s3   € Ù”�R“˜2’¤ R£¨C¢ØØ�:‰:‹<Ò:˜2Ô!:Ð:Ð:ó    Úworkdirc                 óR   — | xs dD ]  }t        |«      rŒdt        |«      › d�c S  y)z‘Error message if *workdir* has a disallowed character, else None.
    Allowlist rather than deny-list so novel metacharacters can't slip through.Ú z/Blocked: workdir contains disallowed character z<. Use a simple filesystem path without shell metacharacters.N)r   Úrepr)r   r   s     r   Ú_validate_workdirr   &   sA   € ð Šm˜ò ˆÜ$ RÕ(àAÄ$ÀrÃ(Àð LMð Mòðð r   ÚcommandÚlimitc                 ó    — | €yt        | t        «      r| d| S 	 t        | «      d| S # t        $ r dt	        | «      j
                  › d�cY S w xY w)z>Return a log-safe preview for possibly-invalid command values.Nz<None>Ú<Ú>)Ú
isinstanceÚstrr   Ú	ExceptionÚtypeÚ__name__)r   r   s     r   Ú_safe_command_previewr!   2   s_   € à€ØÜ�'œ3ÔØ�v˜ˆÐð-Ü�G‹}˜V˜eÐ$Ð$øÜò -Ø”4˜“=×)Ñ)Ð*¨!Ð,Ò,ð-ús   š( ¨"AÁAÚerrorÚstatusc                 ó:   — t        j                  dd| |dœd¬«      S )z<The guard result envelope: exit_code 1 + *error* + *status*.r   é   )ÚoutputÚ	exit_coder"   r#   F)Úensure_ascii)ÚjsonÚdumps)r"   r#   s     r   Ú_blocked_jsonr+   >   s   € ä�:‰: °!¸eÈvÑVÐejÔkÐkr   z=(?:^|[;&|]\s*|&&\s*|\|\|\s*|\$\(\s*)(?:nohup|disown|setsid)\bz\s&\sz\s&\s*(?:#.*)?$c                 ó¢   — t        | «      }t        j                  dd|«      }t        j                  dd|«      }t        j                  dd|«      S )u¼  Blank quoted / backtick content and provably-inert heredoc bodies so
    regex checks can't match keywords (nohup, setsid, '&') inside strings.

    Heredocs are masked FIRST: their delimiter may itself be quoted
    (``<<'EOF'``). ``strip_inert_heredoc_bodies`` is conservative â€” only a
    quoted, terminated delimiter on a simple opener fed to a known non-shell
    consumer is masked, so a real background operator can't hide in one.
    z'[^']*'z''z"(?:[^"\\]|\\.)*"z""z`[^`]*`z``)r   ÚreÚsub)r   Úresults     r   Ú_strip_quotesr0   J   sF   € ô (¨Ó0€FÜ�V‰V�J  fÓ-€FÜ�V‰VÐ(¨$°Ó7€FÜ�6‰6�*˜d FÓ+Ð+r   c              #   ód   K  — | ](  }t        j                  |t         j                  «      –— Œ* y ­w©N)r-   ÚcompileÚ
IGNORECASE)Ú.0Úps     r   ú	<genexpr>r7   Y   s#   è ø€ ò 	(È¬¯
©
°1´b·m±m×(Dñ 	(ùs   ‚.0)z@\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start|serve|watch)\bz\bdocker\s+compose\s+up\bz\bnext\s+dev\bz\bvite(?:\s|$)z\bnodemon\bz\buvicorn\bz\bgunicorn\bz$\bpython(?:3)?\s+-m\s+http\.server\ba	  Foreground command uses shell-level background wrappers (nohup/disown/setsid). Re-send WITHOUT the wrapper as terminal(command="<cmd>", background=true, notify_on_complete=true) so Hermes tracks the process, then run readiness checks and tests in separate commands.c                 óZ   — t         j                  | «      xs t        j                  | «      S r2   )Ú_INLINE_BACKGROUND_AMP_REÚsearchÚ_TRAILING_BACKGROUND_AMP_RE©Úss    r   ú<lambda>r>   n   s$   € Ô+×2Ñ2°1Ó5Ò^Ô9T×9[Ñ9[Ð\]Ó9^€ r   uã   Foreground command uses '&' backgrounding. Re-send WITHOUT the '&' as terminal(command="<cmd>", background=true) â€” add notify_on_complete=true for bounded jobs â€” then run health checks and tests in follow-up terminal calls.c                 ó4   ‡ — t        ˆ fd„t        D «       «      S )Nc              3   ó@   •K  — | ]  }|j                  ‰«      –— Œ y ­wr2   )r:   )r5   r6   r=   s     €r   r7   z<lambda>.<locals>.<genexpr>t   s   øè ø€ ÒK a�a—h‘h˜q—kÑKùs   ƒ)ÚanyÚ_LONG_LIVED_FOREGROUND_PATTERNSr<   s   `r   r>   r>   t   s   ø€ ”#ÓKÔ+JÔKÓK€ r   zÁThis foreground command appears to start a long-lived server/watch process. Run it with background=true, verify readiness (health endpoint/log signal), then execute tests in a separate command.c                 ó¼   — dj                  | j                  «       j                  «       «      }d|v xs* |j                  d«      xs d|v xs |j                  d«      S )zGReturn True for informational invocations that should never be blocked.Ú z --helpz -hz
 --versionz -v)ÚjoinÚlowerÚsplitÚendswith)r   Ú
normalizeds     r   Ú#_looks_like_help_or_version_commandrJ   |   sa   € à—‘˜'Ÿ-™-›/×/Ñ/Ó1Ó2€Jà�ZÐò 	&Ø×Ñ˜uÓ%ò	&à˜:Ð%ò	&ð ×Ñ˜uÓ%ð	r   c                 ód   ‡— t        | «      ryt        | «      Št        ˆfd„t        D «       d«      S )z�Guidance text when a foreground command looks long-lived or uses shell
    backgrounding (it should be a managed background session), else None.Nc              3   ó:   •K  — | ]  \  }} |‰«      sŒ|–— Œ y ­wr2   © )r5   ÚhitÚmsgÚunquoteds      €r   r7   z2_foreground_background_guidance.<locals>.<genexpr>�   s   øè ø€ ÒK™˜˜c¹SÀ½]”ÑKùs   ƒ”)rJ   r0   ÚnextÚ_FOREGROUND_GUIDANCE)r   rP   s    @r   Ú_foreground_background_guidancerS   ‡   s.   ø€ ô +¨7Ô3ØÜ˜WÓ%€HÜÓKÔ%9ÔKÈTÓRÐRr   ÚenvÚ	guard_cwdÚscript_pathÚ	max_bytesc                 ó€  — | €y	 t        |«      j                  «       }|j                  «       st        |«      |z  }|j                  «       ru|j	                  «       }t	        j
                  |j                  «      rF|j                  |k  r7|j                  «       }t        |«      |k  rd|v rdS |j                  dd¬«      S 	 | j                  d|dz   › dt        j                  |«      › �«      }|j                  d	d
«      dk(  r|j                  dd«      }|rd|v rdS |S 	 y# t        $ r Y Œlw xY w# t        $ r Y yw xY w)a  Best-effort script read: host filesystem first, then a bounded
    ``env.execute('head -c ... < path')`` for remote backends. Binary content
    (NUL byte) is not a script: feeding it to the guard tokenizes machine code
    into bogus paths and crashes the scanner, so it yields None.Nó    zutf-8Úreplace)Úerrorszhead -c r%   z < Ú
returncodeéÿÿÿÿr   r&   r   ú )r   Ú
expanduserÚis_absoluteÚis_fileÚstatÚS_ISREGÚst_modeÚst_sizeÚ
read_bytesÚlenÚdecoder   ÚexecuteÚshlexÚquoteÚget)	rT   rU   rV   rW   Ú
local_pathÚmetadataÚdatar/   r&   s	            r   Ú_read_script_for_guardrp   �   sG  € ð
 €{ØðÜ˜+Ó&×1Ñ1Ó3ˆ
Ø×%Ñ%Ô'Ü˜i›¨:Ñ5ˆJØ×ÑÔØ!—‘Ó(ˆHÜ�|‰|˜H×,Ñ,Ô-°(×2BÑ2BÀiÒ2OØ!×,Ñ,Ó.�Ü�t“9 	Ò)Ø#*¨d¡?˜4Ð^¸¿¹ÀGÐT]¸Ó8^Ð^ðØ—‘˜x¨	°A© °c¼%¿+¹+ÀkÓ:RÐ9SÐTÓUˆØ�:‰:�l BÓ'¨1Ò,Ø—Z‘Z ¨"Ó-ˆFÙ! f°Ñ&6�4ÐB¸FÐBð -ð
 øô ò Ùðûô ò ØØðús1   …B(D" Â.D" ÃAD1 ÄD1 Ä"	D.Ä-D.Ä1	D=Ä<D=Úenv_typeÚcwdÚsession_keyc                 ó~  ‡‡‡— ddl m} ddlm}m}  |«       syddlmŠm}	m}
m	}m
}m}  || «      r || «      rt        dd«      S  ||«      }|€t        ‰dd«      xs |} |||||¬	«      Š || ‰ˆˆˆfd
„¬«      \  }}|r|rt        d|› d�d«      S |r( || «      r |
| «      rt        |	d«      S t        dd«      S y)u$  Refuse gateway lifecycle commands issued from inside the supervised gateway.

    ``systemctl``/``launchctl``/``hermes gateway restart|stop|uninstall``
    targeting hermes-gateway would SIGTERM the gateway â€” and this very
    subprocess â€” before completing, so the service may never come back.
    Applies unconditionally (``force=True`` cannot bypass it). Gated on the
    SUPERVISED-gateway probe, not the raw ``_HERMES_GATEWAY`` marker: that
    marker leaks into every process that merely imports gateway.run (hermes
    serve, CLI, web server), which must still be able to restart the gateway;
    an unsupervised foreground ``hermes gateway run`` has no KeepAlive to turn
    a self-restart into a respawn loop, so it passes too.
    Returns the JSON error string when blocked, else None.
    r   )Ú_is_supervised_gateway_process)Ú_resolve_command_cwdÚget_session_cwdN)Ú_MAX_REFERENCED_SCRIPT_BYTESÚHOST_INTERPRETER_KILL_REJECTIONÚcontains_host_interpreter_killÚ!contains_launchctl_submit_commandÚ!lifecycle_scan_root_within_budgetÚscan_gateway_lifecyclea‘  Blocked: launchctl submit/bootstrap is restricted inside a supervised gateway regardless of the job label, to prevent indirect gateway restart loops. This guard does not inspect the job's KeepAlive settings or determine whether it is independent of Hermes. Perform authorized LaunchAgent maintenance from a separate shell outside the gateway, not by switching launchctl verbs to bypass this rejection.r"   rr   )r   Údefault_cwdrs   rq   c                 ó    •— t        ‰‰| ‰«      S r2   )rp   )r6   rx   rT   rU   s    €€€r   r>   z)gateway_lifecycle_block.<locals>.<lambda>ë   s   ø€ Ô%;¸CÀÈAÐOkÓ%l€ r   )rr   Úread_remote_scriptzOBlocked: the lifecycle guard could not scan this command or referenced script: zÍ. Nothing in the command is known to contain a gateway lifecycle command, but a script the command executes must be scannable (a regular text file under 1 MiB) before it can run inside the gateway process.a+  Blocked: command or referenced script cannot restart, stop, or uninstall the gateway from inside the gateway process. The gateway would kill this command before it could complete (SIGTERM propagates to child processes). Run `hermes gateway restart` from a separate shell outside the running gateway.)Útools.process_registryru   Útools.terminal_toolrv   rw   Úcron.lifecycle_guardrx   ry   rz   r{   r|   r}   r+   Úgetattr)r   rT   rq   rr   r   rs   ru   rv   rw   ry   rz   r{   r|   r}   Úguard_cwd_baseÚunsafeÚrefusalrx   rU   s    `               @@r   Úgateway_lifecycle_blockrˆ   ²   s  ú€ õ, FßIá)Ô+Ø÷÷ ñ )¨Ô1Ñ6WÐX_Ô6`ÜðIð ó
ð 	
ñ % [Ó1€NØÐÜ   e¨TÓ2Ò9°cˆÙ$Ø ^ÀÐW_ô€Iñ -ØØÝlô�O€FˆGñ
 ‘'ô Ø]Ð^eÐ]fð g<ð <ð ó
ð 	
ñ ñ -¨WÔ5Ñ:XÐY`Ô:aÜ Ð!@À'ÓJÐJÜð:ð
 ó
ð 	
ð r   c                 ó¸   — ddl m}m} ddlm}  |«       sy ||||¬«      } || |«      \  }}	|syt
        j                  dt        | «      «       t        |	d«      S )uŸ  Windows-only guard against git-mutating the checkout backing this interpreter.

    NTFS locks loaded module files, so rewriting the live checkout can corrupt
    the running process; POSIX keeps old inodes alive for open handles, so the
    guard is off there (``guard_active``). Local backend only â€” remote
    backends cannot reach that checkout. Returns the JSON error string when
    blocked, else None.
    r   )Údetect_self_repo_git_mutationÚguard_active)rv   N)r   r~   rs   z,Blocked self-repo git mutation (command: %s)Úblocked)	Útools.self_repo_guardrŠ   r‹   r‚   rv   ÚloggerÚwarningr!   r+   )
r   rr   r   rs   rŠ   r‹   rv   rU   rN   rO   s
             r   Úself_repo_blockr�     s[   € ÷ RÝ8áŒ>ØÙ$¨WÀ#ÐS^Ô_€IÙ,¨W°iÓ@�H€CˆÙØÜ
‡N�NÐAÔCXÐY`ÓCaÔbÜ˜˜iÓ(Ð(r   )éÈ   )(Ú__doc__r)   Úloggingr-   rj   rb   Úpathlibr   Útypingr   r   Útools.shell_heredocr   Ú	getLoggerrŽ   Ú	frozensetr   r   Úboolr   r   Úintr!   r+   r3   r4   Ú	MULTILINEÚ_SHELL_LEVEL_BACKGROUND_REr9   r;   r0   ÚtuplerB   r:   rR   rJ   rS   rp   rˆ   r�   rM   r   r   ú<module>rž      sV  ðñ	ó Û Û 	Û Û Ý ß  å :à	ˆ×	Ñ	Ð0Ó	1€ñ & oÓ6Ð ð;˜cð ; dó ;ð	˜sð 	 s¨T¡zó 	ñ	- 3ð 	-¨sð 	-¸Só 	-ðl˜ð l cð l¨có lð
 (˜RŸZ™ZØDÀbÇmÁmÐVX×VbÑVbÑFbóÐ ð '˜BŸJ™J xÓ0Ð Ø(˜bŸj™jÐ);Ó<Ð ð,˜3ð , 3ó ,ñ #(ñ 	(ð 	Oô 	(ó 	#Ð ð 	#×)Ñ)ð	1ðñ 	_ð	]ðñ 	Lð	4ððÐ ð.°ð ¸ó ðS¨Sð S°S¸4±Zó Sð ð °ð À#ð ÐRUð ÐZbÐcfÑZgó ðDSàðSð 
ðSð ð	Sð
 
ðSð �c‰]ðSð ðSð ˆc�]óSðl)àð)ð 
ð)ð �c‰]ð	)ð
 ð)ð ˆc�]ô)r   