+
    „WÝj{$  ã                   ó  a € R t0 t R t^ RIt^ RIt^ RIt^ RIHt ]! 0 Rm4      t	] ^ k ]! 0 Rm4      t
RtR R ltR R lt]! 4       3R R lltRR	 R
 lltR R ltRR R lltR R ltRtRtR R ltR R ltR R ltR# )a  Listener boundary primitives shared by the dashboard app and the gateway's session listener:
Host validation, the auth-gate decision and the native WebSocket ticket subprotocol.

Import-light on purpose (no FastAPI): ``gateway.run_api`` serves the native session WebSocket
without importing the dashboard app (about 690 routes, ~0.6 s), and must apply the same
DNS-rebinding Host check and gate decision. ``hermes_cli.web_server`` and
``hermes_cli.web_server_chat`` re-export these names, so ``web_server.<name>`` keeps working.
N)ÚOptionalc                ó:   € V ^8„  d   QhR\         \        ,          /# ©é   Úreturn)Ú	frozensetÚstr)Úformats   "ÚRC:\Users\johnn\AppData\Local\hermes\hermes-agent\hermes_cli\web_server_boundary.pyÚ__annotate__r      s   € ÷ Fñ F¤¬3¥ñ Fó    c                 ó  € ^ RI Hp  V ! 4       p V'       d*   \        P                  P	                  V4      P
                  MRpV'       d   \        VP                  4       04      # \        4       #   \         d    Rp L;i ; i)zÀReturn the exact hostname declared by ``dashboard.public_url``.

One source of truth for OAuth redirects, Host and WS Origin validation.
Malformed or unset values fail closed as an empty set.
)Úresolve_public_urlN)	Ú hermes_cli.dashboard_auth.prefixr   ÚurllibÚparseÚurlparseÚhostnameÚ
ValueErrorr   Úlower)r   Ú
public_urlr   s      r
   Ú_dashboard_public_hostsr      sj   € õ Dá#Ó%€JðßAK”6—<‘<×(Ñ(¨Ó4×=Ò=ÐQUˆ÷ -5Œ9�h—n‘nÓ&Ð'Ó(ÐE¼)»+ÐEøô ô ØŠðús   �A. —+A. Á.A>Á=A>c                ó0   € V ^8„  d   QhR\         R\         /# )r   Úhost_headerr   )r   )r	   s   "r
   r   r   -   s   € ÷ ñ ¤sð ¬sñ r   c                óÄ  a€ T ;'       g    RP                  4       oS'       dH   RS9   gA   \        ;QJ d    V3R lR 4       F  '       g   K   RM	  RM! V3R lR 4       4      '       d   R# SP                  R4      '       dl   SP                  R4      pVR8X  d   R# S^V pR	V9  d   R# SV^,           R
 pV'       d    \        P
                  ! RV4      '       g   R# VP                  4       # SP                  R	4      ^8”  d   R# R	S9   dE   SP                  R	^4      w  r$V'       d   VP                  4       '       g   R# VP                  4       # SP                  4       # )zÐReturn a normalized hostname from a valid HTTP Host authority.

Host headers are authorities, not full URLs. Reject ambiguous ports,
malformed IPv6 brackets, and URL syntax so validation always fails closed.
Ú z://c              3   ó,   <"  € T F	  qS9   x € K  	  R # 5i©N© )Ú.0ÚcÚvalues   & €r
   Ú	<genexpr>Ú(_host_header_hostname.<locals>.<genexpr>4   s   øé € Ð)QÑ>P¸¨u®*Ó>Pùs   ƒz"'<> 
	/?#@TFÚ[Ú]Ú:Nz:\d+éÿÿÿÿ)
ÚstripÚanyÚ
startswithÚfindÚreÚ	fullmatchr   ÚcountÚrsplitÚisdigit)r   Úcloser   ÚsuffixÚportr!   s   &    @r
   Ú_host_header_hostnamer4   -   s  ø€ ð ×Ð˜B×%Ñ%Ó'€Eß�E˜U”N§c£cÔ)QÑ>PÓ)Q§c§c¢cÔ)QÑ>PÓ)Q×&QÒ&QÙà×Ñ˜×ÒØ—
‘
˜3“ˆØ�BŒ;ÙØ˜˜5�>ˆà�hÔÙØ�u˜q•y�zÐ"ˆßœ"Ÿ,š, w°×7Ò7ÙØ�~‰~ÓÐð ‡{�{�3Ó˜!ÔÙØ
ˆe„|ØŸ™ c¨1Ó-‰ˆß˜tŸ|™|Ÿ~š~ÙØ�~‰~ÓÐØ�;‰;‹=Ðr   c                ó^   € V ^8„  d   QhR\         R\         R\        \         ,          R\        /# )r   r   Ú
bound_hostÚtrusted_public_hostsr   ©r   r   Úbool)r	   s   "r
   r   r   O   s4   € ÷ !ñ !Üð!äð!ô $¤C�.ð!ô 
ñ	!r   c                óš   € \        V 4      pV'       g   R# W29   g   VR9   d   R# VP                  4       pV\        9   d
   V\        9   # W48H  # )aP  True if the Host header targets the interface we bound to.

Accepts:
- Exact bound host (with or without port suffix)
- Loopback aliases when bound to loopback
- Exact operator-declared public hosts (with or without port suffix)
- Any host when bound to 0.0.0.0 (explicit opt-in to non-loopback,
  no protection possible at this layer)
FT>   ú::ú0.0.0.0)r4   r   Ú_LOOPBACK_HOST_VALUES)r   r6   r7   Ú	host_onlyÚbound_lcs   &&&  r
   Ú_is_accepted_hostr@   O   sS   € ô & kÓ2€IßÙð Ô(¨JÐ:KÔ,KÙØ×ÑÓ!€HØÔ(Ô(ØÔ1Ñ1Ð1ØÑ Ð r   c                ó<   € V ^8„  d   QhR\         R\        R\        /# )r   ÚhostÚallow_publicr   )r   r9   )r	   s   "r
   r   r   j   s!   € ÷ -ñ -œcð -´ð -Ä$ñ -r   c                ó   € V \         9  # )u$  True iff the auth gate must be active: any non-loopback bind.

RFC1918 / CGNAT / link-local are deliberately PUBLIC â€” a hostile LAN device
is the threat model. ``allow_public`` (legacy ``--insecure``) is accepted for
old launch scripts but IGNORED since the June 2026 hermes-0day campaign.
©r=   )rB   rC   s   &&r
   Úshould_require_authrF   j   s   € ð Ô,Ñ,Ð,r   c                óF   € V ^8„  d   QhR\         \        ,          R\        /# )r   r7   r   )r   r   r9   )r	   s   "r
   r   r   t   s"   € ÷ Mñ M´9¼Sµ>ð MÄdñ Mr   c                ój   € \         ;QJ d    R V  4       F  '       g   K   R# 	  R# ! R V  4       4      # )z[A non-loopback ``dashboard.public_url`` host engages the auth gate even on a loopback bind.c              3   ó2   "  € T F  q\         9  x € K  	  R # 5ir   rE   )r   Úhs   & r
   r"   Ú+public_hosts_engage_gate.<locals>.<genexpr>v   s   é € ÐLÑ7K°!Ô-Ö-Ó7Kùs   ‚TF)r)   )r7   s   &r
   Úpublic_hosts_engage_gaterL   t   s-   € ç‹3ÑLÑ7KÓL�3Œ3ÐLŠ3ÐLˆ3ÑLÑ7KÓLÓLÐLr   c                ót   € V ^8„  d   QhR\         R\        \         ,          R\        \         ,          R\        /# )r   rB   Ússh_session_tokenÚssh_owner_noncer   )r   r   r9   )r	   s   "r
   r   r   y   s8   € ÷ ñ Ü
ðä¤•}ðô œc•]ðô 
ñ	r   c                óð   € V \         9   ;'       dg    \        P                  P                  R4      R8H  ;'       d=    \	        \        P                  P                  R4      ;'       g    T;'       g    T4      # )u
  True for a Desktop-owned loopback backend (#96490).

A non-loopback ``dashboard.public_url`` would otherwise engage the
ticket-only gate for the private loopback backends Desktop spawns, whose
per-spawn session token the gate's WS path refuses â€” Desktop could not boot.
The public dashboard is a separate non-loopback process that stays gated, so
this never opens the public surface. Requires ALL of: loopback bind,
``HERMES_DESKTOP=1``, and an operator-minted credential (env token, SSH
session token, or owner nonce).
ÚHERMES_DESKTOPÚ1ÚHERMES_DASHBOARD_SESSION_TOKEN)r=   ÚosÚenvironÚgetr9   )rB   rN   rO   s   &&&r
   Ú_desktop_loopback_auth_exemptrW   y   sh   € ð  	Ô%Ñ%÷ 	kð 	kÜ�J‰J�N‰NÐ+Ó,°Ñ3÷	kð 	kä”—‘—‘Ð @ÓA×iÐiÐEV×iÐiÐZiÓjðr   c                óR   € V ^8„  d   QhR\         R\        \         ,          R\        /# )r   rB   r7   r   r8   )r	   s   "r
   r   r   �   s*   € ÷ Wñ W¤ð W¼IÄc½Nð WÌtñ Wr   c                óv   € \        V 4      '       d   \        V 4      # \        V 4      ;'       g    \        V4      # )z´The ``app.state.auth_required`` value ``web_server._configure_auth_gate(host, False, None,
None)`` resolves for a listener with no SSH credentials (the gateway's session listener).)rW   rF   rL   )rB   r7   s   &&r
   Úlistener_auth_requiredrZ   �   s4   € ô % T×*Ò*Ü" 4Ó(Ð(Ü˜tÓ$×VÐVÔ(@ÐAUÓ(VÐVr   zhermes-gateway-v1zhermes-gateway-ticket.c                óF   € V ^8„  d   QhR\         \        \        3,          /# r   )Útupler   )r	   s   "r
   r   r   ›   s   € ÷ 9ñ 9¬u´S¼#°X­ñ 9r   c                óô  € \        V P                  P                  RR4      ;'       g    R4      pVP                  R4       Uu. uF*  q"P	                  4       '       g   K  VP	                  4       NK,  	  ppV Uu. uF!  q"P                  \        4      '       g   K  VNK#  	  ppV'       g   R# \        V9  g   \        V4      ^8w  d   R# V^ ,          \        \        4      R pV'       d   VR3# R# u upi u upi )zEReturn ``(ticket, reason)`` from an unambiguous gateway protocol set.zsec-websocket-protocolr   Ú,NÚok)r   Únone)r   Úinvalid)	r   ÚheadersrV   Úsplitr(   r*   Ú"_GATEWAY_WS_TICKET_PROTOCOL_PREFIXÚ_GATEWAY_WS_PROTOCOLÚlen)ÚwsÚrawr!   Ú	protocolsÚticket_protocolsÚtickets   &     r
   Ú#_gateway_ws_ticket_from_subprotocolrl   ›   sÓ   € ä
ˆb�j‰j�n‰nÐ5°rÓ:×@Ð@¸bÓ
A€CØ,/¯I©I°c¬NÓL©N 5¿k¹k¿m”�—‘–©N€IÐLá$ó^Ù$�%×(8Ñ(8Ô9[×(\�ˆ™9ð ð ^çØÐÜ 9Ô,´Ð4DÓ0EÈÔ0JØÐØ˜aÕ ¤Ô%GÓ!HÐ!IÐJ€Fß#ˆF�Dˆ>Ð8¨Ð8ùò Mùò^s   ÁC0ÁC0Á9C5ÂC5c                ó:   € V ^8„  d   QhR\         \        ,          /# r   ©r   r   )r	   s   "r
   r   r   ©   s   € ÷ Mñ M¤8¬C¥=ñ Mr   c                ó|  € \        VRR4      '       d   R# \        VRR4      ;'       g    RP                  4       P                  4       pV'       d   V\        9  d   R# V P                  '       d   V P                  P
                  MRpV'       g   RT;'       g    R 2# V\        9   d   R# RT R	T;'       g    R 2# )
uá  Rejection reason token for the WebSocket peer IP under listener *state*
(``auth_required`` / ``bound_host``), or None when allowed.

Loopback bind: only loopback peers (the legacy ``?token=`` is the only auth,
LAN hosts must not get to guess it); an empty peer fails closed.  Explicit
non-loopback bind (``--insecure``) or gated mode: any peer â€” DNS-rebinding is
blocked by :func:`ws_host_origin_reason`, and in gated mode
``ws.client.host`` is the X-Forwarded-For value anyway.
Úauth_requiredFNr6   r   zmissing_or_empty_peer bound=Ú?zpeer_not_loopback peer=ú bound=)Úgetattrr(   r   Ú_LOOPBACK_HOSTSÚclientrB   )rg   Ústater6   Úclient_hosts   &&  r
   Úws_client_reasonrx   ©   s¢   € ô ˆu�o u×-Ò-ÙÜ˜% ¨rÓ2×8Ð8°b×?Ñ?ÓA×GÑGÓI€Jß�j¬Ô7ÙØ$&§I§I I�"—)‘)—.’.°2€KßØ-¨j×.?Ð.?¸CÐ-@ÐAÐAØ”oÔ%ÙØ$ [ M°¸×9JÐ9JÀsÐ8KÐLÐLr   c                ó:   € V ^8„  d   QhR\         \        ,          /# r   rn   )r	   s   "r
   r   r   À   s   € ÷ ñ ¬´­ñ r   c                ó2  € \        VRR4      pV'       g   R# \        VR\        4       4      pV P                  P                  RR4      p\	        WBV4      '       g   RT;'       g    R RV 2# V P                  P                  R	R4      pV'       g   R#  \
        P                  P                  V4      pVe   VP                  R9  d   R# Ve/   VP                  '       d   \	        VP                  W#4      '       g	   R
V RV 2# R#   \         d    Rp Lbi ; i)u‚  ``host_mismatch â€¦`` / ``origin_mismatch â€¦`` under listener *state*, or None when allowed.

HTTP middleware does not run for WebSocket routes, so the DNS-rebinding
Host check is repeated here; an Origin header, when present, must target the
bound host.  Non-web origins (packaged Electron: file://, null, app://) are
trusted â€” the credential check is the real auth boundary there.
r6   Nr7   rB   r   zhost_mismatch host=rq   rr   Úoriginzorigin_mismatch origin=>   ÚhttpÚhttps)rs   r   rb   rV   r@   r   r   r   r   ÚschemeÚnetloc)rg   rv   r6   r7   r   r{   Úparseds   &&     r
   Úws_host_origin_reasonr�   À   sý   € ô ˜ ¨dÓ3€JßÙÜ" 5Ð*@Ä)Ã+ÓNÐØ—*‘*—.‘. ¨Ó,€KÜ˜[Ð6J×KÒKØ$ [×%7Ð%7°CÐ$8¸À
¸|ÐLÐLØ�Z‰Z�^‰^˜H bÓ)€FßÙðÜ—‘×&Ñ& vÓ.ˆð Ò˜fŸm™mÐ3DÔDÙØ‚~˜VŸ]Ÿ]˜]Ô2CÀFÇMÁMÐS]×2tÒ2tØ(¨¨°¸
°|ÐDÐDÙøô ô ØŠðús   ÂD ÄDÄDc                ó@   € V ^8„  d   Qh/ ^ \         9   d
   \        ;R&   # )r   r=   )Ú__conditional_annotations__r   )r	   s   "r
   r   r      s   € ÷ ñ ÷" PÒ O”yÑ Oò#r   >   Ú	localhostú::1ú	127.0.0.1>   r„   Ú
testclientr…   r†   i   )F)NN)rƒ   Ú__doc__rT   r,   Úurllib.parser   Útypingr   r   r=   rt   Ú _DESKTOP_ATTACHMENT_WS_MAX_BYTESr   r4   r@   rF   rL   rW   rZ   re   rd   rl   rx   r�   r   )rƒ   s   @r
   Ú<module>rŒ      s‹   øðöó 
Û 	Û Ý ñ
 $-Ò-NÓ#OÐ Ó Oñ ÒKÓL€ð $5Ð  õFõ ñJ ,5«;÷!÷6-õM÷
õ,Wð +Ð Ø%=Ð "õ9õM÷.r   