"""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; in tests it is an in-memory stub (``tests/gateway/relay/stub_connector.py``). This module defines the protocol surface only — no concrete transport. The contract has four concerns: 1. Lifecycle: ``connect`` / ``disconnect``. 2. Handshake: ``handshake`` returns the ``CapabilityDescriptor`` the connector advertises for the platform this adapter fronts. 3. Inbound: ``set_inbound_handler`` registers a callback the transport invokes with each normalized ``MessageEvent`` the connector delivers. 4. Outbound: ``send_outbound`` carries send/edit/typing actions back to the connector; ``get_chat_info`` proxies a chat-info lookup; ``send_interrupt`` routes a mid-turn /stop down the socket that owns the session_key. 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]] @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.""" ... async def send_outbound(self, action: Dict[str, Any]) -> 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``. """ ... 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 (the /stop routing invariant). On the gateway side this is the OUTBOUND direction; the actual task cancellation happens when the connector echoes an interrupt inbound (handled in Task 1.4). """ ... async def send_follow_up(self, action: Dict[str, Any]) -> Dict[str, Any]: """Act on a shared-identity capability bound to a session (A2 outbound). Some platforms hand the connector a credential that acts on the SHARED bot identity (e.g. a Discord interaction follow-up token, valid ~15min). Under A2 that credential NEVER reaches the gateway — the connector stripped it at the edge and bound it in its capability vault keyed by the session. To use it, the gateway issues a SEMANTIC action against the session it is already in; it never names or holds a token. The action dict carries: ``op`` == ``"follow_up"`` ``session_key`` the session whose bound capability to wield ``kind`` the capability kind (e.g. ``"discord.interaction_token"``) ``content`` the message content to send via that capability ``metadata?`` optional extras The connector resolves the real capability (``resolveOutboundCapability`` on its side), enforces the tenant match (tenant B can never wield tenant A's capability), and egresses. Returns ``{success, message_id?, error?}``; ``success`` is False when the capability is absent/expired or the tenant doesn't match — the gateway then has nothing to retry with (by design: a leaked gateway holds zero capability material). """ ...