"""Relay transport protocol — the gateway<->connector wire contract. EXPERIMENTAL. The ``RelayAdapter`` (gateway side) delegates all wire I/O to a ``RelayTransport``. The gateway dials OUT to the connector, so a production transport is a WebSocket client (``ws_transport.py``); in tests it is an in-memory stub (``tests/gateway/relay/stub_connector.py``). This module defines the protocol surface only: lifecycle (connect/disconnect), handshake (the advertised ``CapabilityDescriptor``), inbound (``set_inbound_handler``) and outbound (``send_outbound`` / ``get_chat_info`` / ``send_interrupt``). EXPERIMENTAL: may change without a deprecation cycle until >=2 Class-1 platforms validate it. See docs/relay-connector-contract.md. """ from __future__ import annotations from typing import Any, Awaitable, Callable, Dict, Optional, Protocol, runtime_checkable from gateway.platforms.base import MessageEvent from gateway.relay.descriptor import CapabilityDescriptor # Callback the transport invokes for each inbound normalized event. InboundHandler = Callable[[MessageEvent], Awaitable[None]] # Callback for each forwarded passthrough request (§5.1): ``(forward, buffer_id)``. # ``forward`` is a ws_transport.PassthroughForward, typed Any here because # ws_transport imports FROM this module; ``buffer_id`` (§5.3 buffered flip) is # acked by the handler after durable handoff. PassthroughHandler = Callable[[Any, Optional[str]], Awaitable[None]] @runtime_checkable class RelayTransport(Protocol): """Full gateway<->connector transport contract.""" async def connect(self) -> bool: """Open the connection to the connector; return True on success.""" ... async def disconnect(self) -> None: """Close the connection.""" ... async def handshake(self) -> CapabilityDescriptor: """Return the capability descriptor the connector advertises.""" ... def set_inbound_handler(self, handler: InboundHandler) -> None: """Register the callback invoked with each inbound MessageEvent.""" ... def set_passthrough_handler(self, handler: "PassthroughHandler") -> None: """Register the callback for each forwarded passthrough request (§5.1). The connector answers the provider's edge ACK itself, then forwards the real request over this same outbound socket (a hosted gateway has no public inbound port). Optional on a transport (a stub may not implement it). """ ... async def send_outbound( self, action: Dict[str, Any], *, platform: Optional[str] = None ) -> Dict[str, Any]: """Carry an outbound action (send/edit/typing) to the connector. Returns a result dict; for ``op == "send"`` it carries ``success`` and optionally ``message_id`` / ``error``. ``platform`` tags WHICH fronted platform this reply targets (the transport resolves the matching botId); omitted ⇒ the connector uses the session's default platform. """ ... async def get_chat_info(self, chat_id: str) -> Dict[str, Any]: """Proxy a chat-info lookup to the connector.""" ... async def send_interrupt(self, session_key: str, reason: Optional[str] = None) -> None: """Route a mid-turn /stop to the connector for ``session_key``. The connector forwards it down the socket owned by the gateway instance running that session. This is the OUTBOUND direction; the actual task cancellation happens when the connector echoes an interrupt inbound. """ ... async def go_idle(self, timeout_s: float = 10.0) -> bool: """Ask the connector to flip this instance to buffered-only (§5.3). Sends ``going_idle`` and awaits the connector-authoritative ``going_idle_ack`` (live delivery stopped; inbound now buffers for replay on reconnect). Returns True on ack, False on timeout / not-connected — the caller closes regardless. Optional on a transport. Emitted as part of the gateway's EXISTING drain transition -- not a new idle path. """ ... async def send_follow_up( self, action: Dict[str, Any], *, platform: Optional[str] = None ) -> Dict[str, Any]: """Act on a shared-identity capability bound to a session (A2 outbound). Some platforms hand the connector a credential acting on the SHARED bot identity (e.g. a Discord interaction follow-up token). Under A2 it NEVER reaches the gateway: the connector binds it in its capability vault keyed by session, and the gateway issues a SEMANTIC action against that session. Action dict: ``op == "follow_up"``, ``session_key``, ``kind`` (e.g. ``"discord.interaction_token"``), ``content``, optional ``metadata``. The connector resolves the capability, enforces the tenant match, and egresses. Returns ``{success, message_id?, error?}``; ``success`` is False when the capability is absent/expired or the tenant mismatches — nothing to retry with (by design: a leaked gateway holds zero capability material). """ ...