tawasuyu

tawasuyu/brahman

Rust 332TOML 127JSON 17Markdown 154710 Sutichaykuna

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.

2059af4fb994239718d8967fa3bb93e85d12dc98 · ruwaq Sergio · (5 killa ñawpaq) · tayta 73dadbb16 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 17

crates/core/brahman-handshake/src/network.rs

crates/core/brahman-handshake/src/server.rs

crates/core/brahman-handshake/tests/handshake.rs

crates/core/brahman-handshake/tests/network_discovery.rs

crates/core/brahman-handshake/tests/network_libp2p.rs

crates/core/ente-zero/src/main.rs

9 qillqakuna · +412 −4

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e95a324..ca484ee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,57 @@ ratio/diff ver `git show <sha>`.
 
 ## 2026-05-09
 
+### feat(brahman-handshake): Fase 2 — discovery remoto vía DHT por flow type
+Tercer paso del plan "el encuentro entre Entes no se restringe a
+local". Cuando un Init local acepta una sesión cuya Card declara
+outputs, anuncia al DHT (Kademlia, vía `brahman-net`) que él 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}"`. Determinístico cross-host.
+  Cambiar la canonicalización rompe compatibilidad — el prefijo `v1`
+  documenta la versión del esquema y obliga a bump al modificar.
+- `announce_outputs(net, card)`: llama `start_providing` en el DHT
+  por cada `Flow` en `card.flow.output`. Idempotente, fire-and-forget.
+- `find_remote_providers(net, flow_name, type_ref) -> Vec<PeerId>`:
+  query DHT por la key derivada. Lista vacía si nadie anuncia o si
+  la query no resuelve dentro del timeout interno de Kad.
+
+Wire en el server:
+- `ServerConfig` gana `pub net: Option<Arc<BrahmanNet>>`. Si está set,
+  cada Card registrada con outputs se anuncia automáticamente al DHT
+  desde `register_session`. `None` = server "ciego al DHT" (correcto
+  cuando no hay conectividad o el operador no quiere exponer).
+- `ServerConfig` ahora tiene `Debug` manual (BrahmanNet no implementa
+  Debug; loggeamos sólo presencia/ausencia).
+
+Canonicalización 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`: dos nodos, A registra Card
+  con `flow.output = monad-list:json`, B dial-ea a A y descubre el
+  `peer_id` de A vía `find_remote_providers`. Asserts contains.
+- `dht_discovery_negative_unknown_flow`: B busca un flow que nadie
+  anunció, devuelve lista vacía sin colgarse.
+
+Lo que esto desbloquea:
+- Un `nouser daemon` corriendo en máquina A puede ser descubierto por
+  un `nouser-explorer` en máquina B sin conocimiento previo del peer
+  — sólo necesitan compartir DHT (vía bootstrap inicial).
+- La cadena completa "explorer → daemon → llm-provider" puede cruzar
+  máquinas, no sólo procesos.
+
+Lo que queda para Fase 3 (trust):
+- Cards remotas se aceptan hoy sin verificación. Para producción se
+  necesita firma Ed25519 sobre la Card y verificación antes de
+  aceptar el Hello remoto. Local sigue confiando en SO_PEERCRED.
+- Stop-providing al cleanup de sesión (hoy records DHT viven hasta
+  TTL ~24h aunque la sesión cierre).
+
 ### feat(brahman-handshake): Fase 1 — handshake brahman sobre stream libp2p
 Segundo paso del plan "el encuentro entre Entes no se restringe a
 local". El protocolo brahman (Hello / HelloAck / Ping / Pong /
diff --git a/Cargo.lock b/Cargo.lock
index 15aeb83..258a2f8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1199,6 +1199,7 @@ name = "brahman-handshake"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "blake3",
  "brahman-broker",
  "brahman-card",
  "brahman-net",
diff --git a/crates/core/brahman-handshake/Cargo.toml b/crates/core/brahman-handshake/Cargo.toml
index b931377..057a003 100644
--- a/crates/core/brahman-handshake/Cargo.toml
+++ b/crates/core/brahman-handshake/Cargo.toml
@@ -12,6 +12,7 @@ description = "Brahman — handshake runtime Init↔módulo. Local sobre Unix so
 brahman-card = { path = "../brahman-card" }
 brahman-broker = { path = "../brahman-broker" }
 brahman-net = { path = "../../shared/brahman-net" }
+blake3 = { workspace = true }
 futures = { workspace = true }
 serde = { workspace = true }
 postcard = { workspace = true }
diff --git a/crates/core/brahman-handshake/src/network.rs b/crates/core/brahman-handshake/src/network.rs
index d032062..c1af3a4 100644
--- a/crates/core/brahman-handshake/src/network.rs
+++ b/crates/core/brahman-handshake/src/network.rs
@@ -43,11 +43,11 @@
 
 use std::sync::Arc;
 
-use brahman_card::{Card, WitInterface};
+use brahman_card::{Card, TypeRef, WitInterface};
 use brahman_net::{BrahmanNet, OpenStreamError, PeerId, Stream, StreamProtocol};
 use futures::StreamExt;
 use tokio_util::compat::{Compat, FuturesAsyncReadCompatExt};
-use tracing::warn;
+use tracing::{debug, warn};
 
 use crate::client::{Client, ClientError};
 use crate::server::Server;
@@ -138,3 +138,92 @@ pub async fn connect_libp2p(
     let client = Client::connect_with_stream(stream.compat(), card, wit).await?;
     Ok(client)
 }
+
+// =====================================================================
+// Discovery remoto via DHT — Fase 2
+// =====================================================================
+//
+// Cuando un Ente registra una Card con outputs en el Init local, el
+// Init anuncia al DHT (`net.start_providing(key)`) bajo una key
+// derivada de `(flow_name, TypeRef)`. Cualquier nodo conectado al
+// mismo DHT puede consultar `find_remote_providers(flow_name, type)`
+// y obtener la lista de `PeerId`s que dijeron proveer ese flow.
+//
+// La key es **estable y libre de colisiones** entre versiones del
+// monorepo: usa blake3 sobre un canon textual `brahman-flow|{name}|{type_canon}`.
+// Cambiar la canonicalización rompe el discovery cross-version, así
+// que cualquier modificación requiere bump de versión documentado.
+
+/// Prefijo de namespace para todas las keys DHT del subprotocolo
+/// brahman. Discrimina contra otros usos del mismo DHT (sync minga,
+/// futuros) — protege contra colisiones accidentales.
+const FLOW_KEY_PREFIX: &str = "brahman-flow|v1|";
+
+/// Canonicaliza un `TypeRef` a string estable. Cambios aquí rompen
+/// la compatibilidad de discovery cross-version; bump documentado
+/// en `FLOW_KEY_PREFIX` al modificar.
+fn canonicalize_type(t: &TypeRef) -> String {
+    match t {
+        TypeRef::Primitive { name } => format!("prim:{}", name),
+        TypeRef::Wit {
+            package,
+            interface,
+            name,
+        } => format!(
+            "wit:{}#{}#{}",
+            package,
+            interface.as_deref().unwrap_or(""),
+            name
+        ),
+    }
+}
+
+/// Deriva la key del DHT para un `(flow_name, type_ref)` específico.
+/// blake3-32B determinístico — la misma tupla en cualquier máquina
+/// produce la misma key.
+pub fn flow_dht_key(flow_name: &str, type_ref: &TypeRef) -> [u8; 32] {
+    let canon = format!(
+        "{}{}|{}",
+        FLOW_KEY_PREFIX,
+        flow_name,
+        canonicalize_type(type_ref)
+    );
+    *blake3::hash(canon.as_bytes()).as_bytes()
+}
+
+/// Anuncia al DHT que este nodo provee cada output flow declarado
+/// en `card`. Llamarlo tras `register_session` propaga la
+/// disponibilidad a todos los peers que comparten DHT con éste.
+///
+/// Idempotente: re-anunciar la misma key actualiza el TTL del record
+/// en el DHT. Best-effort: si `start_providing` falla por falta de
+/// peers cercanos (DHT vacío), el record vive en el store local
+/// hasta que llegue una conexión.
+pub fn announce_outputs(net: &BrahmanNet, card: &Card) {
+    for flow in &card.flow.output {
+        let key = flow_dht_key(&flow.name, &flow.ty);
+        debug!(
+            target: "brahman_handshake::network",
+            flow = %flow.name,
+            "announce_output → DHT"
+        );
+        net.start_providing(&key);
+    }
+}
+
+/// Consulta el DHT por peers que han anunciado proveer el flow
+/// `(flow_name, type_ref)`. Devuelve la lista resuelta de `PeerId`s.
+/// Lista vacía si nadie anuncia, si la query timeout-ea, o si el
+/// DHT no ha encontrado providers.
+///
+/// Para cada `PeerId` devuelto, el caller puede luego dial-ar al
+/// peer (a sus addrs conocidas vía Identify) y abrir un sub-handshake
+/// remoto con [`connect_libp2p`].
+pub async fn find_remote_providers(
+    net: &BrahmanNet,
+    flow_name: &str,
+    type_ref: &TypeRef,
+) -> Vec<PeerId> {
+    let key = flow_dht_key(flow_name, type_ref);
+    net.find_providers(&key).await
+}
diff --git a/crates/core/brahman-handshake/src/server.rs b/crates/core/brahman-handshake/src/server.rs
index 11511f6..61a363f 100644
--- a/crates/core/brahman-handshake/src/server.rs
+++ b/crates/core/brahman-handshake/src/server.rs
@@ -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),
diff --git a/crates/core/brahman-handshake/tests/handshake.rs b/crates/core/brahman-handshake/tests/handshake.rs
index ac20ff9..23b7a05 100644
--- a/crates/core/brahman-handshake/tests/handshake.rs
+++ b/crates/core/brahman-handshake/tests/handshake.rs
@@ -58,7 +58,7 @@ fn sock_path(name: &str) -> std::path::PathBuf {
 #[tokio::test]
 async fn full_handshake_roundtrip() {
     let path = sock_path("happy");
-    let server = Server::bind(&path, ServerConfig { init_attached: true, broker: None }).unwrap();
+    let server = Server::bind(&path, ServerConfig { init_attached: true, broker: None, net: None }).unwrap();
 
     let session_handle = tokio::spawn({
         async move {
@@ -193,6 +193,7 @@ async fn broker_registers_and_unregisters_with_session() {
         ServerConfig {
             init_attached: false,
             broker: Some(broker.clone()),
+            net: None,
         },
     )
     .unwrap();
@@ -235,6 +236,7 @@ async fn broker_matches_two_live_modules() {
         ServerConfig {
             init_attached: false,
             broker: Some(broker.clone()),
+            net: None,
         },
     )
     .unwrap();
@@ -310,6 +312,7 @@ async fn match_event_pushed_on_producer_arrival() {
         ServerConfig {
             init_attached: false,
             broker: Some(broker.clone()),
+            net: None,
         },
     )
     .unwrap();
diff --git a/crates/core/brahman-handshake/tests/network_discovery.rs b/crates/core/brahman-handshake/tests/network_discovery.rs
new file mode 100644
index 0000000..16b8dce
--- /dev/null
+++ b/crates/core/brahman-handshake/tests/network_discovery.rs
@@ -0,0 +1,230 @@
+//! Test E2E de Fase 2: discovery remoto vía DHT.
+//!
+//! Pipeline:
+//! 1. **Provider node (A)**: arma server con `BrahmanNet` configurado;
+//!    listen TCP; un cliente local registra una Card con un output
+//!    flow. El server llama `announce_outputs` automáticamente, lo
+//!    que hace `start_providing` en el DHT bajo la key derivada del
+//!    flow.
+//! 2. **Consumer node (B)**: arma su propio `BrahmanNet`; dial-ea al
+//!    multiaddr del provider para que ambos se conozcan vía Identify
+//!    (esto popula sus respectivos routing tables de Kademlia).
+//! 3. **B llama `find_remote_providers(flow_name, type)`**: la query
+//!    DHT propaga vía Kad, y eventually el provider responde con su
+//!    `PeerId`.
+//! 4. **Verificación**: el `PeerId` que B descubre coincide con el
+//!    de A.
+//!
+//! Notas:
+//! - Kademlia replication factor por defecto es 20; con 2 nodos no
+//!   hay propagación material — A es el único provider, B llega a A
+//!   vía la conexión directa establecida en step 2 y obtiene el record
+//!   del store local de A.
+//! - El test usa flow `monad-list:json` por familiaridad (es el flow
+//!   real que `nouser daemon` declara). Sirve también como prueba de
+//!   que el sistema completo (daemon + DHT) funcionaría con cero
+//!   cambios en la Card.
+
+use std::collections::BTreeSet;
+use std::sync::Arc;
+use std::time::Duration;
+
+use brahman_broker::{Broker, BrokerConfig};
+use brahman_card::{
+    ulid::Ulid, Card, CardKind, Flow, Flows, Lifecycle, Payload, Priority, Supervision, TypeRef,
+    CARD_SCHEMA_VERSION,
+};
+use brahman_handshake::network::{find_remote_providers, run_libp2p_accept_loop};
+use brahman_handshake::server::{Server, ServerConfig};
+use brahman_net::{BrahmanNet, Multiaddr, Protocol};
+use tempfile::TempDir;
+use tokio::sync::Mutex;
+
+fn provider_card(label: &str, flow_name: &str, type_name: &str) -> Card {
+    Card {
+        schema_version: CARD_SCHEMA_VERSION,
+        id: Ulid::new(),
+        label: label.into(),
+        provides: BTreeSet::new(),
+        requires: BTreeSet::new(),
+        permissions: Default::default(),
+        soma: Default::default(),
+        payload: Payload::Virtual,
+        supervision: Supervision::Delegate,
+        lifecycle: Lifecycle::Daemon,
+        priority: Priority::Normal,
+        kind: CardKind::Ente,
+        flow: Flows {
+            input: vec![],
+            output: vec![Flow {
+                name: flow_name.into(),
+                ty: TypeRef::Primitive {
+                    name: type_name.into(),
+                },
+                pin_to: None,
+            }],
+        },
+        ..Default::default()
+    }
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+async fn dht_discovery_finds_remote_provider() {
+    // ---- Node A (provider): server + libp2p net + Card con output ----
+    let tmp = TempDir::new().unwrap();
+    let a_unix = tmp.path().join("a.sock");
+
+    let a_broker = Arc::new(Mutex::new(Broker::new(BrokerConfig::default())));
+    let a_net = Arc::new(BrahmanNet::new().unwrap());
+    let a_peer = a_net.peer_id;
+
+    let a_server = Arc::new(
+        Server::bind(
+            &a_unix,
+            ServerConfig {
+                init_attached: true,
+                broker: Some(a_broker.clone()),
+                net: Some(a_net.clone()), // ← clave Fase 2: anuncia al DHT
+            },
+        )
+        .unwrap(),
+    );
+
+    let listen_addr: Multiaddr = "/ip4/127.0.0.1/tcp/0".parse().unwrap();
+    let a_addr = a_net.listen(listen_addr).await;
+    let mut a_full_addr = a_addr.clone();
+    a_full_addr.push(Protocol::P2p(a_peer));
+
+    tokio::spawn(run_libp2p_accept_loop(a_server.clone(), a_net.clone()));
+
+    // Unix accept loop: necesario para que Client::connect al socket
+    // local no cuelgue (Server no se auto-accepta; el caller arma el
+    // loop). Cada session entrante corre en su propia task.
+    {
+        let s = a_server.clone();
+        tokio::spawn(async move {
+            loop {
+                match s.accept_one().await {
+                    Ok(session) => {
+                        tokio::spawn(async move {
+                            let _ = session.handle().await;
+                        });
+                    }
+                    Err(_) => break,
+                }
+            }
+        });
+    }
+
+    // Registrar la Card local en A con un flow output.
+    let card = provider_card("test.engine_remote", "monad-list", "json");
+    let mut local_client = brahman_handshake::client::Client::connect(&a_unix, card)
+        .await
+        .expect("registro local en A");
+
+    // ---- Node B (consumer): otro net que dial-a a A ----
+    let b_net = BrahmanNet::new().unwrap();
+    b_net.dial(a_full_addr.clone());
+
+    // Esperar a que la conexión se establezca y Identify popule el
+    // routing table de Kad. En localhost con 2 peers, ~250ms es de
+    // sobra; sumamos margen para CI.
+    tokio::time::sleep(Duration::from_millis(500)).await;
+
+    // ---- Discovery: B busca providers de "monad-list:json" ----
+    let providers = find_remote_providers(
+        &b_net,
+        "monad-list",
+        &TypeRef::Primitive {
+            name: "json".into(),
+        },
+    )
+    .await;
+
+    assert!(
+        providers.contains(&a_peer),
+        "B debería descubrir a A vía DHT. Encontrados: {:?}, esperado: {}",
+        providers,
+        a_peer
+    );
+
+    // Sanidad: el cliente local sigue vivo durante todo el test (lo
+    // que mantiene la Card registrada y por tanto el record DHT vivo).
+    local_client.farewell().await.ok();
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+async fn dht_discovery_negative_unknown_flow() {
+    // Mismo setup que el test happy-path, pero B busca un flow que A
+    // NO ofrece. Debe devolver lista vacía dentro del timeout
+    // razonable (no colgarse).
+    let tmp = TempDir::new().unwrap();
+    let a_unix = tmp.path().join("a.sock");
+    let a_broker = Arc::new(Mutex::new(Broker::new(BrokerConfig::default())));
+    let a_net = Arc::new(BrahmanNet::new().unwrap());
+    let a_peer = a_net.peer_id;
+
+    let a_server = Arc::new(
+        Server::bind(
+            &a_unix,
+            ServerConfig {
+                init_attached: true,
+                broker: Some(a_broker),
+                net: Some(a_net.clone()),
+            },
+        )
+        .unwrap(),
+    );
+
+    let a_addr = a_net.listen("/ip4/127.0.0.1/tcp/0".parse().unwrap()).await;
+    let mut a_full = a_addr.clone();
+    a_full.push(Protocol::P2p(a_peer));
+
+    tokio::spawn(run_libp2p_accept_loop(a_server.clone(), a_net.clone()));
+
+    // Unix accept loop: necesario para que Client::connect al socket
+    // local no cuelgue (Server no se auto-accepta; el caller arma el
+    // loop). Cada session entrante corre en su propia task.
+    {
+        let s = a_server.clone();
+        tokio::spawn(async move {
+            loop {
+                match s.accept_one().await {
+                    Ok(session) => {
+                        tokio::spawn(async move {
+                            let _ = session.handle().await;
+                        });
+                    }
+                    Err(_) => break,
+                }
+            }
+        });
+    }
+
+    let card = provider_card("test.engine_other", "monad-list", "json");
+    let mut local = brahman_handshake::client::Client::connect(&a_unix, card)
+        .await
+        .unwrap();
+
+    let b_net = BrahmanNet::new().unwrap();
+    b_net.dial(a_full);
+    tokio::time::sleep(Duration::from_millis(500)).await;
+
+    // Buscamos un flow que NADIE anunció.
+    let providers = find_remote_providers(
+        &b_net,
+        "flow-que-no-existe",
+        &TypeRef::Primitive {
+            name: "json".into(),
+        },
+    )
+    .await;
+
+    assert!(
+        providers.is_empty(),
+        "no debería haber providers para un flow inexistente, got: {:?}",
+        providers
+    );
+
+    local.farewell().await.ok();
+}
diff --git a/crates/core/brahman-handshake/tests/network_libp2p.rs b/crates/core/brahman-handshake/tests/network_libp2p.rs
index 5b94812..94a6527 100644
--- a/crates/core/brahman-handshake/tests/network_libp2p.rs
+++ b/crates/core/brahman-handshake/tests/network_libp2p.rs
@@ -55,6 +55,7 @@ async fn libp2p_handshake_roundtrip() {
             ServerConfig {
                 init_attached: true,
                 broker: Some(broker.clone()),
+                net: None,
             },
         )
         .unwrap(),
diff --git a/crates/core/ente-zero/src/main.rs b/crates/core/ente-zero/src/main.rs
index db5c526..5c9b7f1 100644
--- a/crates/core/ente-zero/src/main.rs
+++ b/crates/core/ente-zero/src/main.rs
@@ -164,6 +164,10 @@ async fn primordial_loop(
         brahman_handshake::server::ServerConfig {
             init_attached: true,
             broker: Some(brahman_broker.clone()),
+            // Fase 2: el Init aún no expone red P2P por default. Cuando
+            // arje quiera publicar Cards al DHT remoto, pasar aquí un
+            // `Some(Arc<BrahmanNet>)` con la malla configurada.
+            net: None,
         },
     ) {
         Ok(server) => {