feat: Phase D-3 + D-4 — service_socket en Card, providers coexisten

Cierra el ciclo del swap automático Nous mock↔real:

- brahman-card: Card.service_socket: Option<PathBuf> y espejo en
  WireCard. Path del data plane (distinto al Init). Cualquier
  consumer que matchee con esta Card conecta directo, sin discovery
  extra.
- brahman-broker: BrokeredCard propaga service_socket. Sin
  participación en matching — sólo metadata.
- brahman-handshake::MatchEvent: nuevo campo
  producer_service_socket. Server lo busca en BrokeredCard al emitir
  Available.
- nouser-nous::transport: provider_socket_path(provider: &str)
  devuelve nouser-nous-{provider}.sock por default. Mock y real
  coexisten en sockets distintos (Phase D-4). default_socket_path()
  conserva el comportamiento single-provider.
- Mock declara nouser-nous-mock.sock; real declara
  nouser-nous-real.sock. La Card se construye DESPUÉS del bind.
- brahman-status imprime "socket:" por sesión cuando está presente.

Validación end-to-end:
  $ ente-zero & nouser-nous-mock & nouser-nous-real &
  $ ls /run/user/1001/nouser-nous-*.sock
    nouser-nous-mock.sock
    nouser-nous-real.sock
  $ brahman-status
  Sessions (2):
    [ente]  nouser.nous_real
        socket: /run/user/1001/nouser-nous-real.sock
    [ente]  nouser.nous_mock
        socket: /run/user/1001/nouser-nous-mock.sock

Pendiente (no crítico): nouser-core attract --remote usa todavía
NOUSER_NOUS_SOCKET hardcoded. Siguiente paso: subscribirse al
MatchEvent del broker y usar producer_service_socket directo, así
BRAHMAN_BROKER_CONTEXT=test/prod swapea provider sin tocar al
consumer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-08 19:38:23 +00:00
parent 794884a90f
commit 5edc912ed8
9 changed files with 134 additions and 23 deletions
+14 -9
View File
@@ -45,13 +45,10 @@ const MODEL_ID: &str = "mock-pseudo-32d";
async fn main() -> std::io::Result<()> {
init_tracing();
// 1. Sidecar al brahman-init.
let card = build_card();
info!(label = %card.label, "publicando Card al brahman-init");
brahman_sidecar::spawn(card);
// 2. Bind del socket Nous.
let sock_path = transport::default_socket_path();
// 1. Resolver socket del data-plane ANTES de armar la Card, para
// declararlo en `Card.service_socket` y que los consumidores lo
// descubran vía MatchEvent.
let sock_path = transport::provider_socket_path("mock");
if sock_path.exists() {
std::fs::remove_file(&sock_path)?;
}
@@ -61,6 +58,11 @@ async fn main() -> std::io::Result<()> {
let listener = UnixListener::bind(&sock_path)?;
info!(socket = %sock_path.display(), "nouser-nous-mock escuchando");
// 2. Sidecar al brahman-init con la Card que declara el socket.
let card = build_card(sock_path.clone());
info!(label = %card.label, "publicando Card al brahman-init");
brahman_sidecar::spawn(card);
// 3. Accept loop.
loop {
let (stream, _addr) = listener.accept().await?;
@@ -84,8 +86,10 @@ fn init_tracing() {
}
/// Card que el mock anuncia al brahman-init. Es kind=Ente (un proceso),
/// con flujos JSON y bias de prioridad para contexto `test`.
fn build_card() -> Card {
/// con flujos JSON, bias de prioridad para contexto `test`, y el socket
/// data-plane declarado en `service_socket` (consumidores lo reciben
/// directo en el `MatchEvent::Available`).
fn build_card(service_socket: std::path::PathBuf) -> Card {
let mut priority_contexts = BTreeMap::new();
priority_contexts.insert(
"test".into(),
@@ -105,6 +109,7 @@ fn build_card() -> Card {
lifecycle: Lifecycle::Daemon,
priority: Priority::Normal,
kind: CardKind::Ente,
service_socket: Some(service_socket),
flow: Flows {
input: vec![Flow {
name: FLOW_EMBED_REQUEST.into(),
+11 -8
View File
@@ -65,13 +65,9 @@ async fn main() -> std::io::Result<()> {
--features embeddings para activar el modelo)"
);
// 1. Sidecar al brahman-init (mismo patrón que el mock).
let card = build_card();
info!(label = %card.label, mode = MODEL_ID, "publicando Card al brahman-init");
brahman_sidecar::spawn(card);
// 2. Bind del socket Nous (mismo path que el mock — son swappable).
let sock_path = transport::default_socket_path();
// 1. Resolver socket del data-plane (default `nouser-nous-real.sock`,
// distinto del mock para coexistir).
let sock_path = transport::provider_socket_path("real");
if sock_path.exists() {
std::fs::remove_file(&sock_path)?;
}
@@ -81,6 +77,11 @@ async fn main() -> std::io::Result<()> {
let listener = UnixListener::bind(&sock_path)?;
info!(socket = %sock_path.display(), "nouser-nous-real escuchando");
// 2. Sidecar al brahman-init con Card declarando el socket.
let card = build_card(sock_path.clone());
info!(label = %card.label, mode = MODEL_ID, "publicando Card al brahman-init");
brahman_sidecar::spawn(card);
// 3. Inicializar el modelo (sólo en modo embeddings).
#[cfg(feature = "embeddings")]
let backend = embeddings::Backend::init().map_err(|e| {
@@ -128,7 +129,8 @@ fn init_tracing() {
/// Card que real-nous anuncia. Idéntica al mock excepto por:
/// - label distinto (`nouser.nous_real`) para que coexistan en el broker.
/// - `priority_contexts.prod = +1` (gana en contexto prod).
fn build_card() -> Card {
/// - `service_socket` propio para que clientes lo descubran directo.
fn build_card(service_socket: std::path::PathBuf) -> Card {
let mut priority_contexts = BTreeMap::new();
priority_contexts.insert(
"prod".into(),
@@ -147,6 +149,7 @@ fn build_card() -> Card {
lifecycle: Lifecycle::Daemon,
priority: Priority::Normal,
kind: CardKind::Ente,
service_socket: Some(service_socket),
flow: Flows {
input: vec![Flow {
name: FLOW_EMBED_REQUEST.into(),
+19 -5
View File
@@ -117,18 +117,32 @@ pub mod transport {
/// Variable de entorno para sobreescribir la ruta del socket.
pub const SOCKET_ENV: &str = "NOUSER_NOUS_SOCKET";
/// Nombre por default del socket dentro del runtime dir.
/// Nombre genérico del socket cuando hay un solo proveedor.
pub const SOCKET_NAME: &str = "nouser-nous.sock";
/// Ruta canónica al socket de Nous.
/// Ruta canónica al socket cuando un único proveedor está activo
/// (consumidores que no quieren elegir).
pub fn default_socket_path() -> PathBuf {
if let Ok(p) = std::env::var(SOCKET_ENV) {
return PathBuf::from(p);
}
let base = std::env::var_os("XDG_RUNTIME_DIR")
runtime_base().join(SOCKET_NAME)
}
/// Ruta default para un proveedor identificado (`"mock"`, `"real"`,
/// etc). Permite que mock y real coexistan sin clash de socket.
/// `NOUSER_NOUS_SOCKET` igual override esta función si está set.
pub fn provider_socket_path(provider: &str) -> PathBuf {
if let Ok(p) = std::env::var(SOCKET_ENV) {
return PathBuf::from(p);
}
runtime_base().join(format!("nouser-nous-{}.sock", provider))
}
fn runtime_base() -> PathBuf {
std::env::var_os("XDG_RUNTIME_DIR")
.map(PathBuf::from)
.unwrap_or_else(std::env::temp_dir);
base.join(SOCKET_NAME)
.unwrap_or_else(std::env::temp_dir)
}
}