⎇ a97f6b98f39e1ab9d4982ce0fda907a19c1ffa83
crates/core/brahman-handshake/src/messages.rs
Historia
Quién tocó qué
crudo
feat(brahman-handshake): ListMatches endpoint + timeline en broker-explorer
1//! Mensajes del protocolo de handshake.
2//!
3//! Todos los mensajes que cruzan el wire son variantes de [`Frame`].
4
5use std::path::PathBuf;
6
7use brahman_broker::MatchStrategy;
8use brahman_card::{TypeRef, WireCard, WitInterface};
9use serde::{Deserialize, Serialize};
10use ulid::Ulid;
11
12/// Identificador de sesión emitido por el servidor en `HelloAck`.
13pub type SessionId = Ulid;
14
15/// Saludo inicial del módulo. Lleva la Card en forma `WireCard`
16/// (postcard-friendly: sin extensiones JSON arbitrarias). El servidor
17/// la convierte a `Card` para uso interno. Opcionalmente, una
18/// `WitInterface` ya extraída — si está presente, el módulo es
19/// "consciente" y el server lo registra como `ResolvedCard::from_conscious`.
20///
21/// **Firma (Fase 3, trust remoto)**: el campo `signature` es
22/// obligatorio para conexiones libp2p (donde el server exige que la
23/// public key derive al `peer_id` autenticado por Noise) y opcional
24/// para Unix socket (donde SO_PEERCRED del kernel ya provee
25/// autenticación). La firma cubre los bytes postcard de
26/// `(WireCard, Option<WitInterface>)` — ver
27/// [`HelloSignature::sign_payload`].
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct Hello {
30 /// Versión del schema de Card que el cliente sigue.
31 pub schema_version: u16,
32 /// Versión del protocolo handshake del cliente.
33 pub protocol_version: String,
34 /// Tarjeta de Presentación, proyectada al wire.
35 pub card: WireCard,
36 /// Interfaz WIT extraída por el cliente (típicamente con
37 /// `brahman-card-wit`). `None` si el módulo es agnóstico.
38 #[serde(default)]
39 pub wit: Option<WitInterface>,
40 /// Firma Ed25519 sobre `(card, wit)`. Requerida para conexiones
41 /// remotas (libp2p); opcional para Unix socket. Ver módulo
42 /// [`super::signature`] para construcción y verificación.
43 #[serde(default)]
44 pub signature: Option<HelloSignature>,
45 /// Cert opcional que vincula la session keypair (la que firma el
46 /// Hello) a una **identity master** estable. Si está presente,
47 /// la política de admisión se evalúa contra el `master_peer_id`
48 /// derivado del cert — no contra el session peer_id. Esto permite
49 /// rotar la session sin invalidar las allowlists remotas.
50 ///
51 /// Ver [`super::identity::SessionCert`] para shape y semantics.
52 /// Si es `None`, fallback al modelo de Fase 3: la política
53 /// evalúa el session peer_id directamente.
54 #[serde(default)]
55 pub identity_cert: Option<crate::identity::SessionCert>,
56}
57
58/// Firma de un Hello. La `public_key` viaja en el formato canónico
59/// libp2p (protobuf) — el verificador la decodifica y compara su
60/// `peer_id` derivado con la identidad libp2p autenticada por Noise.
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
62pub struct HelloSignature {
63 /// Public key del firmante, encoded como `libp2p::identity::PublicKey::encode_protobuf()`.
64 pub public_key: Vec<u8>,
65 /// Bytes de la firma Ed25519 sobre el payload canonical.
66 pub signature: Vec<u8>,
67}
68
69/// Respuesta del servidor a un `Hello` aceptado.
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct HelloAck {
72 /// Versión del crate del servidor.
73 pub server_version: String,
74 /// Versión del protocolo soportada por el servidor.
75 pub protocol_version: String,
76 /// Identificador de sesión asignado.
77 pub session: SessionId,
78 /// `true` si el Init está vinculado al servidor; `false` si el servidor
79 /// corre standalone (modo degradado).
80 pub init_attached: bool,
81}
82
83/// Latido del cliente. El servidor responde con [`Pong`] llevando su reloj.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct Ping {
86 pub session: SessionId,
87}
88
89/// Respuesta a un `Ping` con timestamp del servidor (ms desde UNIX_EPOCH).
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct Pong {
92 pub timestamp_ms: u64,
93}
94
95/// Cierre cooperativo de la sesión por parte del cliente.
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct Farewell {
98 pub session: SessionId,
99}
100
101/// Errores del protocolo emitidos por el servidor.
102#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)]
103pub enum HandshakeError {
104 #[error("protocolo incompatible: {0}")]
105 ProtocolMismatch(String),
106 #[error("card inválida: {0}")]
107 InvalidCard(String),
108 #[error("schema de card incompatible: cliente={client}, servidor={server}")]
109 SchemaMismatch { client: u16, server: u16 },
110 #[error("sin autorización: {0}")]
111 Unauthorized(String),
112 #[error("capacidad requerida no satisfecha: {0}")]
113 CapabilityUnmet(String),
114 #[error("rechazado: {0}")]
115 Rejected(String),
116 #[error("error interno: {0}")]
117 Internal(String),
118}
119
120/// Notificación push del server al consumer: un match disponible o perdido.
121///
122/// El server emite `Available` cuando un productor empieza a satisfacer un
123/// `flow.input` del consumer (ya sea porque el productor acaba de
124/// registrarse, o porque cambió el match anterior). Emite `Lost` cuando
125/// el productor previo dejó de satisfacer el input (desregistro o
126/// cambio de match).
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct MatchEvent {
129 pub kind: MatchEventKind,
130 /// Nombre del input del consumer al que aplica el evento.
131 pub consumer_flow: String,
132 /// Sesión y label del productor (en `Lost` puede ser nil/vacío).
133 pub producer_session: SessionId,
134 pub producer_label: String,
135 pub producer_flow: String,
136 /// Tipo del flujo matcheado.
137 pub ty: TypeRef,
138 /// Estrategia que ganó (relevante en `Available`).
139 pub via: MatchStrategy,
140 /// `true` si fue resuelto por `pin_to`.
141 pub pinned: bool,
142 /// Socket de servicio (data plane) que declaró el productor.
143 /// Si está presente, el consumer puede conectar directo sin
144 /// pasar por discovery adicional. `None` si el productor no
145 /// declaró service_socket en su Card.
146 #[serde(default)]
147 pub producer_service_socket: Option<PathBuf>,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
151#[serde(rename_all = "kebab-case")]
152pub enum MatchEventKind {
153 Available,
154 Lost,
155}
156
157/// Pedido de listado de sesiones activas registradas en el broker. La
158/// `session` es el id propio del que pregunta — el server lo valida
159/// contra la sesión actual de la conexión, mismo patrón que `Ping`.
160///
161/// Pensado para herramientas de observabilidad (broker-explorer y
162/// CLIs de diagnóstico). No expone secrets: sólo metadata pública
163/// que el módulo ya anunció en su `Hello`.
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct ListSessions {
166 pub session: SessionId,
167}
168
169/// Una entrada en la respuesta a `ListSessions`. Slim por diseño —
170/// el observer arma la UI con esto sin tener que abrir conexiones
171/// adicionales por sesión.
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct SessionEntry {
174 pub session: SessionId,
175 /// Label declarado en `WireCard.label` — el "nombre humano" del
176 /// módulo.
177 pub label: String,
178 /// Versión del schema de Card que el módulo declaró.
179 pub schema_version: u16,
180 /// Nombres de los `flow.output` que la Card declara producir.
181 pub outputs: Vec<String>,
182 /// Nombres de los `flow.input` que la Card declara consumir.
183 pub inputs: Vec<String>,
184 /// `true` si el módulo se anunció como "consciente" (trajo
185 /// `WitInterface` extraída en el Hello).
186 pub conscious: bool,
187}
188
189/// Respuesta a `ListSessions`. El orden no está garantizado — los
190/// clientes que necesiten estabilidad pueden ordenar por `session`
191/// (Ulid es ordenable temporal).
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct SessionList {
194 pub entries: Vec<SessionEntry>,
195}
196
197/// Pedido del listado de matches actuales del broker. La `session`
198/// se valida igual que `ListSessions`. Si el server no tiene broker
199/// configurado, devuelve la lista vacía (no es un error — refleja
200/// que no hay matching activo).
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct ListMatches {
203 pub session: SessionId,
204}
205
206/// Respuesta a `ListMatches` con el snapshot de matches consumidor↔productor
207/// actualmente computados por el broker.
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct MatchList {
210 pub matches: Vec<brahman_broker::Match>,
211}
212
213/// Frame único de wire — discriminada por variante. Cada conexión es un
214/// stream de frames.
215///
216/// Direcciones:
217/// - Cliente → Server: `Hello`, `Ping`, `Farewell`, `ListSessions`,
218/// `ListMatches`.
219/// - Server → Cliente: `HelloAck`, `Pong`, `Error`, `MatchEvent`,
220/// `SessionList`, `MatchList`.
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub enum Frame {
223 Hello(Hello),
224 HelloAck(HelloAck),
225 Ping(Ping),
226 Pong(Pong),
227 Farewell(Farewell),
228 Error(HandshakeError),
229 MatchEvent(MatchEvent),
230 ListSessions(ListSessions),
231 SessionList(SessionList),
232 ListMatches(ListMatches),
233 MatchList(MatchList),
234}