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:
Generated
+32
-1
@@ -1139,6 +1139,22 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brahman-admin"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"brahman-broker",
|
||||
"brahman-card",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"ulid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brahman-broker"
|
||||
version = "0.1.0"
|
||||
@@ -1176,6 +1192,17 @@ dependencies = [
|
||||
"ulid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brahman-sidecar"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"brahman-card",
|
||||
"brahman-handshake",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bs58"
|
||||
version = "0.5.1"
|
||||
@@ -2772,6 +2799,7 @@ name = "ente-zero"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"brahman-admin",
|
||||
"brahman-broker",
|
||||
"brahman-handshake",
|
||||
"ente-brain",
|
||||
@@ -5785,6 +5813,8 @@ dependencies = [
|
||||
name = "nakui-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"brahman-card",
|
||||
"brahman-sidecar",
|
||||
"petgraph",
|
||||
"rhai",
|
||||
"serde",
|
||||
@@ -5793,6 +5823,7 @@ dependencies = [
|
||||
"surrealdb",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"ulid",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -11440,7 +11471,7 @@ name = "yahweh-shell"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"brahman-card",
|
||||
"brahman-handshake",
|
||||
"brahman-sidecar",
|
||||
"gpui",
|
||||
"notify",
|
||||
"serde",
|
||||
|
||||
@@ -7,6 +7,8 @@ members = [
|
||||
"crates/core/brahman-card",
|
||||
"crates/core/brahman-handshake",
|
||||
"crates/core/brahman-broker",
|
||||
"crates/core/brahman-admin",
|
||||
"crates/shared/brahman-sidecar",
|
||||
"crates/core/ente-card",
|
||||
"crates/core/ente-bus",
|
||||
"crates/core/ente-cas",
|
||||
|
||||
@@ -28,7 +28,7 @@ notify = { workspace = true }
|
||||
|
||||
# Brahman protocol — sidecar thread que se presenta al Init.
|
||||
brahman-card = { path = "../../core/brahman-card" }
|
||||
brahman-handshake = { path = "../../core/brahman-handshake" }
|
||||
brahman-sidecar = { path = "../../shared/brahman-sidecar" }
|
||||
ulid = { workspace = true }
|
||||
|
||||
[[bin]]
|
||||
|
||||
@@ -1,84 +1,19 @@
|
||||
//! Sidecar brahman: yahweh se presenta al Init como módulo `Widget`.
|
||||
//! Card de yahweh-shell + spawn del sidecar brahman compartido.
|
||||
//!
|
||||
//! Vive en un thread aparte con tokio runtime current_thread, desacoplado
|
||||
//! de GPUI. Si el Init no está disponible, loggea y termina — yahweh
|
||||
//! sigue funcionando standalone. Si conecta, mantiene la sesión viva
|
||||
//! con pings periódicos hasta que la GUI termine o el server caiga.
|
||||
//!
|
||||
//! Card declarada:
|
||||
//! - label: `brahman.ui_engine`
|
||||
//! - lifecycle: `Widget`
|
||||
//! - flow.input: `render-data` (json)
|
||||
//! - flow.output: `user-intent` (json)
|
||||
//! - permissions: filesystem read-write (yahweh persiste `layout.json`),
|
||||
//! IPC `wit-v1`.
|
||||
//! La lógica de thread + tokio + ping-loop vive en `brahman-sidecar`;
|
||||
//! aquí sólo declaramos la identidad de yahweh como módulo Widget.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::time::Duration;
|
||||
|
||||
use brahman_card::{
|
||||
Card, Flow, Flows, FsPolicy, IpcPolicy, Lifecycle, Payload, Permissions, Priority, Supervision,
|
||||
TypeRef, CARD_SCHEMA_VERSION,
|
||||
};
|
||||
use brahman_handshake::{client::Client, transport};
|
||||
use ulid::Ulid;
|
||||
|
||||
/// Período entre pings al Init.
|
||||
const PING_INTERVAL: Duration = Duration::from_secs(30);
|
||||
|
||||
/// Spawn del sidecar brahman. No-op si el thread no se puede crear.
|
||||
/// Devuelve inmediatamente; la conexión se establece en background.
|
||||
/// Spawn del sidecar con la Card de yahweh.
|
||||
pub fn spawn() {
|
||||
let result = std::thread::Builder::new()
|
||||
.name("brahman-client".into())
|
||||
.spawn(run_thread);
|
||||
if let Err(e) = result {
|
||||
eprintln!("[brahman] no se pudo spawnear el sidecar: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
fn run_thread() {
|
||||
let rt = match tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.enable_time()
|
||||
.build()
|
||||
{
|
||||
Ok(rt) => rt,
|
||||
Err(e) => {
|
||||
eprintln!("[brahman] tokio runtime falló: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
rt.block_on(run_client());
|
||||
}
|
||||
|
||||
async fn run_client() {
|
||||
let path = transport::default_socket_path();
|
||||
let card = build_card();
|
||||
|
||||
let mut client = match Client::connect(&path, card).await {
|
||||
Ok(c) => {
|
||||
eprintln!(
|
||||
"[brahman] attached: session={} init_attached={} server={}",
|
||||
c.session(),
|
||||
c.server_info().init_attached,
|
||||
c.server_info().server_version
|
||||
);
|
||||
c
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[brahman] no conectado a {} ({e})", path.display());
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
tokio::time::sleep(PING_INTERVAL).await;
|
||||
if let Err(e) = client.ping().await {
|
||||
eprintln!("[brahman] ping falló: {e}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
brahman_sidecar::spawn(build_card());
|
||||
}
|
||||
|
||||
fn build_card() -> Card {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "brahman-admin"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
description = "Brahman — admin API: snapshot del estado del broker (sesiones + matches) por Unix socket, formato JSON."
|
||||
|
||||
[dependencies]
|
||||
brahman-broker = { path = "../brahman-broker" }
|
||||
brahman-card = { path = "../brahman-card" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
ulid = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[[example]]
|
||||
name = "brahman-status"
|
||||
path = "examples/brahman-status.rs"
|
||||
@@ -0,0 +1,55 @@
|
||||
//! `brahman-status` — CLI para inspeccionar el estado del Init.
|
||||
//!
|
||||
//! Conecta al socket admin (default `$XDG_RUNTIME_DIR/brahman-admin.sock`,
|
||||
//! override con `$BRAHMAN_ADMIN_SOCKET`), recibe el snapshot, y lo imprime.
|
||||
|
||||
use brahman_admin::{client, transport};
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let path = transport::default_socket_path();
|
||||
let snap = client::query(&path).await?;
|
||||
|
||||
println!(
|
||||
"Init: server={} protocol={} attached={}",
|
||||
snap.server_version, snap.protocol_version, snap.init_attached
|
||||
);
|
||||
println!();
|
||||
println!("Sessions ({}):", snap.sessions.len());
|
||||
if snap.sessions.is_empty() {
|
||||
println!(" (ninguna)");
|
||||
} else {
|
||||
for s in &snap.sessions {
|
||||
println!(
|
||||
" {} {} lifecycle={:?} priority={:?}",
|
||||
s.session, s.label, s.lifecycle, s.priority
|
||||
);
|
||||
for f in &s.inputs {
|
||||
println!(" in {}: {:?}", f.name, f.ty);
|
||||
}
|
||||
for f in &s.outputs {
|
||||
println!(" out {}: {:?}", f.name, f.ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!();
|
||||
println!("Matches ({}):", snap.matches.len());
|
||||
if snap.matches.is_empty() {
|
||||
println!(" (ninguno)");
|
||||
} else {
|
||||
for m in &snap.matches {
|
||||
let pin_marker = if m.pinned { "📌" } else { " " };
|
||||
println!(
|
||||
" {} {}.{} ← {}.{} via {:?}",
|
||||
pin_marker,
|
||||
m.consumer_label,
|
||||
m.consumer.flow_name,
|
||||
m.producer_label,
|
||||
m.producer.flow_name,
|
||||
m.via
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//! Cliente admin: lee un `StatusSnapshot` desde un socket admin.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use thiserror::Error;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
use crate::snapshot::StatusSnapshot;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AdminError {
|
||||
#[error("E/S: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("respuesta vacía")]
|
||||
Empty,
|
||||
#[error("JSON inválido: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
/// Conecta al socket admin, lee la línea JSON y deserializa.
|
||||
pub async fn query(path: impl AsRef<Path>) -> Result<StatusSnapshot, AdminError> {
|
||||
let stream = UnixStream::connect(path).await?;
|
||||
let mut reader = BufReader::new(stream);
|
||||
let mut line = String::new();
|
||||
let n = reader.read_line(&mut line).await?;
|
||||
if n == 0 {
|
||||
return Err(AdminError::Empty);
|
||||
}
|
||||
let snapshot = serde_json::from_str(&line)?;
|
||||
Ok(snapshot)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//! `brahman-admin` — observabilidad del broker.
|
||||
//!
|
||||
//! Expone un Unix socket separado (no se mezcla con el handshake) en el
|
||||
//! que cada conexión recibe un `StatusSnapshot` JSON y se cierra. Es
|
||||
//! single-shot por conexión: pensado para herramientas como
|
||||
//! `brahman-status`, dashboards y health-checks.
|
||||
//!
|
||||
//! Wire format: una línea JSON por conexión, terminada en `\n`. Esto
|
||||
//! hace trivial inspeccionar con `nc` o `socat` además del cliente
|
||||
//! tipado de este crate.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(rust_2018_idioms)]
|
||||
|
||||
pub mod client;
|
||||
pub mod server;
|
||||
pub mod snapshot;
|
||||
pub mod transport;
|
||||
|
||||
pub use snapshot::StatusSnapshot;
|
||||
|
||||
/// Versión del crate de admin.
|
||||
pub const ADMIN_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
@@ -0,0 +1,107 @@
|
||||
//! Servidor admin: emite un `StatusSnapshot` JSON por conexión y cierra.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use brahman_broker::Broker;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::snapshot::StatusSnapshot;
|
||||
|
||||
/// Configuración del servidor admin.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AdminConfig {
|
||||
/// `true` si el Init está atado al servidor que aloja este admin.
|
||||
pub init_attached: bool,
|
||||
}
|
||||
|
||||
/// Servidor admin escuchando en un Unix socket.
|
||||
pub struct AdminServer {
|
||||
listener: UnixListener,
|
||||
socket_path: PathBuf,
|
||||
broker: Arc<Mutex<Broker>>,
|
||||
config: AdminConfig,
|
||||
}
|
||||
|
||||
impl AdminServer {
|
||||
/// Crea el listener. Si `path` existe, lo elimina (asume socket stale).
|
||||
pub fn bind(
|
||||
path: impl Into<PathBuf>,
|
||||
broker: Arc<Mutex<Broker>>,
|
||||
config: AdminConfig,
|
||||
) -> std::io::Result<Self> {
|
||||
let socket_path = path.into();
|
||||
if socket_path.exists() {
|
||||
std::fs::remove_file(&socket_path)?;
|
||||
}
|
||||
if let Some(parent) = socket_path.parent() {
|
||||
if !parent.as_os_str().is_empty() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
}
|
||||
let listener = UnixListener::bind(&socket_path)?;
|
||||
Ok(Self {
|
||||
listener,
|
||||
socket_path,
|
||||
broker,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn socket_path(&self) -> &Path {
|
||||
&self.socket_path
|
||||
}
|
||||
|
||||
/// Loop de aceptación: cada conexión recibe un snapshot y se cierra.
|
||||
pub async fn run(self) -> std::io::Result<()> {
|
||||
loop {
|
||||
let (stream, _addr) = self.listener.accept().await?;
|
||||
let broker = self.broker.clone();
|
||||
let config = self.config.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_conn(stream, broker, config).await {
|
||||
warn!(error = %e, "admin conn falló");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AdminServer {
|
||||
fn drop(&mut self) {
|
||||
if let Err(e) = std::fs::remove_file(&self.socket_path) {
|
||||
if e.kind() != std::io::ErrorKind::NotFound {
|
||||
warn!(path = %self.socket_path.display(), error = %e, "no se pudo limpiar admin socket");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_conn(
|
||||
mut stream: UnixStream,
|
||||
broker: Arc<Mutex<Broker>>,
|
||||
config: AdminConfig,
|
||||
) -> std::io::Result<()> {
|
||||
let snapshot = build_snapshot(&broker, &config).await;
|
||||
let mut json = serde_json::to_string(&snapshot)?;
|
||||
json.push('\n');
|
||||
stream.write_all(json.as_bytes()).await?;
|
||||
stream.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn build_snapshot(broker: &Arc<Mutex<Broker>>, config: &AdminConfig) -> StatusSnapshot {
|
||||
let b = broker.lock().await;
|
||||
let sessions: Vec<_> = b.cards().cloned().collect();
|
||||
let matches = b.all_matches();
|
||||
StatusSnapshot {
|
||||
server_version: crate::ADMIN_VERSION.to_string(),
|
||||
protocol_version: brahman_card::PROTOCOL_VERSION.to_string(),
|
||||
init_attached: config.init_attached,
|
||||
sessions,
|
||||
matches,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//! Tipos del snapshot que el admin server emite.
|
||||
|
||||
use brahman_broker::{BrokeredCard, Match};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Snapshot completo del estado del Init en un instante.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StatusSnapshot {
|
||||
/// Versión del crate del Init que respondió.
|
||||
pub server_version: String,
|
||||
/// Versión del protocolo brahman.
|
||||
pub protocol_version: String,
|
||||
/// `true` si el Init está atado al servidor.
|
||||
pub init_attached: bool,
|
||||
/// Cards actualmente registradas (sesiones vivas).
|
||||
pub sessions: Vec<BrokeredCard>,
|
||||
/// Matches consumer↔producer derivados del set actual.
|
||||
pub matches: Vec<Match>,
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//! Convenciones de transporte para el socket admin.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Variable de entorno que sobreescribe la ruta del socket admin.
|
||||
pub const SOCKET_ENV: &str = "BRAHMAN_ADMIN_SOCKET";
|
||||
|
||||
/// Nombre del socket admin dentro del runtime dir.
|
||||
pub const SOCKET_NAME: &str = "brahman-admin.sock";
|
||||
|
||||
/// Ruta canónica al socket admin del Init.
|
||||
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")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(std::env::temp_dir);
|
||||
base.join(SOCKET_NAME)
|
||||
}
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use brahman_card::{Card, Flow, Priority, TypeRef};
|
||||
use brahman_card::{Card, Flow, Lifecycle, Priority, TypeRef};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
@@ -60,10 +60,11 @@ pub struct BrokerConfig {
|
||||
}
|
||||
|
||||
/// Vista mínima de una Card que el broker necesita.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BrokeredCard {
|
||||
pub session: SessionId,
|
||||
pub label: String,
|
||||
pub lifecycle: Lifecycle,
|
||||
pub priority: Priority,
|
||||
pub inputs: Vec<Flow>,
|
||||
pub outputs: Vec<Flow>,
|
||||
@@ -74,6 +75,7 @@ impl BrokeredCard {
|
||||
Self {
|
||||
session,
|
||||
label: card.label.clone(),
|
||||
lifecycle: card.lifecycle,
|
||||
priority: card.priority,
|
||||
inputs: card.flow.input.clone(),
|
||||
outputs: card.flow.output.clone(),
|
||||
@@ -82,14 +84,14 @@ impl BrokeredCard {
|
||||
}
|
||||
|
||||
/// Punto extremo de un flujo: qué sesión + nombre del flow dentro de su Card.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct Endpoint {
|
||||
pub session: SessionId,
|
||||
pub flow_name: String,
|
||||
}
|
||||
|
||||
/// Match concreto entre un consumidor y un productor.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Match {
|
||||
pub consumer: Endpoint,
|
||||
pub consumer_label: String,
|
||||
@@ -147,6 +149,11 @@ impl Broker {
|
||||
self.cards.keys().copied()
|
||||
}
|
||||
|
||||
/// Iterador sobre las Cards registradas (vista compartida).
|
||||
pub fn cards(&self) -> impl Iterator<Item = &BrokeredCard> + '_ {
|
||||
self.cards.values()
|
||||
}
|
||||
|
||||
/// Busca el mejor productor para un input específico de un consumidor.
|
||||
///
|
||||
/// Algoritmo:
|
||||
|
||||
@@ -26,6 +26,10 @@ use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use ulid::Ulid;
|
||||
|
||||
// Re-export para que los consumidores no necesiten depender de `ulid`
|
||||
// directamente.
|
||||
pub use ::ulid;
|
||||
|
||||
/// Versión del esquema de la Card.
|
||||
pub const CARD_SCHEMA_VERSION: u16 = 1;
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ ente-echo = { path = "../ente-echo" } # solo para constantes del demo
|
||||
# Brahman protocol — handshake para módulos brahman conscientes
|
||||
brahman-handshake = { path = "../brahman-handshake" }
|
||||
brahman-broker = { path = "../brahman-broker" }
|
||||
brahman-admin = { path = "../brahman-admin" }
|
||||
|
||||
# Runtime / utilidades de PID 1
|
||||
serde = { workspace = true }
|
||||
|
||||
@@ -169,6 +169,29 @@ async fn primordial_loop(
|
||||
}
|
||||
}
|
||||
|
||||
// Brahman admin: socket separado para snapshots de estado (sesiones +
|
||||
// matches del broker). Misma política de degradación grácil.
|
||||
let admin_sock = brahman_admin::transport::default_socket_path();
|
||||
match brahman_admin::server::AdminServer::bind(
|
||||
&admin_sock,
|
||||
brahman_broker.clone(),
|
||||
brahman_admin::server::AdminConfig {
|
||||
init_attached: true,
|
||||
},
|
||||
) {
|
||||
Ok(admin) => {
|
||||
info!(socket = %admin_sock.display(), "brahman admin escuchando");
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = admin.run().await {
|
||||
warn!(?e, "brahman admin server cayó");
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(?e, socket = %admin_sock.display(), "brahman admin deshabilitado");
|
||||
}
|
||||
}
|
||||
|
||||
let mut graph = EnteGraph::new(seed_card);
|
||||
graph.instantiate_seed_dependencies(&graph_tx).await?;
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@ sha2 = "0.10"
|
||||
surrealdb = { version = "2", default-features = false, features = ["kv-mem"] }
|
||||
tokio = { version = "1", features = ["rt", "macros"] }
|
||||
|
||||
# Brahman protocol — presencia ante el Init cuando `nakui run` arranca.
|
||||
brahman-card = { path = "../../../core/brahman-card" }
|
||||
brahman-sidecar = { path = "../../../shared/brahman-sidecar" }
|
||||
ulid = { version = "1" }
|
||||
|
||||
[[bin]]
|
||||
name = "nakui"
|
||||
path = "src/bin/nakui.rs"
|
||||
|
||||
@@ -313,6 +313,10 @@ fn cmd_run(args: &[String]) -> Result<(), CliError> {
|
||||
.unwrap_or_else(|| "<memory>".into()),
|
||||
);
|
||||
|
||||
// Sidecar brahman: nakui se presenta al Init mientras el daemon vive.
|
||||
// No bloquea; si el Init no está, el sidecar termina silenciosamente.
|
||||
brahman_sidecar::spawn(brahman_card_for_nakui());
|
||||
|
||||
let executor = Executor::load_module(&module_dir)
|
||||
.map_err(|e| CliError::Op(format!("load module {}: {}", module_dir.display(), e)))?;
|
||||
let log = EventLog::open(&log_path).map_err(|e| CliError::Op(format!("open log: {}", e)))?;
|
||||
@@ -453,3 +457,59 @@ fn cmd_verify_log(args: &[String]) -> Result<(), CliError> {
|
||||
Err(e) => Err(CliError::Op(format!("verify failed: {}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Card que nakui presenta al Init brahman cuando arranca como daemon.
|
||||
///
|
||||
/// Lifecycle Daemon (proceso largo). Flujos JSON: consume `command`
|
||||
/// (queries del UI), produce `report` (resultados de cómputo). Los
|
||||
/// nombres están escogidos para que el broker pueda matchearlos contra
|
||||
/// `user-intent` / `render-data` de yahweh-shell por compatibilidad de
|
||||
/// tipo (todos `json`).
|
||||
fn brahman_card_for_nakui() -> brahman_card::Card {
|
||||
use brahman_card::{
|
||||
Card, Flow, Flows, FsPolicy, IpcPolicy, Lifecycle, Payload, Permissions, Priority,
|
||||
Supervision, TypeRef, CARD_SCHEMA_VERSION,
|
||||
};
|
||||
use std::collections::BTreeSet;
|
||||
use std::time::Duration;
|
||||
|
||||
Card {
|
||||
schema_version: CARD_SCHEMA_VERSION,
|
||||
id: ulid::Ulid::new(),
|
||||
lineage: None,
|
||||
label: "brahman.nakui_erp".into(),
|
||||
provides: BTreeSet::new(),
|
||||
requires: BTreeSet::new(),
|
||||
payload: Payload::Virtual,
|
||||
supervision: Supervision::Restart {
|
||||
initial: Duration::from_millis(200),
|
||||
max: Duration::from_secs(30),
|
||||
},
|
||||
lifecycle: Lifecycle::Daemon,
|
||||
priority: Priority::Normal,
|
||||
permissions: Permissions {
|
||||
filesystem: FsPolicy::ReadWrite,
|
||||
ipc: IpcPolicy {
|
||||
allow: vec!["wit-v1".into()],
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
flow: Flows {
|
||||
input: vec![Flow {
|
||||
name: "command".into(),
|
||||
ty: TypeRef::Primitive {
|
||||
name: "json".into(),
|
||||
},
|
||||
pin_to: None,
|
||||
}],
|
||||
output: vec![Flow {
|
||||
name: "report".into(),
|
||||
ty: TypeRef::Primitive {
|
||||
name: "json".into(),
|
||||
},
|
||||
pin_to: None,
|
||||
}],
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user