
    `gjo                        U d Z ddlmZ ddlZddlZddlmZmZmZm	Z	 ddl
mZmZ ddlmZmZmZ ddlmZ ddlmZ dd	lmZ  ej.                  e      Zdd
ZeedZded<    G d de      Zy)u  RelayAdapter — one generic gateway adapter fronted by the connector. EXPERIMENTAL.

A single ``BasePlatformAdapter`` subclass that, at handshake, receives a
``CapabilityDescriptor`` from the connector telling it which platform it is
fronting and which capabilities to advertise to the ``GatewayStreamConsumer``.
It implements the four abstract methods (``connect`` / ``disconnect`` / ``send``
/ ``get_chat_info``) plus the capability surface (``MAX_MESSAGE_LENGTH``,
``message_len_fn``, ``supports_draft_streaming``) by delegating wire I/O to an
injected transport and reading capabilities off the descriptor.

There is NO per-platform gateway code: the connector is the only side that knows
"this chat_id maps to a Discord channel, send it via the Discord websocket."
The gateway sees an ordinary ``MessageEvent`` in and calls ``adapter.send`` out.

EXPERIMENTAL: the transport protocol and descriptor schema may change without a
deprecation cycle until >=2 Class-1 platforms validate them.
    )annotationsN)AnyCallableDictOptional)PlatformPlatformConfig)BasePlatformAdapterMessageEvent
SendResult)CapabilityDescriptor)RelayTransport)SessionSourcec                <    t        | j                  d            dz  S )z1Count UTF-16 code units (Telegram's length unit).z	utf-16-le   )lenencode)texts    H/root/.hermes/venv/lib/python3.12/site-packages/gateway/relay/adapter.py
_utf16_lenr   "   s    t{{;'(A--    )charsutf16zDict[str, Callable[[str], int]]_LEN_FNSc                  (    e Zd ZdZ	 d	 	 	 	 	 	 	 d fdZedd       Zedd       Z	 	 d	 	 	 	 	 ddZddddZ	dd	Z
d d!d
Zd"dZddZddZd#dZd$dZd%dZdd&dZd ZddZddZ	 	 d	 	 	 	 	 	 	 	 	 d'dZd(dZ	 d	 	 	 	 	 	 	 	 	 d)dZ xZS )*RelayAdapterzLGeneric relay adapter advertising a connector-negotiated capability profile.c                    t         |   |t        j                         || _        || _        |j                  | _        i | _        i | _	        i | _
        |j                  dv| _        d | _        y )N plain)super__init__r   RELAY
descriptor
_transportmax_message_lengthMAX_MESSAGE_LENGTH_scope_by_chat_dm_user_by_chat_platform_by_chatmarkdown_dialectsupports_code_blocks_revocation_monitor)selfconfigr$   	transport	__class__s       r   r"   zRelayAdapter.__init__1   sq     	0$#","?"? /1
 13 24$.$?$?}$T! BF r   c                     y)aL  Relay authorization is enforced by the connector, not locally.

        The connector authenticates this gateway's WS (per-instance secret) and
        performs owner-only author-binding resolution before delivering, so any
        inbound relay event was already authorized as THIS instance's bound user
        (``user_instance_binding``, keyed on the connector-observed author id).
        The instance therefore must not default-deny relay users for lack of a
        local ``RELAY_ALLOWED_USERS`` env allowlist. See
        ``BasePlatformAdapter.authorization_is_upstream``.
        T r.   s    r   authorization_is_upstreamz&RelayAdapter.authorization_is_upstream\   s     r   c                ^    t         j                  | j                  j                  t              S N)r   getr$   len_unitr   r4   s    r   message_len_fnzRelayAdapter.message_len_fnj   s    ||DOO44c::r   c                .    | j                   j                  S r7   )r$   supports_draft_streaming)r.   	chat_typemetadatas      r   r<   z%RelayAdapter.supports_draft_streamingn   s    
 777r   F)is_reconnectc                 K   | j                   t        d      | j                   j                  | j                         t	        | j                   dd       }t        |      r || j                         t	        | j                   dd       }t        |      r || j                         | j                   j                          d {   }|sy	 | j                   j                          d {   }| j                  |       t        | j                   d      r| j                          y7 b7 ># t        $ r }t        j                  d|       Y d }~yd }~ww xY ww)Nz(RelayAdapter has no transport configuredset_interrupt_inbound_handlerset_passthrough_handlerFzrelay handshake failed: %sauth_revokedT)r%   RuntimeErrorset_inbound_handler_on_inboundgetattrcallableon_interrupt_on_passthroughconnect	handshake	Exceptionloggerwarning_apply_descriptorhasattr_start_revocation_monitor)r.   r?   set_interruptset_passthroughokr$   excs          r   rK   zRelayAdapter.connectv   s    $ ??"IJJ++D,<,<=  1PRVWM"$++,
 "$//3LdSO$D001??**,,	#88::J 	z* 4??N3**,+ - ; 	NN7=	sN   CED'ED+ *D)+D+ /9E)D+ +	E4E
EEEc                    | j                   | j                   j                         sy	 t        j                  | j	                         d      | _         y# t
        $ r
 d| _         Y yw xY w)zSpawn (once) the task that turns a transport auth-revocation into a
        clean non-retryable 'relay disabled' fatal. Idempotent.Nzrelay-revocation-monitor)name)r-   doneasynciocreate_task_watch_for_revocationrD   r4   s    r   rR   z&RelayAdapter._start_revocation_monitor   sf     ##/8P8P8U8U8W	,'.':':**,3M(D$  	, (,D$	,s   *A A'&A'c                  K   | j                   }	 	 |t        |dd      rnt        j                  |       d{    .	 |t        |dd      syt
        j                  d       | j                  ddd       	 | j                          d{    y7 Y# t        j                  $ r  w xY w7 # t        $ r t
        j                  d	d
       Y yw xY ww)a  Poll the transport for a terminal 4401 revocation (opt-out). On
        revocation, surface a non-retryable `relay_disabled` fatal so the
        dashboard renders a clean 'Relay disabled' state instead of a red
        'retrying' spin, and notify the gateway's fatal-error handler so the
        adapter is cleanly removed (it is NOT queued for reconnection, because
        the credential is dead until the instance is recreated).TNrC   FuI   relay credential revoked (opt-out) — marking the relay adapter disabledrelay_disableduA   Relay disabled (opted out — recreate the instance to re-enable))	retryablez*relay revocation fatal-error notify failedexc_info)r%   rG   rZ   sleepCancelledErrorrN   rO   _set_fatal_error_notify_fatal_errorrM   debug)r.   poll_interval_sr0   s      r   r\   z"RelayAdapter._watch_for_revocation   s      OO		$	>5(QmmO444  GI~u$MW	

 	O 	 	

	V**,,,! 5%% 		 - 	VLLEPTLU	Vsa   C)B BB :C:B/ B-B/ CB B**C-B/ / CCCCc                Z    || _         |j                  | _        |j                  dv| _        y)zCAdopt a (re)negotiated descriptor into the live capability surface.r   N)r$   r&   r'   r+   r,   )r.   r$   s     r   rP   zRelayAdapter._apply_descriptor   s*    $","?"?$.$?$?}$T!r   c                d   K   | j                  |       | j                  |       d{    y7 w)zGBridge a connector-delivered MessageEvent into the normal adapter path.N)_capture_scopehandle_message)r.   events     r   rF   zRelayAdapter._on_inbound   s(     E"!!%(((s   &0.0c                   	 t        |dd      }|syt        |dd      }|syt        |dd      }t        |d|      }|r&|dk7  r!t        |      | j                  t        |      <   t        |dd      }|r"t        |      | j                  t        |      <   yt        |dd      }|r"t        |      | j                  t        |      <   yy# t
        $ r Y yw xY w)	u  Remember a chat_id's egress discriminator from an inbound event so our
        outbound (the agent's reply) can re-assert it for the connector's egress
        tenant resolution. Never raises — scope tracking must not break inbound.

        Two cases, matching the connector's two tenant-resolution paths:
          - SCOPED message: remember chat_id -> scope_id. The connector resolves
            the tenant from metadata.scope_id (routing table).
          - DM (no scope): remember chat_id -> the authentic author user_id.
            A DM carries no scope discriminator, so the connector instead resolves
            the tenant from the recipient's author binding (resolveByUser); it
            needs the user_id on the OUTBOUND action to do that. Without this, a
            DM reply has no resolvable discriminator and the connector's egress
            guard declines it as "target not routed to an onboarded tenant".
            See gateway-gateway routedEgressGuard.ts / the tenant resolvers.
        sourceNchat_idplatformvaluerelayscope_iduser_id)rG   strr*   r(   r)   rM   )r.   rl   srcchatrp   platform_valuescopert   s           r   rj   zRelayAdapter._capture_scope   s     	%40C3	40D sJ5H$XwAN.G";474G&&s4y1CT2E14U##CI. c9d3G36w<%%c$i0  		s"   C C A2C 0C 	CCc                6   t        |xs i       }|j                  d      s+| j                  j                  t        |            }|r||d<   |j                  d      s<|j                  d      s+| j                  j                  t        |            }|r||d<   |S )u  Ensure the outbound metadata carries the discriminator the connector's
        egress guard needs to resolve the owning tenant. Two cases:

          - SCOPED reply: re-attach metadata.scope_id (routing-table resolution).
          - DM reply: there is no scope, so re-attach metadata.user_id — the
            authentic author id we saw inbound — which the connector resolves to
            the tenant via the recipient's author binding (resolveByUser). Without
            one of these, egress is declined as 'target not routed to an onboarded
            tenant'. See gateway-gateway routedEgressGuard.ts / the tenant resolvers.

        No-op when the relevant value is already present or unknown for this chat.
        rs   rt   )dictr8   r(   ru   r)   )r.   ro   r>   metary   dm_users         r   _with_scopezRelayAdapter._with_scope  s      $HN3xx
#''++CL9E#(Z  xx
#DHHY,?++//G=G")Yr   c                `    t        | j                  dd      }|syt        fd|D              S )a  Whether ``platform`` is one of the platforms this gateway fronts over
        the relay (Phase 1.5). Reads the transport's advertised identity set; used
        to decide whether a follow-up's platform-prefixed `kind` names a real
        fronted platform worth tagging on the frame (vs. leaving egress to the
        session default). Safe when the transport is absent or single-identity._identitiesNFc              3  .   K   | ]  \  }}|k(    y wr7   r3   ).0p_rp   s      r   	<genexpr>z4RelayAdapter._platform_is_fronted.<locals>.<genexpr>:  s     1TQ1=1s   )rG   r%   any)r.   rp   idss    ` r   _platform_is_frontedz!RelayAdapter._platform_is_fronted1  s.     doo}d;1S111r   c                D   K   | j                  ||       d{    y7 w)a  Bridge a connector-delivered /stop into the adapter's interrupt path.

        The connector forwards a mid-turn interrupt down the socket owned by
        the gateway instance running ``session_key``; this routes it to the
        existing per-session interrupt mechanism (sets the
        ``_active_sessions[session_key]`` Event and clears typing), cancelling
        the right turn without touching sibling sessions.
        N)interrupt_session_activity)r.   session_keyro   s      r   rI   zRelayAdapter.on_interrupt<  s      --k7CCCs     c           
     b  K   	 t        |dd      xs d}|dk(  r>| j                  |      }|+| j                  |       | j                  |       d{    yt        j                  d|t        |dd      t        |dd             y7 4# t        $ r t        j                  d	d
       Y yw xY ww)uL  Handle a connector-forwarded passthrough request (Phase 5 §5.1).

        The passthrough plane (Discord interactions, Twilio webhooks, …) answers
        the provider's latency-critical ACK at the connector EDGE, then forwards
        the real, ALREADY-SANITIZED request to this gateway over the outbound WS.
        The connector is the trust boundary: it verified the provider signature
        at the edge and stripped any shared-identity credential (e.g. a Discord
        interaction follow-up token) into its vault — so this body carries no
        token, and the agent later acts on it via the token-less ``follow_up``
        path (``send_follow_up``), never holding the credential.

        For a Discord interaction we decode the (JSON) body and convert it to a
        normalized ``MessageEvent`` so it flows through the SAME agent path as a
        chat message (``handle_message``); the agent's reply egresses over the
        normal outbound/follow_up path. Non-JSON or non-interaction forwards are
        logged and dropped for now (Twilio/SMS over the relay is a later unit).

        NEVER raises: a malformed forward must not kill the read loop.

        NOTE (open semantic sub-design, flagged for review): the interaction ->
        MessageEvent mapping below is the v1 default. The exact agent UX for a
        slash-command / button interaction (vs. a plain message) — command name
        surfacing, option rendering, deferred-vs-immediate response — is the open
        piece tracked in the spec; the TRANSPORT + receive mechanism (this whole
        path) is settled.
        rp   r   discordNzMrelay passthrough_forward dropped (no handler): platform=%s method=%s path=%smethod?pathz)relay passthrough_forward handling failedTr`   )rG   _discord_interaction_to_eventrj   rk   rN   inforM   rO   )r.   forward	buffer_idrp   rl   s        r   rJ   zRelayAdapter._on_passthroughG  s     6	Ww
B7=2H9$::7C$''.--e444KK_3/-	 5  	WNNFQUNV	WsG   B/AB	 BB	 B/.B	 B/B	 	 B,)B/+B,,B/c                p   ddl }ddlm} 	 |j                  t	        t        |dd            j                  d            }t        |t              sy|j                  d      }|j                  d      xs i }|d	k(  rt        |j                  d
      xs d      }n&|dk(  rt        |j                  d      xs d      }nd}|j                  d      xs i }t        |t              r|j                  d      ndxs |j                  d      xs i }	t        |j                  d      xs d      }
|j                  d      }t        t        j                  |
|rdndt        |	t              r+|	j                  d      rt        |	j                  d            ndt        |	t              r+|	j                  d      rt        |	j                  d            nd|rt        |      nd|j                  d      rt        |j                  d            nd      }t        ||j                   |      S # t        $ r Y yw xY w)a  Convert a forwarded Discord interaction body to a MessageEvent, or None.

        Builds the session source the same way the connector does for an
        interaction (``interactionSessionSource`` on the connector side), so the
        agent's session key matches the one the connector bound the follow-up
        capability under. Returns None when the body isn't a usable interaction
        (e.g. a PING, which the connector already answers at the edge and never
        forwards).
        r   N)MessageTypebodyr   zutf-8typedatar   rX   r      	custom_idmemberuser
channel_idguild_idchanneldmidusername)rp   ro   r=   rt   	user_namers   
message_id)r   message_typern   )jsongateway.platforms.baser   loadsbytesrG   decoderM   
isinstancer{   r8   ru   r   r   r#   r   TEXT)r.   r   r   r   payloadityper   r   r   r   r   r   rn   s                r   r   z*RelayAdapter._discord_interaction_to_events  s    	6	jjww'D!E!L!LW!UVG '4( F#{{6"(bA:txx'-2.DaZtxx,23DDX&,"&0&>

6"DfW[[Y_M`fdf\28b9
;;z*^^#+i+5dD+AdhhtnC'Z^3=dD3IdhhWaNbc$((:./hl&.S]D18T1Bs7;;t,-
 K4D4DVTT7  		s   4H) )	H54H5c                  K   | j                   6| j                   j                          	 | j                    d {    d | _         | j
                  lt        | j
                  dd       }t        |      r'	  |       }t        j                  |      r
| d {    | j
                  j                          d {    y y 7 # t        j                  t        f$ r Y w xY w7 I# t        $ r t        j                  dd       Y iw xY w7 Ow)Ngo_idlez$relay going_idle failed during drainTr`   )r-   cancelrZ   rc   rM   r%   rG   rH   iscoroutinerN   rf   
disconnect)r.   r   results      r   r   zRelayAdapter.disconnect  s      ##/$$++-.... (,D$??& dooy$?G X"))F**62$ //,,...' '	 /**I6 & %  XLL!GRVLWX.sy   'D	C  B>C  5D	4!C! CC! D	7D8D	>C   CD	CD	C! ! DD	DD	c                0  K   | j                   yt        | j                   dd      }t        |      sy	  |       }t        j                  |      rt        | d{         S t        |      S 7 # t        $ r t        j                  dd       Y yw xY ww)u  Quiesce the relay for a scale-to-zero suspend (D12 / Phase 0).

        Unlike ``disconnect()`` (terminal teardown for shutdown/restart), this
        keeps the adapter's reconnect path armed so the gateway re-dials and
        drains its buffered backlog when the machine wakes. Delegates to the
        transport's ``go_dormant()`` when available; a transport without it (the
        stub) is a no-op that returns False, so callers degrade safely.

        NOTE: deliberately does NOT stop the revocation monitor — going dormant
        is not a teardown; the monitor stays live so a real opt-out/revocation
        during dormancy is still surfaced on wake.
        NF
go_dormantzrelay go_dormant failedTr`   )	r%   rG   rH   rZ   r   boolrM   rN   rf   )r.   r   r   s      r   r   zRelayAdapter.go_dormant  s      ??"T__lDA

#	$,F""6*&L))< ) 	LL2TLB	sF   1B&A0 A.A0 "B#
A0 -B.A0 0 BBBBc           
       K   | j                   t        dd      S | j                   j                  d|||| j                  ||      d| j                  j                  t        |                   d {   }t        t        |j                  d            |j                  d      |j                  d	      
      S 7 Hw)NFno transportsuccesserrorsend)opro   contentreply_tor>   rp   r   r   r   r   r   r   )r%   r   send_outboundr~   r*   r8   ru   r   )r.   ro   r   r   r>   r   s         r   r   zRelayAdapter.send  s      ??"e>BB44""$ ,,Wh? ++//G= 5 	
 	
 I./zz,/**W%
 	
	
s   A2B?4B=5A	B?c                v   K   | j                   |ddS | j                   j                  |       d {   S 7 w)Nr   )rX   r   )r%   get_chat_info)r.   ro   s     r   r   zRelayAdapter.get_chat_info  s6     ??"#T22__227;;;;s   0979c                  K   | j                   t        dd      S d}|r,d|v r(|j                  dd      d   }| j                  |      r|}| j                   j	                  d||||xs i d	|
       d{   }t        t        |j                  d            |j                  d      |j                  d            S 7 Hw)a  Send via a shared-identity capability bound to a session (A2 outbound).

        The gateway never holds the credential: it names the session it is
        already in plus the capability ``kind``, and the connector resolves the
        real value from its vault and egresses (enforcing the tenant match). Used
        e.g. to post a Discord interaction follow-up as the shared bot without
        the token ever reaching the gateway. See RelayTransport.send_follow_up.
        NFr   r   .   r   	follow_up)r   r   kindr   r>   r   r   r   r   r   )r%   r   splitr   send_follow_upr   r8   )r.   r   r   r   r>   follow_up_platformprefixr   s           r   r   zRelayAdapter.send_follow_up  s      ??"e>BB "C4KZZQ'*F((0%+"55!*"$N ( 6 	
 	
 I./zz,/**W%
 	
	
s   A4C6B?7A	Cr7   )r/   r	   r$   r   r0   zOptional[RelayTransport]returnNone)r   r   )r   zCallable[[str], int])NN)r=   Optional[str]r>   Optional[Dict[str, Any]]r   r   )r?   r   r   r   )r   r   )g      ?)rg   floatr   r   )r$   r   r   r   )ro   ru   r>   r   r   Dict[str, Any])rp   ru   r   r   )r   ru   ro   ru   r   r   )r   r   r   r   )
ro   ru   r   ru   r   r   r>   r   r   r   )ro   ru   r   r   )
r   ru   r   ru   r   ru   r>   r   r   r   )__name__
__module____qualname____doc__r"   propertyr5   r:   r<   rK   rR   r\   rP   rF   rj   r~   r   rI   rJ   r   r   r   r   r   r   __classcell__)r1   s   @r   r   r   .   sY   V /3	(F(F )(F ,	(F
 
(FV   ; ;
 $(-18 8 +8 
	8 5: 7r,V@U)
,\6	2	D*WX+UZ/>> #'-1

 
  	

 +
 

2< .2)
)
 )
 	)

 +)
 
)
r   r   )r   ru   r   int)r   
__future__r   rZ   loggingtypingr   r   r   r   gateway.configr   r	   r   r
   r   r   gateway.relay.descriptorr   gateway.relay.transportr   gateway.sessionr   	getLoggerr   rN   r   r   r   __annotations__r   r3   r   r   <module>r      sj   $ #   0 0 3 P P 9 2 )			8	$. -
) t
& t
r   