
    `gj                         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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ZddlmZmZ dd	lmZmZmZmZ dd
l m!Z!m"Z"  ejF                  e$      Z% e&       Z'h dZ(dZ)dZ*dZ+dZ,dZ- e.h d      Z/dee0   de1fdZ2de0de0de1fdZ3de1fdZ4 G d de      Z5y# e$ r dZdZY w xY w)uw  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
  - Generic HMAC supports a V2 signature (X-Webhook-Signature-V2) that
    binds a timestamp into the signed data for replay protection; the
    legacy body-only V1 (X-Webhook-Signature) is deprecated but still
    accepted with a warning, since it has no replay protection
  - Set secret to "INSECURE_NO_AUTH" to skip validation (testing only)
    N)deque)AnyDequeDictListOptional)webTF)PlatformPlatformConfig)BasePlatformAdapterMessageEventMessageType
SendResult)DEFAULT_SCRIPT_TIMEOUT_SECONDSWebhookRouteProcessor>   smsemailqqbotslackwecomfeishumatrixsignalweixindiscordyuanbaodingtalktelegramwhatsapp
mattermostbluebubbleshomeassistantwecom_callbacki!  INSECURE_NO_AUTHzwebhook_subscriptions.json      N@>   ::1	127.0.0.1ip6-loopbackip6-localhost	localhost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    L/root/.hermes/venv/lib/python3.12/site-packages/gateway/platforms/webhook.py_is_loopback_hostr2   s   s$     ::<?22    providedexpectedc                 f    t        j                  | j                         |j                               S )aI  Timing-safe equality for two ``str`` values, tolerant of non-ASCII input.

    ``hmac.compare_digest`` raises ``TypeError`` when given a ``str`` that
    contains non-ASCII characters. The ``provided`` value here is an
    attacker-controlled signature/token header on a public, unauthenticated
    webhook endpoint, so a single non-ASCII byte would otherwise raise out of
    the request handler and return a 500 instead of rejecting the request.
    Comparing as UTF-8 bytes keeps the constant-time guarantee while making a
    hostile header fail closed with a clean rejection.
    )hmaccompare_digestencode)r4   r5   s     r1   _hmac_str_equalr:      s%     x0(//2CDDr3   c                      t         S )z4Check if webhook adapter dependencies are available.)AIOHTTP_AVAILABLE r3   r1   check_webhook_requirementsr>      s    r3   c                       e Zd ZU dZdZeed<   def fdZdddedefd	Z	d8dZ
	 	 d9d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d:dZd8dZd;dZd:dZdd d!edd
fd"Zdd d#edd
fd$Zddd%ed&edefd'Z	 d<d%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
d0Z"d1e!d.e!de!fd2Z#ded3e!defd4Z$ded3e!defd5Z%d6eded3e!defd7Z& xZ'S )=WebhookAdapterzBGeneric webhook receiver that triggers agent runs from HTTP POSTs.Finteractive_resumeconfigc                    t         |   |t        j                         |j                  j                  dt              }|xs d | _        t        |j                  j                  dt                    | _
        |j                  j                  dd      | _        |j                  j                  di       | _        i | _        d| _        t        | j                        | _        d | _        t%               | _        i | _        i | _        t-               | _        d | _        i | _        d| _        d| _        i | _        t        |j                  j                  dd	            | _        t        |j                  j                  d
d            | _        t        |j                  j                  dt>                    | _         tC        | j@                        | _"        y )Nr+   portsecret routesg        i  
rate_limit   max_body_bytesi   script_timeout_seconds)rK   )#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set_v1_signature_warned_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_bytesr   _script_timeout_secondsr   _route_processor)selfrB   	_cfg_host	__class__s      r1   rM   zWebhookAdapter.__init__   s   !1!12 LL$$V\:	$-$5
fll..v|DE
#)<<#3#3Hb#A/5||/?/?"/M02,/"(,T-@-@(A /2e! 028:#>Cg! # 35%)58+ 68 #FLL$4$4\2$F G %(LL-y9%
 -0LL(.-
$ !6#'#?#?!
r3   )is_reconnectrn   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                  j                  d| j                         |j                  j                  d| j                          |j                  j                  d| j                          t        j"                  |      | _        | j$                  j'                          d {    t        j(                  | j$                  | j                  | j*                  t,        j.                  dk(  rdnd       }	 |j1                          d {    | j;                          dj=                  | j                  j?                               xs d}	t6        jA                  d| j                  xs d| j*                  |	       y7 7 t# t2        $ rd}| j$                  j5                          d {  7   d | _        t6        j9                  d| j                  xs d| j*                  |       Y d }~yd }~ww xY ww)NrE   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.).)client_max_sizez/healthz/webhooks/{route_name}z"/p/{profile}/webhooks/{route_name}darwinF)reuse_addresszn[webhook] Could not bind %s:%d: %s. Set a different host or port in config.yaml under platforms.webhook.extra.zall IPv4+IPv6 interfaces, z(none configured)u+   [webhook] Listening on %s:%d — routes: %sz* (all interfaces, IPv4+IPv6)T)!_reload_dynamic_routesr[   itemsrP   rV   
ValueError_INSECURE_NO_AUTHr2   rR   r	   Applicationrh   routeradd_get_handle_healthadd_post_handle_webhook	AppRunnerr\   setupTCPSiterU   sysplatformstartOSErrorcleanuploggererror_mark_connectedjoinkeysinfo)
rk   rn   namerouterE   rq   appsiteexcroute_namess
             r1   connectz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	H ood.B.BC

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

4d6J6JK 	

0$2F2F	
 }}S)ll  """ {{LLJJJJ#&<<8#;%	
	**, 	ii 1 1 34K8K9JJ99JJ		
 U 	#(  	,,&&(((DLLL+ 

88

 	si   B0K53K5CK5)J*AK5;J JJ A/K5J 	K2K-+J.,<K-(K5-K22K5Nc                    K   | j                   r)| j                   j                          d {    d | _         | j                          t        j	                  d       y 7 1w)Nz[webhook] Disconnected)r\   r   _mark_disconnectedr   r   )rk   s    r1   
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| 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 u# 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.
        rq   rr   z[webhook] Response for %s: %sN   Tsuccessgithub_commentr   )platform_registryz"[webhook] Unknown deliver type: %sFzUnknown deliver type: r   r   )r_   rP   r   r   r   _deliver_github_comment_BUILTIN_DELIVER_PLATFORMSgateway.platform_registryr   is_registered	Exceptionrb   _deliver_cross_platformwarning)	rk   r   r   r   r   deliverydeliver_type_is_known_platformr   s	            r1   sendzWebhookAdapter.sendC  s      &&**7B7||Iu55 KK7'$3-Pd++++55gxHHH *-GG!G%6%D%D\%R" #555gx   	;\J#9,!H
 	
# I  sB   A4C?6C,7C?C. $C?C=+C?.	C:7C?9C::C?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)Nr=   ).0key
created_ats      r1   	<genexpr>z6WebhookAdapter._prune_delivery_info.<locals>.<genexpr>x  s!      .#C S!.s   c                     | d   S )N   r=   )items    r1   <lambda>z5WebhookAdapter._prune_delivery_info.<locals>.<lambda>{  s
    $q' r3   )r   r   N)lenra   r`   r   sortedrx   rd   popleftrP   r_   pop)rk   r   cutoffr   r   s        r1   _prune_delivery_infoz#WebhookAdapter._prune_delivery_infop  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'r3   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      ?
   )re   rd   rc   rx   r   minmax)rk   r   r   ktstales         r1   _prune_seen_deliveriesz%WebhookAdapter._prune_seen_deliveries  s    444t,,,#44::<Ktq!F
K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.r=   r   FT)	rf   rP   
isinstancer   _RATE_WINDOW_SECONDSr   r   rg   append)rk   r   r   window
new_windowr   s         r1   _record_rate_limit_hitz%WebhookAdapter._record_rate_limit_hit  s    ""&&z2&%(',V\r':J,6Dj)F++V+NN V+v;$***cr3   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)rc   rP   rd   r   r   r   rg   r   )rk   r   r   seen_ats       r1   _record_delivery_idz"WebhookAdapter._record_delivery_id  s    ''++K83=43H3H#H!!%%k48-0k*t$$%D,<,<q,@#(FF'',r3   c                    K   |ddS w)Nwebhook)r   typer=   )rk   r   s     r1   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   )statusr   )r	   json_response)rk   r   s     r1   r~   zWebhookAdapter._handle_health  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)encodingrE   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): %srv   z(none)z-[webhook] Failed to reload dynamic routes: %s)hermes_constantsr   _DYNAMIC_ROUTES_FILENAMEexistsrX   rZ   rW   r[   r   debugstatst_mtimerY   jsonloads	read_textr   rx   rP   rV   r   rz   r2   rR   r   r   r   r   r   r   )rk   r   hermes_home	subs_pathmtimedatanew_dynamicr   veffective_secretes              r1   rw   z%WebhookAdapter._reload_dynamic_routes  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).
        profilerF   NrB   multiplex_profilesFr   )profiles_to_serveT)	multiplex)	
match_inforP   r.   rb   getattrhermes_cli.profilesr   r   _PROFILE_REJECTED)	rk   r   r   runnercfgr   r   _serveds	            r1   _resolve_request_profilez'WebhookAdapter._resolve_request_profile  s     %%)))4:AAC$$fh-s0%8 	%=*;d*KLwtQdLFL & $$ M 	%$$	%s$   B +B7B B BBc                   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{   }t        |      | j                  kD  rt        j                  ddid      S |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.                  |      }
|j<                  j                  dd      xsH |j<                  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@                  jC                  ||
||j<                        s0t        jE                  d'||       t        j                  d%d(|d)      S |j                  d*      r}tG        jH                  | j@                  jJ                  |j                  d*      |
       d{   \  }}|s0t        jE                  d+||       t        j                  d%d*|d)      S |xs |
}
|j                  d,d      }| jM                  ||
||      }|j                  d-g       }|rG	 dd.l'm(}m)}  |       }|D ]2  }d/| }||v r |||0      }|s|} nt        j'                  d1|       4 |j<                  j                  d3|j<                  j                  d4|j<                  j                  d5tU        tW        t)        j(                         d6z                                }t)        j(                         }	| jY                  ||	      s0t        jE                  d7|       t        j                  d8|d9d:      S |j                  d;      r|j                  d<d=      | j[                  |j                  d>i       |
      |
d?}t        jE                  d@|||d<   t        |      |       	 | j]                  ||       d{   }|j`                  rt        j                  dE||d<   |dFd:      S t        j'                  dG||d<   |j                         t        j                  ddB|dCdD      S dH| dI| }|j                  d<d=      | j[                  |j                  d>i       |
      dJ}|| jb                  |<   |	| jd                  |<   | jf                  ji                  |	|f       | jk                  |	       | jm                  |dK| dLdH| |M      }|rto        |tT              r||_8        ts        |tt        jv                  ||
|N      }t        jE                  dO|jx                  ||t        |      |       tG        jz                  | j}                  |            } | j~                  j                  |        | j                  | j~                  j                         t        j                  dP|||dQdR      S 7 # t        j                  $ r t        j                  ddid      cY S t        $ r9}t        j                  d|       t        j                  ddid      cY d}~S d}~ww xY w# t,        j0                  $ rg 	 ddl}t5        |j6                  j9                  |j;                  d                  }
n*# t        $ r t        j                  ddid      cY cY S w xY wY Lw xY w7 # t        $ r!}t        j'                  d2|       Y d}~cd}~ww xY w7 # t        $ r5 t        j_                  dA||       t        j                  ddB|dCdD      cY S w xY ww)SuD   POST /webhooks/{route_name} — receive and process a webhook event.r   rF   r   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  rE   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   eventz$[webhook] filtered event=%s route=%sfilter)r   reasonr   scriptz*[webhook] script ignored event=%s route=%s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-idzX-Request-IDi  z([webhook] Skipping duplicate delivery %s	duplicate)r   r   r   rp   rq   rr   deliver_extra)rq   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   r   r   i  	delivered)r   r   targetr   zD[webhook] direct-deliver target rejected route=%s target=%s error=%szwebhook::)rq   r  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      )Crw   r   rP   r[   r  r   r	   r   content_lengthrh   readHTTPRequestEntityTooLarger   r   r   r   rV   rz   _validate_signaturer   timer   r   r   JSONDecodeErrorurllib.parserZ   parse	parse_qsldecodeheadersr   rj   route_filters_matchr   asyncio	to_threadrun_route_script_render_promptagent.skill_commandsr  r  strrS   r   _render_delivery_extra_direct_deliver	exceptionr   r_   r`   ra   r   r   build_sourcer   r   r   r   TEXTmethodcreate_taskhandle_message_background_tasksaddadd_done_callbackdiscard)!rk   r   r   route_configr   r%  raw_bodyr   rE   r   r  urllibr  allowed_eventskeeptransformed_payloadprompt_templater  r  r  r  
skill_cmds
skill_namecmd_keyskill_contentr   r   resultsession_chat_iddeliver_configr   r	  tasks!                                    r1   r   zWebhookAdapter._handle_webhook  sV	     	##%''++L"=
||''
3 //8''$$;<S  $$OJ<89#  It,5$$,ZL9:3  !//41D000$$-.s 

	K$\\^+H x=4/// $$-.s  !!(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:  $$88':w
 KK6
 $$'&'  H% /6.?.?%%66  */ )%D%
 @
 (("+"*!+  *4WG '**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==>  $##*	 
 	
E	 ,,, 	 $$-.s   	KLL<a@$$g}%=cJJ	KV ## 		#LL**8??7+CD  ((123  	h)h  HCQGGH^ F 	  J
 ((&1BS^_ 	s  D c=^ ^^ Dc=$` 9Ec=b	A9c=)b 2b D:c=	b< b9b< #G4c=^ ,`	c=`	.`>`	?c=`		c=b 7ab"a?:b<c=>a??bc=bc=	b6b1+c=1b66c=9b< <;c:7c=9c::c=r	  r   outcomec                 l   K   | j                  ||j                  j                         d{    y7 w)u  Close the per-delivery webhook session once its run finishes.

        A webhook delivery is one-shot: the ``delivery_id`` is baked into the
        session key, so the session will never receive a second turn.  Mirror
        the cron completion path (``cron/scheduler.py`` →
        ``end_session(..., "cron_complete")``) by marking the session ended
        when the run completes.  Without this, webhook sessions keep
        ``ended_at`` NULL forever; ``SessionDB.prune_sessions`` only reaps
        rows with ``ended_at`` set, so unclosed webhook sessions accumulate
        unbounded and drive state.db bloat (the ghost-session leak).

        This hook is the one seam that runs at the TRUE end of the run:
        ``BasePlatformAdapter._process_message_background`` fires it after the
        message handler returns, on the success, failure, and cancellation
        paths alike — so error runs are reaped too.  (``handle_message`` is
        fire-and-forget; wrapping IT closes before the run even starts.)
        ``end_session()`` is first-reason-wins and no-ops on an already-ended
        row, so this never clobbers a ``compression``/``agent_close`` reason.
        N)_end_webhook_sessionr   r   )rk   r	  rR  s      r1   on_processing_completez%WebhookAdapter.on_processing_completea  s(     , ''u||/C/CDDDs   *424rO  c                   K   | j                   }|yt        |dd      }t        |dd      }||y	 t        |dd      }|y ||j                        }t        |dd      }t        |      r	 ||      }	nPt	        |d      r	 |j                          t        |di       xs i }
|
j                  |      }|rt        |dd      nd}	|	st        j                  d	||       y|j                  } ||	d
      }t        j                  |      r
| d{    t        j                  d|	|       y# t        $ r Y w xY w7 +# t        $ r!}t        j                  d||       Y d}~yd}~ww xY ww)uo  Mark the per-delivery webhook session ended in state.db.

        Resolves the persisted ``session_id`` from the gateway session store
        using the SAME source the run was keyed on (so profile multiplexing
        and key construction match exactly), then closes it via the existing
        ``SessionDB.end_session`` API — never a hand-written UPDATE.
        N_session_dbsession_store_session_key_for_sourcepeek_session_id_ensure_loaded_entries
session_idz0[webhook] No session_id to close for %s (key=%s)webhook_completez+[webhook] Closed session %s for delivery %sz,[webhook] Failed to close session for %s: %s)rb   r   r   callablehasattrr[  r   rP   r   r   end_sessionr1  iscoroutine)rk   r	  rO  r   
session_dbstorekey_fnsession_keypeekr]  entriesentry_endrN  r   s                  r1   rT  z#WebhookAdapter._end_webhook_sessiony  s     $$>V]D9
6-	V%>EF~ .K 5"3T:D~!+.
5"23,,. "%R8>BK0CHWUL$?d
F#
  ))D*&89F""6*LL=% % "   	LL> 	sw   /E)D< E)?D< D+ AD< E)/D< D:D< *E)+	D74D< 6D77D< <	E&E!E)!E&&E)bodyrE   c                    dt         dt         ffd} |d      } |d      } |d      }|s|s|r| j                  |||||      S j                  j                  dd	      }|rPd
t	        j
                  |j                         |t        j                        j                         z   }	t        ||	      S j                  j                  dd	      }
|
rt        |
|      S j                  j                  dd	      }|rj                  j                  dd	      }|s1t        j                  dj                  j                  dd	             y	 t        |      }t#        t        t%        j$                               |z
        dkD  r1t        j                  dj                  j                  dd	             y|j                         dz   |z   }t	        j
                  |j                         |t        j                        j                         }t        ||      S j                  j                  dd	      }|rt	        j
                  |j                         |t        j                        j                         }	j                  j                  dd	      }|| j&                  vr1| j&                  j)                  |       t        j                  d|       t        ||	      S t        j+                  d       y# t        t         f$ r Y yw xY w)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 )NrF   )r/  rP   r/   upper)r   r   s    r1   _headerz3WebhookAdapter._validate_signature.<locals>._header  sZ    ##D"- 9??&&tzz|R89??&&tzz|R8r3   r  zsvix-timestampzsvix-signature)rk  rE   msg_id	timestampsignature_headerzX-Hub-Signature-256rF   zsha256=zX-Gitlab-TokenzX-Webhook-Signature-V2zX-Webhook-Timestampu   [webhook] Route '%s' sent X-Webhook-Signature-V2 with no X-Webhook-Timestamp — rejecting rather than falling back to legacy V1r   F,  zD[webhook] Route '%s' generic HMAC V2 timestamp outside replay window   .zX-Webhook-Signaturez[webhook] Route '%s' uses legacy body-only HMAC (no timestamp), which is vulnerable to replay attacks. Add an 'X-Webhook-Timestamp' header and switch to 'X-Webhook-Signature-V2' (HMAC-SHA256 of '<timestamp>.<body>').z9[webhook] Secret configured but no signature header found)r6  _validate_svix_signaturer/  rP   r7   newr9   hashlibsha256	hexdigestr:   r   r   r   rS   	TypeErrorry   absr)  r^   r@  r   )rk   r   rk  rE   ro  svix_idsvix_timestampsvix_signaturegh_sigr5   gl_tokenv2_sigv2_timestamptssigned_contentexpected_v2generic_sigr   s    `                r1   r(  z"WebhookAdapter._validate_signature  s   	# 	# 	 )$ !12 !12n00(!/ 1   $$%:B? 488w~~$ikH #6844 ??&&'7<"8V44$ $$%=rB"??../DbIL0 &&**<<	 & 3tyy{#b()C/Z&&**<< )002T9D@N((ik  #6;77 oo))*?Dxxw~~ik  !++//bAJ!:!::))--j9-
  #;99 	G	
 U z* s   K6 6LLrp  rq  rr  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 ],  }	 |j-                  d	d
      \  }}|dk(  st/        ||      s, 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 secretrt  ,r   v1)rS   rz  ry   r{  r)  r   r   
startswithremoveprefixbase64	b64decodebinasciiErrorr   r9   	b64encoder7   rv  rw  rx  digestr.  splitr:   )rk   rk  rE   rp  rq  rr  r  r  encoded_secretr   r  r5   partversion	signatures                  r1   ru  z'WebhookAdapter._validate_svix_signature0  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" $?9h#G	 C :& 		 NNJ/ KL&  s5   E6 ;F 
F?6FF.F<;F<?	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                 t   | j                  d      }|dk(  rt        j                  d      d d S |dk(  r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 )Nr   __raw__r   r  r  r  .{}i  )	groupr   dumpsr  r   rZ   rP   listr6  )r  r   valuer  r  r  s       r1   _resolvez/WebhookAdapter._render_prompt.<locals>._resolve}  s    ++a.Cizz'!4Ud;;l"!! E		# (eT*!IIdbRL9EuB<'	(
 %$.zz%25D99u:r3   z\{([a-zA-Z0-9_.]+)\})r   r  reMatchr6  sub)rk   r  r  r  r   	truncatedr  s     ``   r1   r4  zWebhookAdapter._render_promptf  sp      

715et<I!* .<yk@
	BHH 	 	" vv-xBBr3   rO   c                     i }|j                         D ]2  \  }}t        |t              r| j                  ||dd      ||<   .|||<   4 |S )z8Render delivery_extra template values with payload data.rF   )rx   r   r6  r4  )rk   rO   r  renderedr   r  s         r1   r7  z%WebhookAdapter._render_delivery_extra  sV     $&++- 	&JC%% $ 3 3E7B K %		&
 r3   r   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.
        rq   rr   z%[webhook] direct-deliver log-only: %sNr   Tr   r   )rP   r   r   r   r   r   )rk   r   r   r   s       r1   r8  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	        |      }|d	k  rt        d
      	 t        j                  d|      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
        t        f$ r& t        j                  d|       t        dd      cY S w xY w# 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  reporF   	pr_numberz;[webhook] github_comment delivery missing repo or pr_numberFzMissing repo or pr_numberr   r   znon-positivez[webhook] invalid pr_number: %rzInvalid pr_numberz[A-Za-z0-9._-]+/[A-Za-z0-9._-]+z![webhook] invalid repo format: %rzInvalid repo formatghprcommentz--repoz--bodyTrI   )capture_outputr  timeoutz![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)rP   r   r   r   rS   ry   rz  r  	fullmatch
subprocessrunr6  
returncoder   stderrFileNotFoundErrorr   )	rk   r   r   rO   r  r  pr_intrN  r   s	            r1   r   z&WebhookAdapter._deliver_github_comment  s     _b1yy$IIk2.	9LLM %@ 
	^F{ 00  ||>ELL<dC%: $	;^^K	  $F   A%7y "$//8&-- "%v}}EEQ I& 	LL19 %8 		R ! 	LL* %;   	;LLFJe3q6::	;ss   AG/ E	 ;:G/6AF G/6F G/	2E>;G/=E>>G/+G,,G/.G,6+G'!G,"G/'G,,G/platform_namec                    K   | j                   st        dd      S 	 t        |      }| j                   j                  j                  |      }|sYt        | j                   dd      xs i j                         D ]-  \  }}t        |t              s|j                  |      }|+|} n |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: _profile_adaptersNz	Platform z not connectedr  r   rF   zNo chat_id or home channel for message_thread_id	thread_id)r   )rb   r   r
   ry   adaptersrP   r   rx   r   rZ   rB   get_home_channelr   r   )rk   r  r   r   target_platformadapter_profamapcandrO   r   homer   r  s                 r1   r   z&WebhookAdapter._deliver_cross_platform  s     ""E 
	&}5O %%..22?C '(;(;=PRV W ][]ddf t!$-xx0#"G !-?  _b1))Ir*&&-->>OD,,!!;M?K  II12Leii6L	#Y/H\\'7X\FFFU  	'9-%I 	T Gs;   FE- A:F#CF(F)F-F	FF		F)r,   N)NN)r   r   r,   zweb.Response)r   r   )rs  )(__name__
__module____qualname____doc__rA   bool__annotations__r   rM   r   r   r6  r   r   r   r   r   floatr   r   r   r   r   r~   rw   r  r   rU  rT  bytesr(  rS   ru  rZ   r4  r7  r8  r   r   __classcell__)rm   s   @r1   r@   r@      s   L  %$8
~ 8
| 5: `T `d `D. #'-1+
+
 +
 3-	+

 4S>*+
 
+
Z7 7$ 7,d% dD d 5 T 
s 
 
4 
43 44S> 4J9Mv:Q
f
E#E.1E	E0>#>69>	>Hq$q,1q;>q	qt "%00 0 	0
 0 0 0 
0l(C(C (C 	(C
 (C 
(CT

$(
	
 

&*
	
8I;I;&*I;	I;V6G 6G+.6G:>6G	6Gr3   r@   )6r  r1  r  r  rw  r7   r   loggingr  r  r   r)  collectionsr   typingr   r   r   r   r   aiohttpr	   r<   ImportErrorgateway.configr
   r   gateway.platforms.baser   r   r   r   !gateway.platforms.webhook_filtersr   r   	getLoggerr  r   objectr   r   rQ   rT   rz   r   r   	frozensetr0   r6  r  r2   r:   r>   r@   r=   r3   r1   <module>r     s  @        	  
   3 3
 4 
 
		8	$
 H  2 & 7     
3HSM 
3d 
3Ec ES ET ED 
jG( jGA  
Cs   C   	CC