feat: segundo módulo (nakui) + admin API + brahman-status

Dos cosas en una sesión, en el orden discutido:

(1) Segundo módulo brahman vivo: nakui-core
  - crates/modules/nakui/core/Cargo.toml: deps brahman-card,
    brahman-sidecar, ulid.
  - crates/modules/nakui/core/src/bin/nakui.rs: brahman_card_for_nakui()
    construye una Card como Lifecycle::Daemon, Supervision::Restart,
    flow.input "command" (json) + flow.output "report" (json). El
    cmd_run llama brahman_sidecar::spawn antes de levantar el server
    de nakui.

(2) crates/shared/brahman-sidecar (estrena crates/shared/)
  Boilerplate del sidecar extraído (DRY): el thread con tokio current
  thread runtime, conexión vía Client::connect, ping loop. Yahweh y
  nakui ahora consumen este crate. API:
  - spawn(card)                    fire-and-forget
  - spawn_with_handle(config)      con JoinHandle
  Example "presence" útil para demos: módulo dummy con label tomado
  del primer arg que se queda vivo hasta SIGTERM.

(3) crates/core/brahman-admin: observabilidad del broker
  Socket Unix paralelo en \$BRAHMAN_ADMIN_SOCKET (default
  \$XDG_RUNTIME_DIR/brahman-admin.sock). Cada conexión recibe un
  StatusSnapshot JSON line-delimited y se cierra. Compatible con nc/socat.
  - StatusSnapshot { server, protocol, init_attached, sessions, matches }
  - server::AdminServer
  - client::query(path)
  - example "brahman-status" CLI

(4) Wiring de ente-zero
  En primordial_loop, junto al handshake server, ahora también levanta
  AdminServer con misma política de degradación grácil.

(5) brahman-broker: BrokeredCard ahora incluye lifecycle. Endpoint y
  Match derivan Serialize/Deserialize. Nuevo método cards() expone
  iterador de BrokeredCard para que el admin pueda construir snapshots.

(6) brahman-card: re-export pub use ulid::* para que módulos no
  necesiten depender de ulid directamente.

(7) yahweh-shell migrado al sidecar compartido. Su brahman_client.rs
  pasa de 96 a 53 líneas: sólo declara la Card, delega el spawn.

Demo end-to-end:
  $ ente-zero &
  $ presence demo.producer &
  $ presence demo.consumer &
  $ brahman-status

  Init: server=0.1.0 protocol=0.1.0 attached=true
  Sessions (2):
    01KR42TY1J... demo.producer  lifecycle=Daemon  priority=Normal
    01KR42TY1K... demo.consumer  lifecycle=Daemon  priority=Normal
  Matches (2):
    demo.producer.in  ←  demo.consumer.out  via Exact
    demo.consumer.in  ←  demo.producer.out  via Exact

El broker matchea bidireccional por tipo. El admin lo expone.

Tests: 27/27. cargo check --workspace: 0 errores.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-08 15:21:49 +00:00
parent 595f68e252
commit 70a7a0d46d
20 changed files with 627 additions and 76 deletions
+22
View File
@@ -0,0 +1,22 @@
[package]
name = "brahman-sidecar"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "Brahman — sidecar reusable: thread + tokio runtime que mantiene viva la sesión de un módulo contra el Init."
[dependencies]
brahman-card = { path = "../../core/brahman-card" }
brahman-handshake = { path = "../../core/brahman-handshake" }
tokio = { workspace = true }
tracing = { workspace = true }
[dev-dependencies]
tracing-subscriber = { workspace = true }
[[example]]
name = "presence"
path = "examples/presence.rs"
@@ -0,0 +1,69 @@
//! `presence` — módulo brahman dummy para pruebas y demos.
//!
//! Declara una Card mínima con label tomado del primer argumento (default
//! `presence-default`) y mantiene la sesión viva hasta SIGTERM/SIGINT.
//! Útil para poblar el broker con sesiones de prueba.
//!
//! Uso:
//! ```sh
//! cargo run -p brahman-sidecar --example presence -- mi-modulo
//! ```
use std::collections::BTreeSet;
use std::time::Duration;
use brahman_card::{
ulid::Ulid, Card, Flow, Flows, Lifecycle, Payload, Priority, Supervision, TypeRef,
CARD_SCHEMA_VERSION,
};
use brahman_sidecar::{spawn_with_handle, SidecarConfig};
fn main() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info".into()),
)
.init();
let label = std::env::args()
.nth(1)
.unwrap_or_else(|| "presence-default".into());
let card = Card {
schema_version: CARD_SCHEMA_VERSION,
id: Ulid::new(),
label: label.clone(),
payload: Payload::Virtual,
supervision: Supervision::OneShot,
lifecycle: Lifecycle::Daemon,
priority: Priority::Normal,
provides: BTreeSet::new(),
requires: BTreeSet::new(),
flow: Flows {
input: vec![Flow {
name: "in".into(),
ty: TypeRef::Primitive {
name: "json".into(),
},
pin_to: None,
}],
output: vec![Flow {
name: "out".into(),
ty: TypeRef::Primitive {
name: "json".into(),
},
pin_to: None,
}],
},
..Default::default()
};
let _handle = spawn_with_handle(SidecarConfig {
card,
ping_interval: Duration::from_secs(5),
});
eprintln!("presence({label}): sidecar lanzado, durmiendo (Ctrl-C para salir)");
std::thread::park();
}
+109
View File
@@ -0,0 +1,109 @@
//! `brahman-sidecar` — boilerplate del cliente brahman extraído.
//!
//! Cualquier módulo que quiera presentarse al Init brahman pero que tenga
//! su propio runtime (GPUI, current_thread tokio, std-thread loop, etc.)
//! puede llamar [`spawn`] con su [`brahman_card::Card`]. Eso arma un
//! thread aparte con un runtime tokio current_thread, conecta al Init,
//! y mantiene la sesión viva con pings periódicos.
//!
//! Si el Init no está disponible, el thread loggea y termina — el módulo
//! sigue funcionando standalone.
//!
//! Errores de conexión / ping se loggean vía `tracing::warn!`. Si querés
//! capturar la salida del thread (por ejemplo para test), usá
//! [`spawn_with_handle`] que devuelve un `JoinHandle`.
#![forbid(unsafe_code)]
#![warn(rust_2018_idioms)]
use std::thread::JoinHandle;
use std::time::Duration;
use brahman_card::Card;
use brahman_handshake::{client::Client, transport};
use tracing::{info, warn};
/// Período entre pings al Init.
pub const DEFAULT_PING_INTERVAL: Duration = Duration::from_secs(30);
/// Configuración del sidecar.
#[derive(Debug, Clone)]
pub struct SidecarConfig {
/// Card que se presenta al Init.
pub card: Card,
/// Período entre pings.
pub ping_interval: Duration,
}
impl SidecarConfig {
/// Configuración con defaults razonables: ping cada 30s.
pub fn new(card: Card) -> Self {
Self {
card,
ping_interval: DEFAULT_PING_INTERVAL,
}
}
}
/// Spawn fire-and-forget. Devuelve inmediatamente; el handle se descarta.
/// Si el thread no se puede crear (raro), loggea y sigue.
pub fn spawn(card: Card) {
if let Err(e) = spawn_with_handle(SidecarConfig::new(card)) {
warn!(error = %e, "no se pudo spawnear el sidecar brahman");
}
}
/// Spawn devolviendo el `JoinHandle` para tests o cleanup explícito.
pub fn spawn_with_handle(config: SidecarConfig) -> std::io::Result<JoinHandle<()>> {
std::thread::Builder::new()
.name("brahman-sidecar".into())
.spawn(move || run_thread(config))
}
fn run_thread(config: SidecarConfig) {
let rt = match tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()
{
Ok(rt) => rt,
Err(e) => {
warn!(error = %e, "tokio runtime falló");
return;
}
};
rt.block_on(run_client(config));
}
async fn run_client(config: SidecarConfig) {
let path = transport::default_socket_path();
let mut client = match Client::connect(&path, config.card).await {
Ok(c) => {
info!(
target: "brahman_sidecar",
session = %c.session(),
init_attached = c.server_info().init_attached,
server = %c.server_info().server_version,
"attached"
);
c
}
Err(e) => {
warn!(
target: "brahman_sidecar",
error = %e,
socket = %path.display(),
"no conectado"
);
return;
}
};
loop {
tokio::time::sleep(config.ping_interval).await;
if let Err(e) = client.ping().await {
warn!(target: "brahman_sidecar", error = %e, "ping falló — terminando sidecar");
return;
}
}
}