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,72 @@
|
||||
//! Codec de wire: frames length-prefixed con cuerpo postcard.
|
||||
//!
|
||||
//! Cada frame en el stream tiene la forma:
|
||||
//! ```text
|
||||
//! [4 bytes LE: longitud N] [N bytes: postcard(Frame)]
|
||||
//! ```
|
||||
//!
|
||||
//! El `MAX_FRAME_BYTES` evita que un cliente malicioso/buggy reserve memoria
|
||||
//! arbitraria al anunciar un length absurdo.
|
||||
|
||||
use std::io::{Error, ErrorKind, Result};
|
||||
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
|
||||
use crate::messages::Frame;
|
||||
|
||||
/// Tamaño máximo de un frame antes de que el reader rechace la conexión.
|
||||
/// 4 MiB cubre cualquier Card razonable con margen amplio.
|
||||
pub const MAX_FRAME_BYTES: usize = 4 * 1024 * 1024;
|
||||
|
||||
/// Escribe un frame al stream.
|
||||
pub async fn write_frame<W: AsyncWrite + Unpin>(w: &mut W, frame: &Frame) -> Result<()> {
|
||||
let bytes = postcard::to_allocvec(frame)
|
||||
.map_err(|e| Error::new(ErrorKind::InvalidData, format!("postcard encode: {e}")))?;
|
||||
if bytes.len() > MAX_FRAME_BYTES {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!("frame demasiado grande: {} bytes", bytes.len()),
|
||||
));
|
||||
}
|
||||
let len = bytes.len() as u32;
|
||||
w.write_all(&len.to_le_bytes()).await?;
|
||||
w.write_all(&bytes).await?;
|
||||
w.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lee un frame del stream.
|
||||
pub async fn read_frame<R: AsyncRead + Unpin>(r: &mut R) -> Result<Frame> {
|
||||
let mut len_buf = [0u8; 4];
|
||||
r.read_exact(&mut len_buf).await?;
|
||||
let len = u32::from_le_bytes(len_buf) as usize;
|
||||
if len > MAX_FRAME_BYTES {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!("frame anunciado demasiado grande: {len} bytes"),
|
||||
));
|
||||
}
|
||||
let mut buf = vec![0u8; len];
|
||||
r.read_exact(&mut buf).await?;
|
||||
postcard::from_bytes(&buf)
|
||||
.map_err(|e| Error::new(ErrorKind::InvalidData, format!("postcard decode: {e}")))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::messages::{Frame, HandshakeError};
|
||||
|
||||
#[tokio::test]
|
||||
async fn frame_roundtrip() {
|
||||
let frame = Frame::Error(HandshakeError::Rejected("test".into()));
|
||||
let mut buf = Vec::new();
|
||||
write_frame(&mut buf, &frame).await.unwrap();
|
||||
let mut cursor = std::io::Cursor::new(buf);
|
||||
let decoded = read_frame(&mut cursor).await.unwrap();
|
||||
match decoded {
|
||||
Frame::Error(HandshakeError::Rejected(s)) => assert_eq!(s, "test"),
|
||||
_ => panic!("variant mismatch"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user