feat(core): brahman-handshake — protocolo runtime Init↔módulo
Crate nuevo en crates/core/brahman-handshake que implementa el handshake real del shared_wit/protocol.wit como wire format Rust↔Rust sobre Unix socket con frames length-prefixed + cuerpo postcard. Componentes: - src/messages.rs: Hello, HelloAck, Ping, Pong, Farewell, HandshakeError y Frame (enum-suma). HandshakeError ya implementa thiserror::Error y cruza el wire. - src/codec.rs: write_frame / read_frame asíncronos con MAX_FRAME_BYTES de 4 MiB. Test interno de roundtrip. - src/server.rs: Server::bind crea el listener en Unix socket; emite ResolvedCard tras validar la Card y devuelve ULID como SessionId. ServerConfig.init_attached se reporta en HelloAck. - src/client.rs: Client::connect hace pre-validación local de la Card (fail fast), envía Hello, parsea HelloAck. ping() y farewell() expuestos. - tests/handshake.rs: 4 tests de integración: * full_handshake_roundtrip — happy path con 3 pings + farewell * rejects_invalid_card_client_side — label vacío rechazado pre-envío * server_rejects_protocol_mismatch — protocol_version 999.0.0 → Error * ping_before_hello_rejected — Ping sin Hello previo → Rejected Limitación conocida: postcard no serializa serde_json::Value (variantes Array/Object con length dinámico). Se removieron por eso los campos `extensions` (Card) y `extra` (Permissions). Forward-compat queda cubierta por schema_version + protocol_version negotiation; si más adelante necesitamos preservar campos JSON desconocidos, irá en un WireCard separado o un envelope. 13/13 tests verdes (brahman-card 8 + brahman-handshake codec 1 + integ 4). cargo check --workspace: 0 errores. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
//! Mensajes del protocolo de handshake.
|
||||
//!
|
||||
//! Todos los mensajes que cruzan el wire son variantes de [`Frame`].
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
/// Identificador de sesión emitido por el servidor en `HelloAck`.
|
||||
pub type SessionId = Ulid;
|
||||
|
||||
/// Saludo inicial del módulo. Lleva la Card completa para que el servidor
|
||||
/// la valide e indexe.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Hello {
|
||||
/// Versión del schema de Card que el cliente sigue.
|
||||
pub schema_version: u16,
|
||||
/// Versión del protocolo handshake del cliente.
|
||||
pub protocol_version: String,
|
||||
/// Tarjeta de Presentación.
|
||||
pub card: brahman_card::Card,
|
||||
}
|
||||
|
||||
/// Respuesta del servidor a un `Hello` aceptado.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HelloAck {
|
||||
/// Versión del crate del servidor.
|
||||
pub server_version: String,
|
||||
/// Versión del protocolo soportada por el servidor.
|
||||
pub protocol_version: String,
|
||||
/// Identificador de sesión asignado.
|
||||
pub session: SessionId,
|
||||
/// `true` si el Init está vinculado al servidor; `false` si el servidor
|
||||
/// corre standalone (modo degradado).
|
||||
pub init_attached: bool,
|
||||
}
|
||||
|
||||
/// Latido del cliente. El servidor responde con [`Pong`] llevando su reloj.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Ping {
|
||||
pub session: SessionId,
|
||||
}
|
||||
|
||||
/// Respuesta a un `Ping` con timestamp del servidor (ms desde UNIX_EPOCH).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Pong {
|
||||
pub timestamp_ms: u64,
|
||||
}
|
||||
|
||||
/// Cierre cooperativo de la sesión por parte del cliente.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Farewell {
|
||||
pub session: SessionId,
|
||||
}
|
||||
|
||||
/// Errores del protocolo emitidos por el servidor.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)]
|
||||
pub enum HandshakeError {
|
||||
#[error("protocolo incompatible: {0}")]
|
||||
ProtocolMismatch(String),
|
||||
#[error("card inválida: {0}")]
|
||||
InvalidCard(String),
|
||||
#[error("schema de card incompatible: cliente={client}, servidor={server}")]
|
||||
SchemaMismatch { client: u16, server: u16 },
|
||||
#[error("sin autorización: {0}")]
|
||||
Unauthorized(String),
|
||||
#[error("capacidad requerida no satisfecha: {0}")]
|
||||
CapabilityUnmet(String),
|
||||
#[error("rechazado: {0}")]
|
||||
Rejected(String),
|
||||
#[error("error interno: {0}")]
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
/// Frame único de wire — discriminada por variante. Cada conexión es un
|
||||
/// stream de frames.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Frame {
|
||||
Hello(Hello),
|
||||
HelloAck(HelloAck),
|
||||
Ping(Ping),
|
||||
Pong(Pong),
|
||||
Farewell(Farewell),
|
||||
Error(HandshakeError),
|
||||
}
|
||||
Reference in New Issue
Block a user