feat(brahman-handshake): Fase 2 — discovery remoto via DHT por flow type

Tercer paso del plan "el encuentro entre Entes no se restringe a
local". Cuando un Init local acepta una sesion cuya Card declara
outputs, anuncia al DHT (Kademlia, via brahman-net) que el provee
esos flow types. Cualquier nodo conectado al mismo DHT puede
consultar y obtener la lista de PeerId's que sirven el flow.

API nueva en brahman_handshake::network:
- flow_dht_key(flow_name, type_ref) -> [u8; 32]: blake3 hash de
  "brahman-flow|v1|{flow}|{type_canon}". Determinista cross-host.
  Cambiar la canonicalizacion rompe compatibilidad — el prefijo v1
  documenta version del esquema y obliga a bump al modificar.
- announce_outputs(net, card): start_providing por cada flow.output.
  Idempotente, fire-and-forget.
- find_remote_providers(net, flow_name, type_ref) -> Vec<PeerId>:
  query DHT. Lista vacia si nadie anuncia.

Wire en el server:
- ServerConfig gana pub net: Option<Arc<BrahmanNet>>. Si esta set,
  cada Card registrada con outputs se anuncia automaticamente al DHT
  desde register_session. None = server "ciego al DHT".
- Debug manual de ServerConfig (BrahmanNet no es Debug).

Canonicalizacion del TypeRef:
- Primitive { name } -> "prim:{name}"
- Wit { package, interface, name } -> "wit:{package}#{interface_or_empty}#{name}"

Tests: 2 nuevos en tests/network_discovery.rs:
- dht_discovery_finds_remote_provider: 2 nodos, A registra Card con
  flow.output = monad-list:json, B dial-ea a A, B llama
  find_remote_providers y descubre el peer_id de A.
- dht_discovery_negative_unknown_flow: B busca flow inexistente,
  devuelve [] sin colgarse.

Callers actualizados con net: None: tests existentes + ente-zero
(arje aun no expone red; pasar Some(Arc<BrahmanNet>) cuando quiera
publicar al DHT remoto).

Lo que esto desbloquea: un nouser daemon en maquina A puede ser
descubierto por nouser-explorer en maquina B sin conocimiento previo
del peer — solo necesitan compartir DHT (via bootstrap inicial).

Pendiente para Fase 3: trust (firma Ed25519 en Cards remotas) +
stop_providing al cleanup de sesion.
This commit is contained in:
Sergio
2026-05-09 14:37:15 +00:00
parent 73dadbb166
commit 2059af4fb9
9 changed files with 412 additions and 4 deletions
+29 -1
View File
@@ -7,6 +7,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
use brahman_broker::{Broker, Endpoint};
use brahman_card::{Card, ResolvedCard, WitInterface, CARD_SCHEMA_VERSION};
use brahman_net::BrahmanNet;
use tokio::io::{split, AsyncRead, AsyncWrite, WriteHalf};
use tokio::net::{UnixListener, UnixStream};
use tokio::sync::{mpsc, Mutex};
@@ -39,7 +40,7 @@ type LastMatches = Arc<Mutex<HashMap<SessionId, HashMap<String, Endpoint>>>>;
const PUSH_CHANNEL_CAPACITY: usize = 32;
/// Configuración del servidor.
#[derive(Debug, Clone, Default)]
#[derive(Clone, Default)]
pub struct ServerConfig {
/// `true` si el Init está atado al servidor (se reporta en `HelloAck`).
pub init_attached: bool,
@@ -47,6 +48,27 @@ pub struct ServerConfig {
/// `register` tras un Hello aceptado y `unregister` al cerrar la
/// sesión (Farewell o EOF). Si es `None`, el broker no se usa.
pub broker: Option<SharedBroker>,
/// Capa P2P compartida. Si está presente, cada Card registrada
/// con outputs se anuncia automáticamente al DHT vía
/// [`brahman_handshake::network::announce_outputs`], permitiendo
/// que un consumer remoto los descubra con
/// [`brahman_handshake::network::find_remote_providers`]. Si es
/// `None`, el server queda "ciego al DHT" — sólo matchea sesiones
/// locales (lo cual es correcto cuando no hay conectividad o no
/// se desea exponer al exterior).
pub net: Option<Arc<BrahmanNet>>,
}
// Manual Debug porque BrahmanNet no implementa Debug (libp2p Swarm
// no es Debug). Sólo loggeamos los campos relevantes para tracing.
impl std::fmt::Debug for ServerConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ServerConfig")
.field("init_attached", &self.init_attached)
.field("broker", &self.broker.as_ref().map(|_| "<broker>"))
.field("net", &self.net.as_ref().map(|_| "<net>"))
.finish()
}
}
/// Servidor de handshake escuchando en un Unix socket.
@@ -508,6 +530,12 @@ async fn register_session(
.await
.register(session_id, &card, wit.clone());
}
// Si el server tiene net configurado, anunciar los outputs al
// DHT para que peers remotos puedan descubrirlos. Idempotente
// y best-effort — fallos de Kad no propagan al handshake.
if let Some(net) = &config.net {
crate::network::announce_outputs(net, &card);
}
let resolved = match wit {
Some(w) => ResolvedCard::from_conscious(card, w),
None => ResolvedCard::from_agnostic(card),