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,108 @@
|
||||
//! Cliente de handshake. Conecta a un Unix socket y mantiene la sesión.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use brahman_card::{Card, CARD_SCHEMA_VERSION};
|
||||
use thiserror::Error;
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
use crate::codec::{read_frame, write_frame};
|
||||
use crate::messages::{Farewell, Frame, HandshakeError, Hello, HelloAck, Ping, SessionId};
|
||||
|
||||
/// Errores del cliente.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ClientError {
|
||||
#[error("E/S: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
/// El servidor respondió con un error explícito.
|
||||
#[error("servidor: {0}")]
|
||||
Server(#[source] HandshakeError),
|
||||
|
||||
/// El servidor envió un frame que no esperábamos en este punto del protocolo.
|
||||
#[error("frame inesperado: {got}")]
|
||||
UnexpectedFrame { got: &'static str },
|
||||
|
||||
/// La Card que el cliente intentó enviar no pasa su propia validación.
|
||||
#[error("card inválida pre-envío: {0}")]
|
||||
InvalidCard(String),
|
||||
}
|
||||
|
||||
/// Cliente conectado y autenticado. Tras `connect` ya completó el handshake
|
||||
/// y tiene su `SessionId`.
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
stream: UnixStream,
|
||||
session: SessionId,
|
||||
server_info: HelloAck,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Conecta al socket, envía Hello con la Card dada y procesa la respuesta.
|
||||
pub async fn connect(path: impl AsRef<Path>, card: Card) -> Result<Self, ClientError> {
|
||||
// Pre-validamos para fallar local antes de hablar con el servidor.
|
||||
card.validate()
|
||||
.map_err(|e| ClientError::InvalidCard(e.to_string()))?;
|
||||
|
||||
let mut stream = UnixStream::connect(path).await?;
|
||||
let hello = Hello {
|
||||
schema_version: CARD_SCHEMA_VERSION,
|
||||
protocol_version: brahman_card::PROTOCOL_VERSION.to_string(),
|
||||
card,
|
||||
};
|
||||
write_frame(&mut stream, &Frame::Hello(hello)).await?;
|
||||
|
||||
let frame = read_frame(&mut stream).await?;
|
||||
let ack = match frame {
|
||||
Frame::HelloAck(a) => a,
|
||||
Frame::Error(e) => return Err(ClientError::Server(e)),
|
||||
Frame::Hello(_) => return Err(ClientError::UnexpectedFrame { got: "Hello" }),
|
||||
Frame::Ping(_) => return Err(ClientError::UnexpectedFrame { got: "Ping" }),
|
||||
Frame::Pong(_) => return Err(ClientError::UnexpectedFrame { got: "Pong" }),
|
||||
Frame::Farewell(_) => return Err(ClientError::UnexpectedFrame { got: "Farewell" }),
|
||||
};
|
||||
Ok(Self {
|
||||
stream,
|
||||
session: ack.session,
|
||||
server_info: ack,
|
||||
})
|
||||
}
|
||||
|
||||
/// `SessionId` asignado por el servidor.
|
||||
pub fn session(&self) -> SessionId {
|
||||
self.session
|
||||
}
|
||||
|
||||
/// Información del servidor recibida en el handshake.
|
||||
pub fn server_info(&self) -> &HelloAck {
|
||||
&self.server_info
|
||||
}
|
||||
|
||||
/// Envía un Ping y devuelve el timestamp del servidor.
|
||||
pub async fn ping(&mut self) -> Result<u64, ClientError> {
|
||||
write_frame(
|
||||
&mut self.stream,
|
||||
&Frame::Ping(Ping {
|
||||
session: self.session,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
match read_frame(&mut self.stream).await? {
|
||||
Frame::Pong(p) => Ok(p.timestamp_ms),
|
||||
Frame::Error(e) => Err(ClientError::Server(e)),
|
||||
_ => Err(ClientError::UnexpectedFrame { got: "non-pong" }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Cierre cooperativo. Consume el cliente.
|
||||
pub async fn farewell(mut self) -> Result<(), ClientError> {
|
||||
write_frame(
|
||||
&mut self.stream,
|
||||
&Frame::Farewell(Farewell {
|
||||
session: self.session,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user