
    7Rj;                     x   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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mZmZ ddlmZ 	 ddlmZ dZdd	lmZmZ dd
lmZmZmZm Z   ejB                  e"      Z# e$       Z%h dZ&dZ'dZ(dZ)dZ*dZ+ e,h d      Z-de.de/fdZ0de/fdZ1 G d de      Z2y# e$ r dZdZY jw xY w)ue  Generic webhook platform adapter.

Runs an aiohttp HTTP server that receives webhook POSTs from external
services (GitHub, GitLab, JIRA, Stripe, etc.), validates HMAC signatures,
transforms payloads into agent prompts, and routes responses back to the
source or to another configured platform.

Configuration lives in config.yaml under platforms.webhook.extra.routes.
Each route defines:
  - events: which event types to accept (header-based filtering)
  - secret: HMAC secret for signature validation (REQUIRED)
  - prompt: template string formatted with the webhook payload
  - skills: optional list of skills to load for the agent
  - deliver: where to send the response (github_comment, telegram, etc.)
  - deliver_extra: additional delivery config (repo, pr_number, chat_id)
  - deliver_only: if true, skip the agent — the rendered prompt IS the
    message that gets delivered.  Use for external push notifications
    (Supabase, monitoring alerts, inter-agent pings) where zero LLM cost
    and sub-second delivery matter more than agent reasoning.

Security:
  - HMAC secret is required per route (validated at startup)
  - Rate limiting per route (fixed-window, configurable)
  - Idempotency cache prevents duplicate agent runs on webhook retries
  - Body size limits checked before reading payload
  - Set secret to "INSECURE_NO_AUTH" to skip validation (testing only)
    N)deque)AnyDequeDictListOptional)urlsplit)webTF)PlatformPlatformConfig)BasePlatformAdapterMessageEventMessageType
SendResult>   smsemailqqbotslackwecomfeishumatrixsignalweixindiscordyuanbaodingtalktelegramwhatsapp
mattermostbluebubbleshomeassistantwecom_callbackz0.0.0.0i!  INSECURE_NO_AUTHzwebhook_subscriptions.json      N@>   ::1ip6-loopbackip6-localhost	localhost	127.0.0.1hostreturnc                 P    | sy| j                         j                         t        v S )af  True when `host` binds only to the local machine.

    Covers IPv4 loopback, the standard `localhost` alias, IPv6 loopback in
    both bracketed and bare form, and the common Debian-style aliases. Any
    falsy value (empty string, None) is conservatively treated as non-loopback
    because an unset host usually means the platform-default public bind.
    F)striplower_LOOPBACK_HOSTS)r*   s    0C:/MCSJAgent/foster-ask-michel/staged/webhook.py_is_loopback_hostr1   Z   s$     ::<?22    c                      t         S )z4Check if webhook adapter dependencies are available.)AIOHTTP_AVAILABLE r2   r0   check_webhook_requirementsr6   g   s    r2   c                       e Zd ZdZdef fdZdefdZd/dZ	 	 d0de	d	e	d
e
e	   de
ee	ef      def
dZdeddfdZdeddfdZde	dedefdZde	dedefdZde	dee	ef   fdZd1dZd/dZd2dZd1dZdddede	defdZ	 d3dede	de	de	d e	d!edefd"Zd#e	d$ed%e	de	de	f
d&Zd'ed$edefd(Zd	e	d)edefd*Z d	e	d)edefd+Z!d	e	d)edefd,Z"d-e	d	e	d)edefd.Z# xZ$S )4WebhookAdapterzBGeneric webhook receiver that triggers agent runs from HTTP POSTs.configc                    t         |   |t        j                         |j                  j                  dt              | _        t        |j                  j                  dt                    | _
        |j                  j                  dd      | _        |j                  j                  di       | _        i | _        d| _        t        | j                        | _        d | _        i | _        i | _        t)               | _        d | _        i | _        d| _        d| _        i | _        t        |j                  j                  dd	            | _        t        |j                  j                  d
d            | _        y )Nr*   portsecret routesg        i  
rate_limit   max_body_bytesi   )super__init__r   WEBHOOKextragetDEFAULT_HOST_hostintDEFAULT_PORT_port_global_secret_static_routes_dynamic_routes_dynamic_routes_mtimedict_routes_runner_delivery_info_delivery_info_createdr   _delivery_info_ordergateway_runner_seen_deliveries_idempotency_ttl_seen_deliveries_next_prune_at_rate_counts_rate_limit_max_body_bytes)selfr9   	__class__s     r0   rC   zWebhookAdapter.__init__o   s4   !1!12 ,,**6<@
fll..v|DE
#)<<#3#3Hb#A/5||/?/?"/M02,/"(,T-@-@(A 028:#>Cg! # 35%)58+ 68 #FLL$4$4\2$F G %(LL-y9%
r2   r+   c                   K   | j                          | j                  j                         D ]  \  }}|j                  d| j                        }|st        d| dt         d      |t        k(  r1t        | j                        st        d| d| j                   d      |j                  d      s|j                  dd	      }|r|d	k(  st        d| d
| d       t        j                         }|j                  j                  d| j                         |j                  j                  d| j                         |j                  j                  d| j                         dd l}	 |j!                  |j"                  |j$                        5 }|j'                  d       |j)                  d| j*                  f       d d d        t,        j/                  d| j*                         y# 1 sw Y   *xY w# t0        t2        f$ r Y nw xY wt        j4                  |      | _        | j6                  j9                          d {  7   t        j:                  | j6                  | j                  | j*                        }|j=                          d {  7   | j?                          djA                  | j                  jC                               xs d}	t,        jE                  d| j                  | j*                  |	       yw)Nr<   z[webhook] Route 'zf' has no HMAC secret. Set 'secret' on the route or globally. For testing without auth, set secret to 'z'.zB' uses INSECURE_NO_AUTH secret but is bound to non-loopback host 'z`'. INSECURE_NO_AUTH is for local testing only. Refusing to start to prevent accidental exposure.deliver_onlydeliverlogz(' has deliver_only=true but deliver is 'z['. Direct delivery requires a real target (telegram, discord, slack, github_comment, etc.).z/healthz/webhooks/{route_name}z"/p/{profile}/webhooks/{route_name}r      r)   z][webhook] Port %d already in use. Set a different port in config.yaml: platforms.webhook.portF, z(none configured)u+   [webhook] Listening on %s:%d — routes: %sT)#_reload_dynamic_routesrQ   itemsrF   rL   
ValueError_INSECURE_NO_AUTHr1   rH   r
   Applicationrouteradd_get_handle_healthadd_post_handle_webhooksocketAF_INETSOCK_STREAM
settimeoutconnectrK   loggererrorConnectionRefusedErrorOSError	AppRunnerrR   setupTCPSitestart_mark_connectedjoinkeysinfo)
r]   namerouter<   ra   app_socket_ssiteroute_namess
             r0   rs   zWebhookAdapter.connect   s    ##%  <<--/KD%YYx)<)<=F 'v .@@Q?RRTV  **3DTZZ3P 'v .::>** FHI  yy())Iu5'U"2$+D6 2''.i 0XY 7 0B oo

9d&9&9:

4d6J6JK 	

0$2F2F	

 	!	1D1DEa 

K45 F LLxz~  {E  {E  F	 FE
 '0 		 }}S)ll  """{{4<<TZZ@jjlii 1 1 34K8K9JJJJ		
 si   B0K?3K?BK?+&G5 /G) (G5 (K?)G2.G5 5HK?H:K?IAK?JA/K?Nc                    K   | j                   r)| j                   j                          d {    d | _         | j                          t        j	                  d       y 7 1w)Nz[webhook] Disconnected)rR   cleanup_mark_disconnectedrt   r   )r]   s    r0   
disconnectzWebhookAdapter.disconnect   sH     <<,,&&(((DL!,- )s   *A A2A chat_idcontentreply_tometadatac                   K   | j                   j                  |i       }|j                  dd      }|dk(  r&t        j                  d||dd        t	        d      S |dk(  r;|xs i j                  d	      st	        d      S | j                  ||       d{   S |d
k(  r| j                  ||       d{   S |t        v }|s	 ddlm	} |j                  |      }| j                  r|r| j                  |||       d{   S t        j                  d|       t	        dd|       S 7 7 w# t        $ r Y ^w xY w7 =w)u  Deliver the agent's response to the configured destination.

        chat_id is ``webhook:{route}:{delivery_id}``.  The delivery info
        stored during webhook receipt is read with ``.get()`` (not popped)
        so that interim status messages emitted before the final response
        — fallback-model notifications, context-pressure warnings, etc. —
        do not consume the entry and silently downgrade the final response
        to the ``log`` deliver type.  TTL cleanup happens on POST.
        ra   rb   z[webhook] Response for %s: %sN   Tsuccesshttp_callbacknotifygithub_commentr   )platform_registryz"[webhook] Unknown deliver type: %sFzUnknown deliver type: r   ru   )rS   rF   rt   r   r   _deliver_http_callback_deliver_github_comment_BUILTIN_DELIVER_PLATFORMSgateway.platform_registryr   is_registered	ExceptionrV   _deliver_cross_platformwarning)	r]   r   r   r   r   deliverydeliver_type_is_known_platformr   s	            r0   sendzWebhookAdapter.send   s^      &&**7B7||Iu55 KK7'$3-Pd++?* N''1!$//44WhGGG++55gxHHH *-GG!G%6%D%D\%R" #555gx   	;\J#9,!H
 	
) H I  sT   BED,E6D.7ED0 $ED?+E.E0	D<9E;D<<Enowc                 b   t        | j                        t        | j                        k  r;t        d t	        | j                  j                         d       D              | _        || j                  z
  }| j                  r| j                  d   d   |k  r| j                  j                         \  }}| j                  j                  |      |k7  r]| j                  j                  |d       | j                  j                  |d       | j                  r| j                  d   d   |k  ryyyy)a  Drop delivery_info entries older than the idempotency TTL.

        Mirrors the cleanup pattern used for ``_seen_deliveries``.  Called
        on each POST so the dict size is bounded by ``rate_limit * TTL``
        even if many webhooks fire and never receive a final response.
        c              3   *   K   | ]  \  }}||f  y w)Nr5   ).0key
created_ats      r0   	<genexpr>z6WebhookAdapter._prune_delivery_info.<locals>.<genexpr>-  s%      .(OC S!(s   c                     | d   S )Nrc   r5   )items    r0   <lambda>z5WebhookAdapter._prune_delivery_info.<locals>.<lambda>0  s    $q'r2   )r   r   N)lenrU   rT   r   sortedrf   rX   popleftrF   rS   pop)r]   r   cutoffr   r   s        r0   _prune_delivery_infoz#WebhookAdapter._prune_delivery_info%  s    t(()C0K0K,LL(- .'-//557=Q(. )D% t,,,''D,E,Ea,H,Kf,T"77??AOJ**..s3zA##C.''++C6 ''D,E,Ea,H,Kf,T',T'r2   c           	      N   || j                   k  ry|| j                  z
  }| j                  j                         D cg c]  \  }}||k  s| }}}|D ]  }| j                  j	                  |d         |t        dt        d| j                  dz              z   | _         yc c}}w )zDOccasionally prune expired delivery IDs without scanning every POST.Nr$   g      ?
   )rY   rX   rW   rf   r   minmax)r]   r   r   ktstales         r0   _prune_seen_deliveriesz%WebhookAdapter._prune_seen_deliveries;  s    444t,,,#44::<K<tq!F
<KA!!%%a. .1Cc#tG\G\_aGa>b4c.c+ Ls   B!B!
route_namec                 J   | j                   j                  |      }t        |t              s t        |xs d      }|| j                   |<   |}|t        z
  }|r#|d   |k  r|j                          |r	|d   |k  rt        |      | j                  k\  ry|j                  |       y)zDReturn True if route is still within limit after recording this hit.r5   r   FT)	rZ   rF   
isinstancer   _RATE_WINDOW_SECONDSr   r   r[   append)r]   r   r   window
new_windowr   s         r0   _record_rate_limit_hitz%WebhookAdapter._record_rate_limit_hitE  s    ""&&z2&%(',V\r':J,6Dj)F++V+NN V+v;$***cr2   delivery_idc                 >   | j                   j                  |      }|||z
  | j                  k  ry|| j                   j                  |d       || j                   |<   t	        | j                         t        | j                  dz  d      kD  r| j                  |       y)z3Return True when this delivery should be processed.NF      T)rW   rF   rX   r   r   r   r[   r   )r]   r   r   seen_ats       r0   _record_delivery_idz"WebhookAdapter._record_delivery_idT  s    ''++K83=43H3H#H!!%%k48-0k*t$$%D,<,<q,@#(FF'',r2   c                    K   |ddS w)Nwebhook)r   typer5   )r]   r   s     r0   get_chat_infozWebhookAdapter.get_chat_info`  s     33s   requestweb.Requestc                 :   K   t        j                  ddd      S w)u$   GET /health — simple health check.okr   )statusplatform)r
   json_response)r]   r   s     r0   rl   zWebhookAdapter._handle_healthg  s       Di!HIIs   c                 D   ddl m}  |       }|t        z  }|j                         sC| j                  r6i | _        t        | j                        | _        t        j                  d       y	 |j                         j                  }|| j                  k  ryt        j                  |j                  d            }t!        |t
              syi }|j#                         D ]  \  }}|| j                  v r|j%                  d| j&                        }	|	st        j)                  d|t*               O|	t*        k(  r7t-        | j.                        s"t        j)                  d	|| j.                         |||<    || _        i | j                  | j                  | _        || _        t        j1                  d
t3        | j                        dj5                  | j                  j7                               xs d       y# t8        $ r }
t        j;                  d|
       Y d}
~
yd}
~
ww xY w)zAReload agent-created subscriptions from disk if the file changed.r   )get_hermes_homezD[webhook] Dynamic subscriptions file removed, cleared dynamic routesNutf-8)encodingr<   z[webhook] Dynamic route '%s' skipped: 'secret' is missing or empty. Set a valid HMAC secret, or use '%s' to explicitly disable auth (testing only).zm[webhook] Dynamic route '%s' skipped: INSECURE_NO_AUTH is only allowed on loopback hosts. Current host: '%s'.z*[webhook] Reloaded %d dynamic route(s): %srd   z(none)z-[webhook] Failed to reload dynamic routes: %s)hermes_constantsr   _DYNAMIC_ROUTES_FILENAMEexistsrN   rP   rM   rQ   rt   debugstatst_mtimerO   jsonloads	read_textr   rf   rF   rL   r   rh   r1   rH   r   r   r}   r~   r   ru   )r]   r   hermes_home	subs_pathmtimedatanew_dynamicr   veffective_secretes              r0   re   z%WebhookAdapter._reload_dynamic_routesk  s   4%'"::	!##')$#D$7$78cd.	MNN$--E222::i1171CDDdD)
 ,.K

1+++#$5543F3F#G 'NNJ ) $(99-djj9NNQ

	 !"A1 %2 $/D Jd22Jd6I6IJDL).D&KK<D(()		$..3356B(
  	MLLH!LL	Ms%   +)G6 5G6 D*G6 6	H?HHc                 B   |j                   j                  d      xs dj                         }|sy| j                  }t	        |dd      }t	        |dd      sy	 ddlm}  |d	
      D ch c]  \  }}|	 }}}||vrt        S |S c c}}w # t        $ r	 t        cY S w xY w)a  Resolve + validate the /p/<profile>/ URL prefix on a webhook request.

        Returns:
          - ``None`` when no profile prefix is present, or multiplexing is off
            (the prefix is ignored, request handled as the default profile).
          - the profile name (str) when present, multiplexing is on, and the
            profile is one this gateway serves.
          - ``_PROFILE_REJECTED`` when a prefix is present but the profile is
            unknown/unconfigured (handler returns 404).
        profiler=   Nr9   multiplex_profilesFr   )profiles_to_serveT)	multiplex)	
match_inforF   r-   rV   getattrhermes_cli.profilesr   r   _PROFILE_REJECTED)	r]   r   r   runnercfgr   r   _serveds	            r0   _resolve_request_profilez'WebhookAdapter._resolve_request_profile  s     %%)))4:AAC$$fh-s0%8 	%=*;d*KL*KwtQd*KFL & $$ M 	%$$	%s$   B +B7B B BBc                 r  K   | j                          |j                  j                  dd      }| j                  j                  |      }| j	                  |      }|t
        u rt        j                  ddid      S |st        j                  dd| id      S |j                  dd	      d
u rt        j                  dd| id      S |j                  xs d}|| j                  kD  rt        j                  ddid      S 	 |j                          d{   }|j                  d| j                        }|s/t        j                  d|       t        j                  ddid      S |t        k7  rB| j!                  |||      s/t        j#                  d|       t        j                  ddid      S t%        j$                         }	| j'                  ||	      st        j                  ddid      S 	 t)        j*                  |      }
|j8                  j                  dd      xsH |j8                  j                  dd      xs* |
j                  d d      xs |
j                  d!d      xs d"}|j                  d#g       }|r4||vr0t        j;                  d$|||       t        j                  d%|d&      S |j                  d'd      }| j=                  ||
||      }|j                  d(g       }|rG	 dd)lm }m!}  |       }|D ]2  }d*| }||v r |||+      }|s|} nt        j#                  d,|       4 |j8                  j                  d.|j8                  j                  d/|j8                  j                  d0tE        tG        t%        j$                         d1z                                }t%        j$                         }	| jI                  ||	      s0t        jK                  d2|       t        j                  d3|d4d5      S |j                  d6      r|j                  d7d8      | jM                  |j                  d9i       |
      |
d:}t        jK                  d;|||d7   tO        |      |       	 | jQ                  ||       d{   }|jT                  rt        j                  d@||d7   |dAd5      S t        j#                  dB||d7   |j                         t        j                  dd=|d>d?      S dC| dD| }|j                  d7d8      | jM                  |j                  d9i       |
      |
d:}|| jV                  |<   |	| jX                  |<   | jZ                  j]                  |	|f       | j_                  |	       | ja                  |dE| dFdC| |G      }|rtc        |tD              r||_2        tg        |th        jj                  ||
|H      }t        jK                  dI|jl                  ||tO        |      |       to        jp                  | js                  |            }| jt                  jw                  |       |jy                  | jt                  jz                         t        j                  dJ|||dKdL      S 7 k# t        $ r9}t        j                  d|       t        j                  ddid      cY d}~S d}~ww xY w# t(        j,                  $ rg 	 ddl}t1        |j2                  j5                  |j7                  d                  }
n*# t        $ r t        j                  ddid      cY cY S w xY wY :w xY w# t        $ r!}t        j#                  d-|       Y d}~4d}~ww xY w7 # t        $ r5 t        jS                  d<||       t        j                  dd=|d>d?      cY S w xY ww)MuD   POST /webhooks/{route_name} — receive and process a webhook event.r   r=   ru   zUnknown or unconfigured profilei  )r   zUnknown route: enabledTFzRoute disabled: i  r   zPayload too largei  Nz![webhook] Failed to read body: %szBad requesti  r<   z7[webhook] Route %s has no HMAC secret; refusing requestz'Webhook route is missing an HMAC secretz([webhook] Invalid signature for route %szInvalid signaturei  zRate limit exceededi  r   zCannot parse bodyzX-GitHub-EventzX-GitLab-Event
event_typer   unknowneventsz6[webhook] Ignoring event %s for route %s (allowed: %s)ignored)r   eventpromptskills)build_skill_invocation_messageget_skill_commands/)user_instructionz[webhook] Skill '%s' not foundz"[webhook] Skill loading failed: %szX-GitHub-Deliverysvix-idX-Request-IDi  z([webhook] Skipping duplicate delivery %s	duplicate)r   r   r   r`   ra   rb   deliver_extra)ra   r  payloadzK[webhook] direct-deliver event=%s route=%s target=%s msg_len=%d delivery=%sz4[webhook] direct-deliver failed route=%s delivery=%szDelivery failed)r   ru   r   i  	delivered)r   r   targetr   zD[webhook] direct-deliver target rejected route=%s target=%s error=%szwebhook::zwebhook/r   )r   	chat_name	chat_typeuser_id	user_name)textmessage_typesourceraw_message
message_idz8[webhook] %s event=%s route=%s prompt_len=%d delivery=%saccepted)r   r   r   r      )>re   r   rF   rQ   r   r   r
   r   content_lengthr\   readr   rt   ru   rL   rh   _validate_signaturer   timer   r   r   JSONDecodeErrorurllib.parserP   parse	parse_qsldecodeheadersr   _render_promptagent.skill_commandsr  r  strrI   r   r   _render_delivery_extrar   _direct_deliver	exceptionr   rS   rT   rU   r   r   build_sourcer   r   r   r   TEXTmethodasynciocreate_taskhandle_message_background_tasksaddadd_done_callbackdiscard)r]   r   r   route_configr   r  raw_bodyr   r<   r   r	  urllibr   allowed_eventsprompt_templater   r   r  r  
skill_cmds
skill_namecmd_keyskill_contentr   r   resultsession_chat_iddeliver_configr  r   tasks                                  r0   rn   zWebhookAdapter._handle_webhook  s     	##%''++L"=
||''
3 //8''$$;<S  $$OJ<89#  It,5$$,ZL9:3  !//41D000$$-.s 
	K$\\^+H !!(D,?,?@LLI $$CD  &&++GXvF>
 ((123 
 iik**:s;$$/0 
	jj*G  OO 0"5 ""#3R8{{<, {{62&  	 &))(B7j>LLH	 $$$z: 
 '**8R8$$Wj*
 !!(B/H
 01
"(J !*.G*,(F#f) )%2F!<j #)" oo))OO##NCDIIK$<N8O4PQ
 iik''S9KK:K $$&{C  N+'++Iu=!%!<!< $$_b97" #H KK]#F#33FHEE ~~(("-!+"*9"5'2	   NNV#	 $$"->{[  %ZL+? $''	59!88  "5w 
 0>O,7:##O4!!((#)?@!!#& ""# -zl+  # 
 z'3/$FN$))"
 	FNNK	
 ""4#6#6u#=>""4(t55==>  $##*	 
 	
Y , 	KLL<a@$$g}%=cJJ	KJ ## 		#LL**8??7+CD  ((123  	B  HCQGGH^ F 	  J
 ((&1BS^_ 	s   D ^7Z ZZ C^73[	 C(^71)] ] 7D:^72]6 ]3]6 G5^7Z 	[.[;[<^7[^7	]7\]"\<7]9^7;\<<]?^7]^7	]0]+%^7+]00^73]6 6;^41^73^44^7bodyr<   c                    dt         dt         ffd} |d      } |d      } |d      }|s|s|r| j                  |||||      S j                  j                  dd	      }|rZd
t	        j
                  |j                         |t        j                        j                         z   }	t	        j                  ||	      S j                  j                  dd	      }
|
rt	        j                  |
|      S j                  j                  dd	      }|rWt	        j
                  |j                         |t        j                        j                         }	t	        j                  ||	      S t        j                  d       y)zGValidate webhook signature (GitHub, GitLab, Svix, generic HMAC-SHA256).r   r+   c                     j                   j                  | d      xsV j                   j                  | j                         d      xs* j                   j                  | j                         d      S )Nr=   )r!  rF   r.   upper)r   r   s    r0   _headerz3WebhookAdapter._validate_signature.<locals>._header  sZ    ##D"- 9??&&tzz|R89??&&tzz|R8r2   r  zsvix-timestampzsvix-signature)r?  r<   msg_id	timestampsignature_headerzX-Hub-Signature-256r=   zsha256=zX-Gitlab-TokenX-Webhook-Signaturez9[webhook] Secret configured but no signature header foundF)r$  _validate_svix_signaturer!  rF   hmacnewencodehashlibsha256	hexdigestcompare_digestrt   r   )r]   r   r?  r<   rC  svix_idsvix_timestampsvix_signaturegh_sigexpectedgl_tokengeneric_sigs    `          r0   r  z"WebhookAdapter._validate_signature  sc   	# 	# 	 )$ !12 !12n00(!/ 1   $$%:B? 488w~~$ikH &&vx88 ??&&'7<&&x88 oo))*?Dxxw~~ik  &&{H== 	G	
 r2   rD  rE  rF  tolerance_secondsc                    |r|r|r|sy	 t        |      }t        t        t	        j                               |z
        |kD  rt
        j                  d       y|j                  d      r*|j                  d      }	 t        j                  |d      }	n%t
        j                  d       |j                         }	|j                         dz   |j                         z   dz   |z   }
t        j                  t!        j"                  |	|
t$        j&                        j)                               j+                         }|j-                         D ]6  }	 |j-                  d	d
      \  }}|dk(  st!        j.                  ||      s6 y y# t        t        f$ r Y yw xY w# t        j                  t        f$ r t
        j                  d       Y yw xY w# t        $ r Y w xY w)z?Validate Svix-compatible signatures used by AgentMail webhooks.Fz8[webhook] Svix signature timestamp outside replay windowwhsec_T)validatez,[webhook] Invalid whsec_ Svix signing secretz9[webhook] Validating Svix-style signature with raw secret   .,rc   v1)rI   	TypeErrorrg   absr  rt   r   
startswithremoveprefixbase64	b64decodebinasciiErrorr   rK  	b64encoderI  rJ  rL  rM  digestr   splitrO  )r]   r?  r<   rD  rE  rF  rW  tsencoded_secretr   signed_contentrT  partversion	signatures                  r0   rH  z'WebhookAdapter._validate_svix_signature  s    9)9f	YB s499;"$%(99NNUVX&#00:N&&~E LLTU--/C4/)2B2B2DDtKdR##HHS.'..9@@B

&( 	 %**,D%)ZZQ%7" $4#6#6y(#K - C :& 		 NNJ/ KL&  s5   F  ;F 
G	 FF.GG		GGtemplater	  r   c                     |s&t        j                  d      dd }d| d| d| dS d	t        j                  d
t        ffd}t        j
                  d||      S )u  Render a prompt template with the webhook payload.

        Supports dot-notation access into nested dicts:
        ``{pull_request.title}`` → ``payload["pull_request"]["title"]``

        Special token ``{__raw__}`` dumps the entire payload as indented
        JSON (truncated to 4000 chars).  Useful for monitoring alerts or
        any webhook where the agent needs to see the full payload.
        r   indentN  zWebhook event 'z' on route 'z':

```json
z
```matchr+   c                 f   | j                  d      }|dk(  rt        j                  d      d d S }|j                  d      D ]0  }t	        |t
              r|j                  |d| d      }*d| dc S  t	        |t
        t        f      rt        j                  |d      d d	 S t        |      S )
Nrc   __raw__r   rq  rs  .{}i  )	groupr   dumpsrh  r   rP   rF   listr$  )rt  r   valuerl  r	  s       r0   _resolvez/WebhookAdapter._render_prompt.<locals>._resolvei  s    ++a.Cizz'!4Ud;; E		#eT*!IIdbRL9EuB<'	 '
 %$.zz%25D99u:r2   z\{([a-zA-Z0-9_.]+)\})r   r{  reMatchr$  sub)r]   ro  r	  r   r   	truncatedr~  s     `    r0   r"  zWebhookAdapter._render_promptR  sp      

715et<I!* .<yk@
	BHH 	 	 vv-xBBr2   rE   c                     i }|j                         D ]2  \  }}t        |t              r| j                  ||dd      ||<   .|||<   4 |S )z8Render delivery_extra template values with payload data.r=   )rf   r   r$  r"  )r]   rE   r	  renderedr   r}  s         r0   r%  z%WebhookAdapter._render_delivery_extraz  sQ     $&++-JC%% $ 3 3E7B K %	 (
 r2   r   c           
      H  K   |j                  di       }t        |j                  dd            j                         }t        |j                  dd            j                         }t        |      }|j                  dk7  s#|j
                  rt        |j
                        r|st        dd      S |j                  d	i       }t        |j                  d
d            j                         }|st        dd      S d||j                  di       |j                  dd      |t        t        j                               d}	t        j                  |	dd      j                  d      }
t        j                  |j                  d      |
t        j                         j#                         }	 ddl}|j'                  d      }|j)                  |      4 d{   }|j+                  ||
d||dd      4 d{   }|j-                          d{    d|j.                  cxk  rdk  r1n n.t        d       cddd      d{    cddd      d{    S t        dd!|j.                         cddd      d{    cddd      d{    S 7 7 7 7 X7 I7  7 # 1 d{  7  sw Y   nxY wddd      d{  7   y# 1 d{  7  sw Y   yxY w# t0        $ r6}t2        j5                  d"|       t        dt        |            cY d}~S d}~ww xY ww)#a  POST a signed final agent result to a fixed loopback callback.

        The callback URL and secret come from route configuration, never from
        the request payload. Loopback-only enforcement prevents a signed
        webhook caller from turning Hermes into an arbitrary HTTP/SSRF client;
        remote agents reach the loopback port through a separately restricted
        SSH reverse forward.
        r  urlr=   r<   httpFz1http_callback requires a signed loopback http URLr   r	  
request_idzMissing request_idmichel_verification_resultoriginrequested_byfoster)r   r  r  r  r;  completed_at)r\  r  )ensure_ascii
separatorsr   r   N   )total)timeoutzapplication/jsonz Michel-Verification-Callback/1.0)zContent-TyperG  r  z
User-Agent)r   r!  r   ,  Tr   zCallback returned HTTP z*[webhook] http_callback delivery error: %s)rF   r$  r-   r	   schemehostnamer1   r   rI   r  r   r{  rK  rI  rJ  rL  rM  rN  aiohttpClientTimeoutClientSessionpostr  r   r   rt   ru   )r]   r   r   rE   r  r<   parsedsource_payloadr  callback_payloadr?  rn  r  r  sessionresponseexcs                    r0   r   z%WebhookAdapter._deliver_http_callback  s     _b1%))E2&'--/UYYx,-335#MMV#??$V__5I 
 "i4++L"=>DDF
e3GHH 7$$((26*..~xH		,
 zz5Z

&/ 	 HHMM'"D'..

)+ 		=++"+5G,,W,=="<<(:/8(2&H	 ( 	 	 "--/))hoo33)$7	 	 	 >== & % 77HI	 	 	 >==	 *	 >	 >	 	 	 >====$  	=LLEsKe3s8<<	=s8  F&L"),K  JK  K7J8K;J$J'J$7KJKK  JK  L"J$3K?J  KK  J"K  L"K  KJ$KK   K"K  $J6	*J-+J6	2K9K  KK  
L"KKKK  L"K   	L)+LLL"LL"c                    K   |j                  dd      }|dk(  r%t        j                  d|dd        t        d      S |dk(  r| j	                  ||       d{   S | j                  |||       d{   S 7 7 w)	u  Deliver *content* directly without invoking the agent.

        Used by ``deliver_only`` routes: the rendered template becomes the
        literal message body, and we dispatch to the same delivery helpers
        that the agent-mode ``send()`` flow uses.  All target types that
        work in agent mode work here — Telegram, Discord, Slack, GitHub
        PR comments, etc.
        ra   rb   z%[webhook] direct-deliver log-only: %sNr   Tr   r   )rF   rt   r   r   r   r   )r]   r   r   r   s       r0   r&  zWebhookAdapter._direct_deliver  s       ||Iu55  KK?#Od++++55gxHHH 11'8
 
 	
	 I
s$   AA=A9A=4A;5A=;A=c           
        K   |j                  di       }|j                  dd      }|j                  dd      }|r|s"t        j                  d       t        dd      S 	 t	        j
                  d	d
dt        |      d|d|gddd      }|j                  dk(  r#t        j                  d||       t        d      S t        j                  d|j                         t        d|j                        S # t        $ r% t        j                  d       t        dd      cY S t        $ r6}t        j                  d|       t        dt        |            cY d}~S d}~ww xY ww)z@Post agent response as a GitHub PR/issue comment via ``gh`` CLI.r  repor=   	pr_numberz;[webhook] github_comment delivery missing repo or pr_numberFzMissing repo or pr_numberr   ghprcommentz--repoz--bodyTr@   )capture_outputr  r  r   z![webhook] Posted comment on %s#%sr   z"[webhook] gh pr comment failed: %suO   [webhook] 'gh' CLI not found — install GitHub CLI for github_comment deliveryzgh CLI not installedz+[webhook] github_comment delivery error: %sN)rF   rt   ru   r   
subprocessrunr$  
returncoder   stderrFileNotFoundErrorr   )r]   r   r   rE   r  r  r;  r   s           r0   r   z&WebhookAdapter._deliver_github_comment  sW     _b1yy$IIk2.	9LLM %@ $	;^^	N	  $F   A%7y "$//8&-- "%v}}EE  	LL* %;   	;LLFJe3q6::	;sO   AE! AC3 ;E!<6C3 2E!3+EE! E(+EEE!EE!platform_namec                 j  K   | j                   st        dd      S 	 t        |      }| j                   j                  j                  |      }|st        dd| d      S |j                  di       }|j                  dd	      }|sD| j                   j                  j                  |      }|r|j                  }nt        dd
|       S d}	|j                  d      xs |j                  d      }
|
rd|
i}	|j                  |||	       d{   S # t        $ r t        dd|       cY S w xY w7 #w)z=Route response to another platform (telegram, discord, etc.).Fz-No gateway runner for cross-platform deliveryr   zUnknown platform: z	Platform z not connectedr  r   r=   zNo chat_id or home channel for Nmessage_thread_id	thread_id)r   )
rV   r   r   rg   adaptersrF   r9   get_home_channelr   r   )r]   r  r   r   target_platformadapterrE   r   homer   r  s              r0   r   z&WebhookAdapter._deliver_cross_platform$  sQ     ""E 
	&}5O %%..22?C!-?  _b1))Ir*&&-->>OD,,!!;M?K  II12Leii6L	#Y/H\\'7X\FFF?  	'9-%I 	> Gs4   D3D C%D3D1D3D.+D3-D..D3)r+   N)NN)r   r   r+   zweb.Response)r   r   )r  )%__name__
__module____qualname____doc__r   rC   boolrs   r   r$  r   r   r   r   r   floatr   r   r   r   r   rl   re   r   rn   bytesr  rI   rH  rP   r"  r%  r   r&  r   r   __classcell__)r^   s   @r0   r8   r8   l   s\   L(
~ (
\Jt JX. #'-13
3
 3
 3-	3

 4S>*3
 
3
j7 7$ 7,d% dD d 5 T 
s 
 
4 
43 44S> 4J9Mv:[
B	6$6,16;>6	6~ "%00 0 	0
 0 0 0 
0l&C&C &C 	&C
 &C 
&CP

$(
	
 F=F=&*F=	F=P

&*
	
84;4;&*4;	4;l+G +G+.+G:>+G	+Gr2   r8   )3r  r+  rb  rd  rL  rI  r   loggingr  r  r  collectionsr   typingr   r   r   r   r   r  r	   r  r
   r4   ImportErrorgateway.configr   r   gateway.platforms.baser   r   r   r   	getLoggerr  rt   objectr   r   rG   rJ   rh   r   r   	frozensetr/   r$  r  r1   r6   r8   r5   r2   r0   <module>r     s   8        	    3 3 !
 4  
		8	$
 H   & 7     
3C 
3D 
3D 
cG( cG{  
Cs   B- -	B98B9