Pausa: 11 crates del fractal Ente #0 con cerebro completo
PID 1 boot + bus interno autenticado + cerebro KCL/Rust: - 6 lib crates de infra (card, bus, cas, kernel, soma, wasm, snapshot) - ente-brain: motor de reglas O(1), observer Shannon, cristalización, audit hash-chain, persistencia rules.k, Prometheus /metrics - KCL schemas card.k + rule.k como gramática autoritativa - compat-logind D-Bus, ente-echo demo provider, ente-zero PID 1 - 22 tests OK, ~3.8k LOC Rust + ~300 LOC KCL Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
//! Glue entre el bucle primordial y `ente-brain`.
|
||||
//!
|
||||
//! Tres responsabilidades:
|
||||
//! 1. Traducir eventos del grafo (`GraphEvent`) a `ente_brain::EventKind`
|
||||
//! + `SubjectInfo` para el observador y el motor.
|
||||
//! 2. Implementar `ActionSink` para que las Acciones del cerebro tengan
|
||||
//! un canal de salida hacia el grafo (Spawn → SpawnRequest, etc.).
|
||||
//! 3. Encapsular el snapshot de SubjectInfo desde el grafo sin filtrar
|
||||
//! detalles internos al cerebro.
|
||||
|
||||
use crate::events::GraphEvent;
|
||||
use crate::graph::EnteGraph;
|
||||
use ente_brain::{ActionSink, EventKind as BrainEventKind, SubjectInfo};
|
||||
use ente_card::Capability;
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::warn;
|
||||
use ulid::Ulid;
|
||||
|
||||
/// Traduce un GraphEvent a (EventKind, SubjectInfo) para alimentar el cerebro.
|
||||
///
|
||||
/// Devuelve `None` para eventos puramente internos del bus (Response, Close)
|
||||
/// que no son interesantes para reglas o estadística.
|
||||
pub fn graph_event_to_brain<'a>(
|
||||
evt: &'a GraphEvent,
|
||||
graph: &EnteGraph,
|
||||
) -> Option<(BrainEventKind, SubjectInfo)> {
|
||||
match evt {
|
||||
GraphEvent::EnteDied { id, .. } => {
|
||||
Some((BrainEventKind::EnteDied, subject_info_for(graph, *id)))
|
||||
}
|
||||
GraphEvent::SpawnRequest { card, .. } => {
|
||||
// El "sujeto" del spawn es el child que va a nacer.
|
||||
let info = SubjectInfo {
|
||||
id: Some(card.id),
|
||||
label: Some(card.label.clone()),
|
||||
capabilities: card.provides.iter().cloned().collect(),
|
||||
};
|
||||
Some((BrainEventKind::EnteSpawned, info))
|
||||
}
|
||||
GraphEvent::BusRequest { from, request, .. } => {
|
||||
let kind = match request {
|
||||
ente_bus::BusRequest::Announce { .. } => BrainEventKind::BusAnnounce,
|
||||
ente_bus::BusRequest::Invoke { cap, .. } => {
|
||||
BrainEventKind::BusInvokeOf(cap.clone())
|
||||
}
|
||||
_ => BrainEventKind::BusInvoke,
|
||||
};
|
||||
let info = match from {
|
||||
Some(id) => subject_info_for(graph, *id),
|
||||
None => SubjectInfo::default(),
|
||||
};
|
||||
Some((kind, info))
|
||||
}
|
||||
GraphEvent::CapabilityRequested { from, .. } => {
|
||||
Some((BrainEventKind::BusInvoke, subject_info_for(graph, *from)))
|
||||
}
|
||||
// Responses, ConnClosed, Shutdown — irrelevantes para reglas
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn subject_info_for(graph: &EnteGraph, id: Ulid) -> SubjectInfo {
|
||||
// Acceso de sólo lectura — usamos el método público lookup_pid + cards
|
||||
// virtuales en el grafo. Si el Ente no existe (ya disuelto), info vacía.
|
||||
if let Some(card) = graph.card_for(&id) {
|
||||
SubjectInfo {
|
||||
id: Some(id),
|
||||
label: Some(card.label.clone()),
|
||||
capabilities: card.provides.iter().cloned().collect(),
|
||||
}
|
||||
} else {
|
||||
SubjectInfo { id: Some(id), label: None, capabilities: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
/// `ActionSink` que enruta acciones del cerebro al bucle primordial.
|
||||
pub struct GraphSink {
|
||||
pub graph_tx: mpsc::Sender<GraphEvent>,
|
||||
pub requester: Ulid,
|
||||
}
|
||||
|
||||
impl ActionSink for GraphSink {
|
||||
fn spawn(&self, card_blob: &str) {
|
||||
// El blob es JSON de EntityCard.
|
||||
match serde_json::from_str::<ente_card::EntityCard>(card_blob) {
|
||||
Ok(card) => {
|
||||
let evt = GraphEvent::SpawnRequest { card, requester: self.requester };
|
||||
if self.graph_tx.try_send(evt).is_err() {
|
||||
warn!("brain spawn: graph_tx lleno o cerrado");
|
||||
}
|
||||
}
|
||||
Err(e) => warn!(?e, "brain spawn: blob no parseable como EntityCard JSON"),
|
||||
}
|
||||
}
|
||||
|
||||
fn invoke(&self, target_cap: Capability, blob: Vec<u8>) {
|
||||
// Sin BusClient en proceso — el sink registra la intención. Una mejora
|
||||
// futura: spawn un BusClient::connect + call. Por ahora log estructurado.
|
||||
warn!(?target_cap, blob_len = blob.len(), "brain invoke: no bus client en glue (TODO)");
|
||||
}
|
||||
|
||||
fn notify(&self, target_id: Ulid, message: &str) {
|
||||
warn!(%target_id, %message, "brain notify: no implementado en glue");
|
||||
}
|
||||
|
||||
fn inhibit(&self, reason: &str) {
|
||||
warn!(%reason, "brain inhibit: no implementado en glue");
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper para que el grafo exponga la Card de un Ente vivo. Lo añadimos como
|
||||
/// trait extension porque graph::EnteGraph mantiene `incarnated` privado.
|
||||
pub trait GraphCardLookup {
|
||||
fn card_for(&self, id: &Ulid) -> Option<&ente_card::EntityCard>;
|
||||
}
|
||||
|
||||
impl GraphCardLookup for EnteGraph {
|
||||
fn card_for(&self, id: &Ulid) -> Option<&ente_card::EntityCard> {
|
||||
// Acceso vía método público que añadiremos en graph/mod.rs.
|
||||
self.peek_card(id)
|
||||
}
|
||||
}
|
||||
|
||||
// Eliminar el campo `_unused` que rustc puede quejarse — placeholder para
|
||||
// evitar warning si algún field queda sin uso.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Deserialize)]
|
||||
struct _Touch {}
|
||||
@@ -0,0 +1,143 @@
|
||||
//! Listener del bus interno. Vive en PID 1, acepta conexiones de Entes hijos,
|
||||
//! extrae credenciales del kernel vía SO_PEERCRED, y enruta cada request al
|
||||
//! grafo. Conexión bidireccional: el grafo puede *empujar* requests hacia
|
||||
//! una conexión registrada (forwarding de Invoke al proveedor).
|
||||
//!
|
||||
//! ## Por qué bidireccional
|
||||
//!
|
||||
//! Un Ente que provee `Capability::Endpoint` debe poder *recibir* invokes
|
||||
//! sin abrir más sockets. Después de Announce, el grafo guarda el lado de
|
||||
//! escritura de su conexión y lo usa para forwardear.
|
||||
|
||||
use crate::events::GraphEvent;
|
||||
use ente_bus::{read_frame, write_frame, BusMessage, BusPayload, BusResponse, PeerCreds};
|
||||
use nix::sys::socket::{getsockopt, sockopt::PeerCredentials};
|
||||
use std::path::PathBuf;
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use tracing::{error, info, trace, warn};
|
||||
use ulid::Ulid;
|
||||
|
||||
pub fn default_socket_path() -> PathBuf {
|
||||
if let Ok(p) = std::env::var(ente_bus::ENV_BUS_SOCK) {
|
||||
return p.into();
|
||||
}
|
||||
let runtime = std::env::var("XDG_RUNTIME_DIR")
|
||||
.unwrap_or_else(|_| std::env::var("TMPDIR").unwrap_or_else(|_| "/tmp".into()));
|
||||
let user = std::env::var("USER").unwrap_or_else(|_| "ente".into());
|
||||
format!("{runtime}/ente-bus-{user}.sock").into()
|
||||
}
|
||||
|
||||
pub fn spawn_bus(path: PathBuf, graph_tx: mpsc::Sender<GraphEvent>) -> anyhow::Result<PathBuf> {
|
||||
let _ = std::fs::remove_file(&path);
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
let listener = UnixListener::bind(&path)?;
|
||||
info!(path = %path.display(), "bus interno escuchando");
|
||||
|
||||
let path_returned = path.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match listener.accept().await {
|
||||
Ok((stream, _addr)) => {
|
||||
let tx = graph_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_conn(stream, tx).await {
|
||||
warn!(?e, "bus connection ended");
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
error!(?e, "bus accept failed, listener cerrando");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(path_returned)
|
||||
}
|
||||
|
||||
async fn handle_conn(stream: UnixStream, graph_tx: mpsc::Sender<GraphEvent>) -> anyhow::Result<()> {
|
||||
// SO_PEERCRED: el kernel adjunta pid/uid/gid al socket en connect/accept.
|
||||
// No-falsificable desde el cliente.
|
||||
let creds = getsockopt(&stream, PeerCredentials)
|
||||
.map_err(|e| anyhow::anyhow!("getsockopt PEERCRED: {e}"))?;
|
||||
let peer = PeerCreds {
|
||||
pid: creds.pid(),
|
||||
uid: creds.uid(),
|
||||
gid: creds.gid(),
|
||||
};
|
||||
trace!(?peer, "bus conn aceptada");
|
||||
|
||||
let (mut reader, mut writer) = stream.into_split();
|
||||
let (out_tx, mut out_rx) = mpsc::channel::<BusMessage>(64);
|
||||
|
||||
// Writer task: única vía de escritura al socket. Multiplexa entre
|
||||
// respuestas a peticiones del cliente y forwards iniciados por el grafo.
|
||||
let writer_task = tokio::spawn(async move {
|
||||
while let Some(msg) = out_rx.recv().await {
|
||||
if let Err(e) = write_frame(&mut writer, &msg).await {
|
||||
warn!(?e, "bus writer falló, terminando");
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut announced_id: Option<Ulid> = None;
|
||||
let result: anyhow::Result<()> = (async {
|
||||
loop {
|
||||
let msg = match read_frame(&mut reader).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
trace!(?e, "bus conn read terminó");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
match msg.payload {
|
||||
BusPayload::Request(req) => {
|
||||
let is_announce = matches!(req, ente_bus::BusRequest::Announce { .. });
|
||||
let (reply_tx, reply_rx) = oneshot::channel();
|
||||
if graph_tx.send(GraphEvent::BusRequest {
|
||||
peer,
|
||||
from: msg.from,
|
||||
request: req,
|
||||
outbound: out_tx.clone(),
|
||||
reply: reply_tx,
|
||||
}).await.is_err() {
|
||||
warn!("graph cerrado, terminando bus connection");
|
||||
return Ok(());
|
||||
}
|
||||
let response = reply_rx.await.unwrap_or_else(|_| {
|
||||
BusResponse::Error("graph dropped reply channel".into())
|
||||
});
|
||||
if is_announce && matches!(response, BusResponse::Ok) {
|
||||
// Auth del Announce ya fue verificada por el grafo;
|
||||
// memorizamos para cleanup en cierre.
|
||||
announced_id = msg.from;
|
||||
}
|
||||
let out = BusMessage {
|
||||
from: None,
|
||||
seq: msg.seq,
|
||||
payload: BusPayload::Response(response),
|
||||
};
|
||||
if out_tx.send(out).await.is_err() { return Ok(()); }
|
||||
}
|
||||
BusPayload::Response(resp) => {
|
||||
// Respuesta a un Invoke que el grafo forwardeó a este peer.
|
||||
let _ = graph_tx.send(GraphEvent::BusResponse {
|
||||
seq: msg.seq,
|
||||
response: resp,
|
||||
}).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}).await;
|
||||
|
||||
if let Some(id) = announced_id {
|
||||
let _ = graph_tx.send(GraphEvent::BusConnClosed { ente_id: Some(id) }).await;
|
||||
}
|
||||
writer_task.abort();
|
||||
result
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
//! Eventos internos del bucle primordial. Todo cambio de estado del fractal
|
||||
//! pasa por aquí — la única vía de mutación del grafo desde tasks externas.
|
||||
|
||||
use ente_bus::{BusMessage, BusRequest, BusResponse, PeerCreds};
|
||||
use ente_card::{Capability, EntityCard};
|
||||
use nix::sys::signal::Signal;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use ulid::Ulid;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GraphEvent {
|
||||
EnteDied { id: Ulid, status: ExitStatus },
|
||||
CapabilityRequested {
|
||||
from: Ulid,
|
||||
cap: Capability,
|
||||
reply: oneshot::Sender<CapabilityGrant>,
|
||||
},
|
||||
SpawnRequest { card: EntityCard, requester: Ulid },
|
||||
/// Request del bus interno. `peer` es no-falsificable (kernel-injected
|
||||
/// via SO_PEERCRED). `from` es la identidad reclamada por el cliente —
|
||||
/// el grafo la verifica contra `peer.pid`.
|
||||
BusRequest {
|
||||
peer: PeerCreds,
|
||||
from: Option<Ulid>,
|
||||
request: BusRequest,
|
||||
outbound: mpsc::Sender<BusMessage>,
|
||||
reply: oneshot::Sender<BusResponse>,
|
||||
},
|
||||
/// Response a un Invoke forwardeado por el grafo a un proveedor.
|
||||
/// `seq` debe coincidir con una entry en pending_invokes.
|
||||
BusResponse { seq: u64, response: BusResponse },
|
||||
/// Cliente del bus cerró su conexión. Si había anunciado identidad,
|
||||
/// el grafo retira esa conexión del registry.
|
||||
BusConnClosed { ente_id: Option<Ulid> },
|
||||
Shutdown { reason: ShutdownReason },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExitStatus {
|
||||
Exit(i32),
|
||||
Killed(Signal),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ShutdownReason {
|
||||
SeedRequested,
|
||||
Signal(Signal),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CapabilityGrant {
|
||||
Granted { token: u64 },
|
||||
NoProvider,
|
||||
Denied { reason: &'static str },
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
//! Bus mediator: integración de `EnteGraph` con el bus interno.
|
||||
//!
|
||||
//! Responsabilidades:
|
||||
//! - Auth de Announce (verificar identidad reclamada contra SO_PEERCRED)
|
||||
//! - Registro de conexiones (`bus_connections` indexado por Ulid)
|
||||
//! - Forwarding de Invokes a proveedores
|
||||
//! - Tracking de invokes en vuelo (`pending_invokes` por seq)
|
||||
//! - Cleanup en cierre de conexión
|
||||
|
||||
use super::{EnteGraph, SERVER_SEQ_FLAG};
|
||||
use ente_bus::{BusMessage, BusPayload, BusRequest, BusResponse, EnteInfo, PeerCreds};
|
||||
use ente_card::Capability;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use tracing::{debug, info, warn};
|
||||
use ulid::Ulid;
|
||||
|
||||
/// Operaciones que requieren identidad verificada en el bus.
|
||||
///
|
||||
/// Sólo `Announce`: establece la entrada en `bus_connections` para forwarding
|
||||
/// y debe ser no-falsificable. Invoke, ListEntes y power-mgmt se aceptan
|
||||
/// anonymous — políticas por capacidad se aplican aguas abajo, no aquí.
|
||||
fn requires_auth(req: &BusRequest) -> bool {
|
||||
matches!(req, BusRequest::Announce { .. })
|
||||
}
|
||||
|
||||
impl EnteGraph {
|
||||
pub async fn on_bus_request(
|
||||
&mut self,
|
||||
peer: PeerCreds,
|
||||
from: Option<Ulid>,
|
||||
request: BusRequest,
|
||||
outbound: mpsc::Sender<BusMessage>,
|
||||
reply: oneshot::Sender<BusResponse>,
|
||||
) {
|
||||
// ---- Auth: kernel-injected SO_PEERCRED vs identidad reclamada ----
|
||||
let from_authenticated = match from {
|
||||
None => None,
|
||||
Some(claimed) => {
|
||||
let expected = self.incarnated.get(&claimed).and_then(|i| i.pid);
|
||||
match expected {
|
||||
Some(p) if p.as_raw() == peer.pid => Some(claimed),
|
||||
Some(p) => {
|
||||
warn!(
|
||||
claimed = %claimed, expected_pid = p.as_raw(),
|
||||
actual_pid = peer.pid,
|
||||
"identity mismatch — rechazando request"
|
||||
);
|
||||
let _ = reply.send(BusResponse::Error("identity mismatch".into()));
|
||||
return;
|
||||
}
|
||||
None => {
|
||||
warn!(?claimed, peer_pid = peer.pid, "Ente desconocido reclamando identidad");
|
||||
let _ = reply.send(BusResponse::Error("unknown ente claimed".into()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if requires_auth(&request) && from_authenticated.is_none() {
|
||||
let _ = reply.send(BusResponse::Error("auth required for this request".into()));
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Dispatch ----
|
||||
match request {
|
||||
BusRequest::Announce { capabilities } => {
|
||||
let id = from_authenticated.expect("auth-required guarantees Some");
|
||||
let label = self.incarnated.get(&id).map(|i| i.card.label.clone())
|
||||
.unwrap_or_else(|| "anónimo".into());
|
||||
info!(%id, %label, ?capabilities, peer_pid = peer.pid, "Announce autenticado");
|
||||
self.bus_connections.insert(id, outbound);
|
||||
let _ = reply.send(BusResponse::Ok);
|
||||
}
|
||||
BusRequest::ListEntes => {
|
||||
let entes = self.incarnated.values()
|
||||
.map(|i| EnteInfo {
|
||||
id: i.card.id,
|
||||
label: i.card.label.clone(),
|
||||
provides: i.card.provides.iter().cloned().collect(),
|
||||
pid: i.pid.map(|p| p.as_raw()),
|
||||
})
|
||||
.collect();
|
||||
let _ = reply.send(BusResponse::Entes(entes));
|
||||
}
|
||||
BusRequest::PowerOff { interactive } => {
|
||||
info!(?from_authenticated, interactive, peer_pid = peer.pid, "PowerOff via bus");
|
||||
let _ = reply.send(BusResponse::Ok);
|
||||
}
|
||||
BusRequest::Reboot { interactive } => {
|
||||
info!(?from_authenticated, interactive, "Reboot via bus");
|
||||
let _ = reply.send(BusResponse::Ok);
|
||||
}
|
||||
BusRequest::Suspend { interactive } => {
|
||||
info!(?from_authenticated, interactive, "Suspend via bus");
|
||||
let _ = reply.send(BusResponse::Ok);
|
||||
}
|
||||
BusRequest::Hibernate { interactive } => {
|
||||
info!(?from_authenticated, interactive, "Hibernate via bus");
|
||||
let _ = reply.send(BusResponse::Ok);
|
||||
}
|
||||
BusRequest::Invoke { cap, blob } => {
|
||||
self.forward_invoke(from_authenticated, cap, blob, reply).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enruta un Invoke al proveedor real de la capacidad. Aloca un seq
|
||||
/// server-side, registra el reply oneshot en `pending_invokes`, y empuja
|
||||
/// el request por la conexión del proveedor.
|
||||
async fn forward_invoke(
|
||||
&mut self,
|
||||
from: Option<Ulid>,
|
||||
cap: Capability,
|
||||
blob: Vec<u8>,
|
||||
reply: oneshot::Sender<BusResponse>,
|
||||
) {
|
||||
let provider_id = match self.pick_invokable_provider(&cap) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
let _ = reply.send(BusResponse::Error(format!("sin proveedor invokable para {cap:?}")));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let outbound = match self.bus_connections.get(&provider_id) {
|
||||
Some(o) => o.clone(),
|
||||
None => {
|
||||
let _ = reply.send(BusResponse::Error("proveedor no conectado al bus".into()));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let seq = self.alloc_invoke_seq();
|
||||
self.pending_invokes.insert(seq, reply);
|
||||
debug!(?from, ?cap, ?provider_id, seq, blob_len = blob.len(), "forwardeando Invoke");
|
||||
|
||||
let msg = BusMessage {
|
||||
from: None,
|
||||
seq,
|
||||
payload: BusPayload::Request(BusRequest::Invoke { cap, blob }),
|
||||
};
|
||||
if outbound.send(msg).await.is_err() {
|
||||
if let Some(orig) = self.pending_invokes.remove(&seq) {
|
||||
let _ = orig.send(BusResponse::Error("conn del proveedor cerrada".into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pick_invokable_provider(&self, cap: &Capability) -> Option<Ulid> {
|
||||
// Sólo proveedores con conexión al bus pueden recibir forwards.
|
||||
// El propio Ente #0 está en `providers` para varias caps pero no
|
||||
// debe recibir forwards — se filtra implícitamente porque la Semilla
|
||||
// no tiene conexión al bus.
|
||||
self.providers.get(cap)?
|
||||
.iter()
|
||||
.find(|id| self.bus_connections.contains_key(id))
|
||||
.copied()
|
||||
}
|
||||
|
||||
pub(in crate::graph) fn alloc_invoke_seq(&mut self) -> u64 {
|
||||
self.next_invoke_seq = self.next_invoke_seq.wrapping_add(1);
|
||||
SERVER_SEQ_FLAG | self.next_invoke_seq
|
||||
}
|
||||
|
||||
pub async fn on_bus_response(&mut self, seq: u64, response: BusResponse) {
|
||||
if let Some(orig) = self.pending_invokes.remove(&seq) {
|
||||
let _ = orig.send(response);
|
||||
} else {
|
||||
warn!(seq, "Response sin pending invoke");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn on_bus_conn_closed(&mut self, ente_id: Option<Ulid>) {
|
||||
if let Some(id) = ente_id {
|
||||
self.bus_connections.remove(&id);
|
||||
// No revocamos providers — la capacidad sigue declarada en su
|
||||
// Card. Sólo perdimos el canal de invocación.
|
||||
debug!(%id, "bus connection cerrada");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//! Mediación de capabilities: emisión y revocación de tokens.
|
||||
//!
|
||||
//! El Init no fuerza políticas — sólo verifica que el proveedor existe y
|
||||
//! emite tokens. Las políticas reales (quién puede pedir qué, rate limits,
|
||||
//! audit) se aplican en capas superiores.
|
||||
|
||||
use super::{EnteGraph, GrantedCapability};
|
||||
use crate::events::CapabilityGrant;
|
||||
use ente_card::Capability;
|
||||
use tokio::sync::oneshot;
|
||||
use ulid::Ulid;
|
||||
|
||||
impl EnteGraph {
|
||||
pub async fn mediate_capability(
|
||||
&mut self,
|
||||
from: Ulid,
|
||||
cap: Capability,
|
||||
reply: oneshot::Sender<CapabilityGrant>,
|
||||
) {
|
||||
let grant = match self.providers.get(&cap).and_then(|s| s.iter().next().copied()) {
|
||||
None => CapabilityGrant::NoProvider,
|
||||
Some(provider) => {
|
||||
let token = self.next_token;
|
||||
self.next_token += 1;
|
||||
self.grants.insert(token, GrantedCapability {
|
||||
cap: cap.clone(), provider, holder: from,
|
||||
});
|
||||
CapabilityGrant::Granted { token }
|
||||
}
|
||||
};
|
||||
let _ = reply.send(grant);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//! Device registry: mantiene el índice de dispositivos del kernel presentes,
|
||||
//! traduce uevents en cambios de `Capability::Device { class }`.
|
||||
|
||||
use super::EnteGraph;
|
||||
use crate::events::GraphEvent;
|
||||
use ente_card::{Capability, DeviceClass};
|
||||
use ente_kernel::{UAction, UEvent};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
impl EnteGraph {
|
||||
pub async fn on_uevent(&mut self, evt: UEvent, _tx: &mpsc::Sender<GraphEvent>) {
|
||||
let class = match &evt.device_class {
|
||||
Some(c) => c.clone(),
|
||||
None => return, // subsystems sin DeviceClass mapeada — ignoramos.
|
||||
};
|
||||
match evt.action {
|
||||
UAction::Add | UAction::Bind | UAction::Online => {
|
||||
let was_first = self.devices_of_class(&class) == 0;
|
||||
self.devices.insert(evt.devpath.clone(), evt.clone());
|
||||
if was_first {
|
||||
// Primera instancia de la clase → la registramos como
|
||||
// capacidad disponible. El "proveedor" virtual es el
|
||||
// Ente #0 (kernel surface).
|
||||
let cap = Capability::Device { class: class.clone() };
|
||||
self.providers.entry(cap).or_default().insert(self.seed.id);
|
||||
info!(?class, devpath = %evt.devpath, "device capability disponible");
|
||||
}
|
||||
}
|
||||
UAction::Remove | UAction::Unbind | UAction::Offline => {
|
||||
self.devices.remove(&evt.devpath);
|
||||
if self.devices_of_class(&class) == 0 {
|
||||
let cap = Capability::Device { class: class.clone() };
|
||||
if let Some(set) = self.providers.get_mut(&cap) {
|
||||
set.remove(&self.seed.id);
|
||||
}
|
||||
let revoked: Vec<u64> = self.grants.iter()
|
||||
.filter(|(_, g)| g.cap == cap)
|
||||
.map(|(t, _)| *t)
|
||||
.collect();
|
||||
for t in revoked {
|
||||
self.grants.remove(&t);
|
||||
}
|
||||
warn!(?class, "última instancia removida — capacidad revocada");
|
||||
}
|
||||
}
|
||||
UAction::Change | UAction::Move => {
|
||||
self.devices.insert(evt.devpath.clone(), evt);
|
||||
debug!(?class, "device modified");
|
||||
}
|
||||
UAction::Unknown => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn devices_of_class(&self, class: &DeviceClass) -> usize {
|
||||
self.devices.values()
|
||||
.filter(|e| e.device_class.as_ref() == Some(class))
|
||||
.count()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
//! Encarnación, muerte y supervisión.
|
||||
//!
|
||||
//! Aquí vive el flujo: Card → autorizar → soma::incarnate / wasm → registro
|
||||
//! en el grafo → SIGCHLD → on_death → Restart/OneShot/Delegate.
|
||||
|
||||
use super::{EnteGraph, Incarnated};
|
||||
use crate::events::{ExitStatus, GraphEvent};
|
||||
use ente_bus::{BusMessage, BusPayload, BusRequest};
|
||||
use ente_card::{Capability, EntityCard, Payload, Supervision};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{info, warn};
|
||||
use ulid::Ulid;
|
||||
|
||||
impl EnteGraph {
|
||||
/// Encarna las dependencias declaradas en la Semilla. Único punto donde
|
||||
/// el Init "decide": después sólo reacciona.
|
||||
pub async fn instantiate_seed_dependencies(
|
||||
&mut self,
|
||||
_tx: &mpsc::Sender<GraphEvent>,
|
||||
) -> anyhow::Result<()> {
|
||||
let cards = std::mem::take(&mut self.pending_genesis);
|
||||
if cards.is_empty() {
|
||||
info!(seed = %self.seed.label, "semilla sin genesis cards");
|
||||
return Ok(());
|
||||
}
|
||||
info!(seed = %self.seed.label, count = cards.len(), "instanciando genesis");
|
||||
let seed_id = self.seed.id;
|
||||
for card in cards {
|
||||
if let Err(e) = self.authorize_and_spawn(card, seed_id).await {
|
||||
warn!(?e, "genesis card falló");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Spawn solicitado por un Ente con `Capability::Spawn`. Verifica auth,
|
||||
/// requires del grafo, y delega la encarnación al backend correspondiente
|
||||
/// (`ente_soma` para procesos, `ente_wasm` para Wasm).
|
||||
pub async fn authorize_and_spawn(
|
||||
&mut self,
|
||||
mut card: EntityCard,
|
||||
requester: Ulid,
|
||||
) -> anyhow::Result<()> {
|
||||
if !self.holder_has(requester, &Capability::Spawn) {
|
||||
warn!(?requester, "spawn denied: lacks Capability::Spawn");
|
||||
return Ok(());
|
||||
}
|
||||
if let Err(e) = card.validate() {
|
||||
warn!(?e, label = %card.label, "card inválida, spawn rechazado");
|
||||
return Ok(());
|
||||
}
|
||||
// Falla rápida sobre `requires` — mejor que daemons en bucle.
|
||||
for req in &card.requires {
|
||||
if !self.providers.contains_key(req) {
|
||||
warn!(?req, label = %card.label, "requires no satisfecho");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
// Lineage por defecto = quien pidió el spawn.
|
||||
if card.lineage.is_none() {
|
||||
card.lineage = Some(requester);
|
||||
}
|
||||
|
||||
let pid = match &card.payload {
|
||||
Payload::Virtual => None,
|
||||
Payload::Native { .. } | Payload::Legacy { .. } => {
|
||||
Some(ente_soma::incarnate(&card)?)
|
||||
}
|
||||
Payload::Wasm { module_sha256, entry } => {
|
||||
// Wasm: hilo dedicado, sin PID. Su muerte se observa por
|
||||
// estado del runtime, no por SIGCHLD.
|
||||
let bytes = ente_cas::resolve(module_sha256)
|
||||
.map_err(|e| anyhow::anyhow!("CAS resolve para {}: {e}", card.label))?;
|
||||
ente_wasm::incarnate_wasm(&card, bytes, entry.clone())?;
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(p) = pid {
|
||||
self.by_pid.insert(p.as_raw(), card.id);
|
||||
}
|
||||
self.register_provider(&card);
|
||||
if let Some(parent) = card.lineage {
|
||||
self.children.entry(parent).or_default().push(card.id);
|
||||
}
|
||||
info!(label = %card.label, ?pid, lineage = ?card.lineage, "Ente encarnado");
|
||||
self.incarnated.insert(card.id, Incarnated { card, pid });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn on_death(
|
||||
&mut self,
|
||||
id: Ulid,
|
||||
status: ExitStatus,
|
||||
_tx: &mpsc::Sender<GraphEvent>,
|
||||
) {
|
||||
let Some(inc) = self.incarnated.remove(&id) else { return };
|
||||
if let Some(p) = inc.pid {
|
||||
self.by_pid.remove(&p.as_raw());
|
||||
}
|
||||
self.unregister_provider(&inc.card);
|
||||
if let Some(parent) = inc.card.lineage {
|
||||
if let Some(siblings) = self.children.get_mut(&parent) {
|
||||
siblings.retain(|c| c != &id);
|
||||
}
|
||||
}
|
||||
info!(label = %inc.card.label, ?status, "Ente disuelto");
|
||||
|
||||
match inc.card.supervision.clone() {
|
||||
Supervision::Restart { initial, max: _ } => {
|
||||
// Backoff exponencial: TODO real con timer del runtime.
|
||||
tokio::time::sleep(initial).await;
|
||||
let new_card = EntityCard { id: Ulid::new(), ..inc.card };
|
||||
if let Err(e) = self.authorize_and_spawn(new_card, self.seed.id).await {
|
||||
warn!(?e, "restart falló");
|
||||
}
|
||||
}
|
||||
Supervision::OneShot => {}
|
||||
Supervision::Delegate => {
|
||||
self.notify_lineage_of_death(&inc, &status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fire-and-forget: si el parent tiene conexión al bus, le forwardeamos
|
||||
/// un Invoke con la muerte del hijo. Sin retry, sin backpressure.
|
||||
fn notify_lineage_of_death(&mut self, inc: &Incarnated, status: &ExitStatus) {
|
||||
let Some(parent) = inc.card.lineage else { return };
|
||||
info!(
|
||||
child = %inc.card.id, parent = %parent, label = %inc.card.label,
|
||||
?status,
|
||||
"Supervision::Delegate — muerte notificada al lineage"
|
||||
);
|
||||
if let Some(out) = self.bus_connections.get(&parent).cloned() {
|
||||
let blob = format!("{}:{:?}", inc.card.id, status);
|
||||
let seq = self.alloc_invoke_seq();
|
||||
let msg = BusMessage {
|
||||
from: None,
|
||||
seq,
|
||||
payload: BusPayload::Request(BusRequest::Invoke {
|
||||
cap: Capability::Endpoint {
|
||||
interface: ente_card::InterfaceId([0xde; 16]),
|
||||
version: 1,
|
||||
},
|
||||
blob: blob.into_bytes(),
|
||||
}),
|
||||
};
|
||||
let _ = out.try_send(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
//! `EnteGraph`: estado del fractal vivo en PID 1.
|
||||
//!
|
||||
//! Diseño:
|
||||
//! - Submódulos por concern: lifecycle, topology, shutdown, bus_mediator,
|
||||
//! devices, capabilities. Cada uno extiende `impl EnteGraph` con métodos
|
||||
//! relacionados.
|
||||
//! - Estado plano (no substructs todavía) — la separación es por
|
||||
//! comportamiento, no por compartimentación de datos.
|
||||
//! - Toda mutación pasa por el bucle primordial vía `GraphEvent`. Los
|
||||
//! submódulos se llaman desde `main.rs::primordial_loop`.
|
||||
|
||||
mod bus_mediator;
|
||||
mod capabilities;
|
||||
mod devices;
|
||||
mod lifecycle;
|
||||
mod shutdown;
|
||||
mod topology;
|
||||
|
||||
use ente_bus::{BusMessage, BusResponse};
|
||||
use ente_card::{Capability, EntityCard};
|
||||
use nix::unistd::Pid;
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use ulid::Ulid;
|
||||
|
||||
pub use shutdown::SHUTDOWN_GRACE;
|
||||
|
||||
/// Bit alto encendido en `seq` para invokes server-iniciados — evita choque
|
||||
/// con secuencias allocadas por clientes.
|
||||
pub(in crate::graph) const SERVER_SEQ_FLAG: u64 = 1u64 << 63;
|
||||
|
||||
pub struct EnteGraph {
|
||||
pub(in crate::graph) seed: EntityCard,
|
||||
/// Entes encarnados como proceso o nodo virtual. id↔pid bidireccional.
|
||||
pub(in crate::graph) incarnated: HashMap<Ulid, Incarnated>,
|
||||
pub(in crate::graph) by_pid: HashMap<i32, Ulid>,
|
||||
/// Quién provee qué capacidad. Resuelve `requires` y `pick_invokable`.
|
||||
pub(in crate::graph) providers: BTreeMap<Capability, BTreeSet<Ulid>>,
|
||||
/// Tokens de capability emitidos. Revocables al morir el proveedor.
|
||||
pub(in crate::graph) next_token: u64,
|
||||
pub(in crate::graph) grants: HashMap<u64, GrantedCapability>,
|
||||
/// Dispositivos del kernel presentes (devpath → última UEvent).
|
||||
pub(in crate::graph) devices: HashMap<String, ente_kernel::UEvent>,
|
||||
/// Cards genesis pendientes de instanciar (extraídas de la Semilla).
|
||||
pub(in crate::graph) pending_genesis: Vec<EntityCard>,
|
||||
/// Hijos directos por lineage. parent → [child, ...].
|
||||
pub(in crate::graph) children: HashMap<Ulid, Vec<Ulid>>,
|
||||
/// Conexiones del bus indexadas por la identidad anunciada y verificada
|
||||
/// con SO_PEERCRED. El value es el extremo de escritura del writer task.
|
||||
pub(in crate::graph) bus_connections: HashMap<Ulid, mpsc::Sender<BusMessage>>,
|
||||
/// Invokes forwardeados pendientes de respuesta del proveedor.
|
||||
pub(in crate::graph) pending_invokes: HashMap<u64, oneshot::Sender<BusResponse>>,
|
||||
pub(in crate::graph) next_invoke_seq: u64,
|
||||
}
|
||||
|
||||
pub(in crate::graph) struct Incarnated {
|
||||
pub card: EntityCard,
|
||||
pub pid: Option<Pid>,
|
||||
}
|
||||
|
||||
pub(in crate::graph) struct GrantedCapability {
|
||||
pub cap: Capability,
|
||||
pub provider: Ulid,
|
||||
pub holder: Ulid,
|
||||
}
|
||||
|
||||
impl EnteGraph {
|
||||
pub fn new(mut seed: EntityCard) -> Self {
|
||||
// Extraemos genesis antes de almacenar la Semilla — evita duplicación
|
||||
// en `incarnated[seed.id]`.
|
||||
let pending_genesis = std::mem::take(&mut seed.genesis);
|
||||
let mut g = Self {
|
||||
seed: seed.clone(),
|
||||
incarnated: HashMap::new(),
|
||||
by_pid: HashMap::new(),
|
||||
providers: BTreeMap::new(),
|
||||
next_token: 1,
|
||||
grants: HashMap::new(),
|
||||
devices: HashMap::new(),
|
||||
pending_genesis,
|
||||
children: HashMap::new(),
|
||||
bus_connections: HashMap::new(),
|
||||
pending_invokes: HashMap::new(),
|
||||
next_invoke_seq: 0,
|
||||
};
|
||||
// El Ente #0 se inscribe a sí mismo como proveedor de las capacidades
|
||||
// que su Card declara — sólo así los hijos pueden requerirlas.
|
||||
g.register_provider(&seed);
|
||||
g.incarnated.insert(seed.id, Incarnated { card: seed, pid: None });
|
||||
g
|
||||
}
|
||||
|
||||
pub fn lookup_pid(&self, pid: Pid) -> Option<Ulid> {
|
||||
self.by_pid.get(&pid.as_raw()).copied()
|
||||
}
|
||||
|
||||
/// Acceso read-only a la Card de un Ente vivo. Usado por el cerebro
|
||||
/// para hidratar `SubjectInfo` sin clonar todo el mapa.
|
||||
pub fn peek_card(&self, id: &Ulid) -> Option<&EntityCard> {
|
||||
self.incarnated.get(id).map(|i| &i.card)
|
||||
}
|
||||
|
||||
/// Identidad de la Semilla. Usado como `requester` para spawns generados
|
||||
/// por reglas auto-cristalizadas (única identidad con Capability::Spawn).
|
||||
pub fn seed_id(&self) -> Ulid {
|
||||
self.seed.id
|
||||
}
|
||||
|
||||
/// Captura el estado live como snapshot serializable. Excluye la Semilla
|
||||
/// (será re-sintetizada al restore con su seed_id preservado).
|
||||
pub fn snapshot(&self) -> ente_snapshot::FractalSnapshot {
|
||||
let entes: Vec<EntityCard> = self.incarnated.iter()
|
||||
.filter(|(id, _)| **id != self.seed.id)
|
||||
.map(|(_, inc)| inc.card.clone())
|
||||
.collect();
|
||||
ente_snapshot::FractalSnapshot {
|
||||
version: ente_snapshot::SNAPSHOT_VERSION,
|
||||
timestamp_ms: ente_snapshot::now_ms(),
|
||||
seed_id: self.seed.id,
|
||||
seed_label: self.seed.label.clone(),
|
||||
entes,
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::graph) fn register_provider(&mut self, card: &EntityCard) {
|
||||
for cap in &card.provides {
|
||||
self.providers.entry(cap.clone()).or_default().insert(card.id);
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::graph) fn unregister_provider(&mut self, card: &EntityCard) {
|
||||
for cap in &card.provides {
|
||||
if let Some(set) = self.providers.get_mut(cap) {
|
||||
set.remove(&card.id);
|
||||
}
|
||||
}
|
||||
// Revocar grants emitidos por el Ente fallecido.
|
||||
let revoked: Vec<u64> = self.grants.iter()
|
||||
.filter(|(_, g)| g.provider == card.id)
|
||||
.map(|(t, _)| *t)
|
||||
.collect();
|
||||
for t in revoked {
|
||||
self.grants.remove(&t);
|
||||
}
|
||||
}
|
||||
|
||||
/// El Ente #0 (semilla) tiene todas sus capacidades declaradas. Otros
|
||||
/// las tienen si su Card las declara o si poseen un grant vivo.
|
||||
pub(in crate::graph) fn holder_has(&self, holder: Ulid, cap: &Capability) -> bool {
|
||||
if holder == self.seed.id {
|
||||
return self.seed.provides.contains(cap);
|
||||
}
|
||||
if let Some(inc) = self.incarnated.get(&holder) {
|
||||
if inc.card.provides.contains(cap) { return true; }
|
||||
}
|
||||
self.grants.values().any(|g| g.holder == holder && &g.cap == cap)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
//! Cascade shutdown: SIGTERM en orden topológico (hojas primero), grace
|
||||
//! period, SIGKILL para stragglers, reap final.
|
||||
|
||||
use super::EnteGraph;
|
||||
use nix::errno::Errno;
|
||||
use nix::sys::signal::{kill, Signal};
|
||||
use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
|
||||
use nix::unistd::Pid;
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
/// Tiempo que damos a los Entes tras SIGTERM antes de escalar a SIGKILL.
|
||||
pub const SHUTDOWN_GRACE: Duration = Duration::from_secs(2);
|
||||
|
||||
impl EnteGraph {
|
||||
pub async fn cascade_shutdown(&mut self) {
|
||||
let order = self.topo_order();
|
||||
let pids: Vec<Pid> = order.iter()
|
||||
.filter_map(|id| self.incarnated.get(id).and_then(|i| i.pid))
|
||||
.collect();
|
||||
|
||||
if pids.is_empty() {
|
||||
info!("cascade shutdown: ningún Ente encarnado, salida limpia");
|
||||
return;
|
||||
}
|
||||
|
||||
info!(
|
||||
count = pids.len(), grace_ms = SHUTDOWN_GRACE.as_millis() as u64,
|
||||
"SIGTERM cascade (topológico, hojas primero)"
|
||||
);
|
||||
for pid in &pids {
|
||||
match kill(*pid, Signal::SIGTERM) {
|
||||
Ok(()) => {}
|
||||
Err(Errno::ESRCH) => {} // ya muerto, lo cosecharemos abajo
|
||||
Err(e) => warn!(?pid, ?e, "kill SIGTERM falló"),
|
||||
}
|
||||
}
|
||||
|
||||
let deadline = Instant::now() + SHUTDOWN_GRACE;
|
||||
while Instant::now() < deadline {
|
||||
if !self.incarnated.values().any(|i| i.pid.is_some()) {
|
||||
break;
|
||||
}
|
||||
match waitpid(None, Some(WaitPidFlag::WNOHANG)) {
|
||||
Ok(WaitStatus::Exited(pid, code)) => {
|
||||
self.reap_during_shutdown(pid);
|
||||
debug!(?pid, code, "reaped (exited)");
|
||||
}
|
||||
Ok(WaitStatus::Signaled(pid, sig, _)) => {
|
||||
self.reap_during_shutdown(pid);
|
||||
debug!(?pid, ?sig, "reaped (signaled)");
|
||||
}
|
||||
Ok(WaitStatus::StillAlive) | Err(Errno::EINTR) => {
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
Err(Errno::ECHILD) => return,
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
warn!(?e, "waitpid fallo en shutdown grace");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let stragglers: Vec<Pid> = self.incarnated.values()
|
||||
.filter_map(|i| i.pid)
|
||||
.collect();
|
||||
|
||||
if stragglers.is_empty() {
|
||||
info!("cascade shutdown completo (todos los Entes terminaron en gracia)");
|
||||
return;
|
||||
}
|
||||
|
||||
warn!(count = stragglers.len(), "stragglers post-SIGTERM, escalando a SIGKILL");
|
||||
for pid in &stragglers {
|
||||
let _ = kill(*pid, Signal::SIGKILL);
|
||||
}
|
||||
loop {
|
||||
match waitpid(None, Some(WaitPidFlag::WNOHANG)) {
|
||||
Ok(WaitStatus::Exited(pid, _)) | Ok(WaitStatus::Signaled(pid, _, _)) => {
|
||||
self.reap_during_shutdown(pid);
|
||||
}
|
||||
Ok(WaitStatus::StillAlive) => {
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
Err(Errno::ECHILD) => break,
|
||||
_ => break,
|
||||
}
|
||||
if !self.incarnated.values().any(|i| i.pid.is_some()) { break; }
|
||||
}
|
||||
info!("cascade shutdown completo (con SIGKILL)");
|
||||
}
|
||||
|
||||
fn reap_during_shutdown(&mut self, pid: Pid) {
|
||||
let Some(id) = self.by_pid.remove(&pid.as_raw()) else { return };
|
||||
if let Some(inc) = self.incarnated.remove(&id) {
|
||||
self.unregister_provider(&inc.card);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//! Topología del fractal: índice de hijos por lineage y orden topológico
|
||||
//! para shutdown.
|
||||
|
||||
use super::EnteGraph;
|
||||
use std::collections::BTreeSet;
|
||||
use ulid::Ulid;
|
||||
|
||||
impl EnteGraph {
|
||||
/// DFS post-order desde la Semilla. Hojas primero, raíz al final.
|
||||
/// Garantiza que SIGTERM va a un padre sólo cuando sus hijos ya recibieron
|
||||
/// la señal (evita orfandad transitoria que confunda Restart supervisors).
|
||||
pub(in crate::graph) fn topo_order(&self) -> Vec<Ulid> {
|
||||
let mut visited = BTreeSet::new();
|
||||
let mut order = Vec::new();
|
||||
self.dfs_post(self.seed.id, &mut visited, &mut order);
|
||||
// Entes encarnados sin lineage hacia el seed (no debería pasar pero
|
||||
// protege contra grafos huérfanos): añadirlos al final.
|
||||
for id in self.incarnated.keys() {
|
||||
if !visited.contains(id) {
|
||||
self.dfs_post(*id, &mut visited, &mut order);
|
||||
}
|
||||
}
|
||||
order
|
||||
}
|
||||
|
||||
fn dfs_post(&self, node: Ulid, visited: &mut BTreeSet<Ulid>, order: &mut Vec<Ulid>) {
|
||||
if !visited.insert(node) { return; }
|
||||
if let Some(children) = self.children.get(&node) {
|
||||
for c in children.clone() {
|
||||
self.dfs_post(c, visited, order);
|
||||
}
|
||||
}
|
||||
order.push(node);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
//! Ente #0 — el primer Ente. PID 1 del fractal.
|
||||
//!
|
||||
//! Reglas no negociables:
|
||||
//! 1. NUNCA lógica de servicio aquí. Sólo: leer Semilla, cosechar zombis,
|
||||
//! mediar capacidades, propagar eventos.
|
||||
//! 2. Single-threaded. Cualquier paralelismo se delega a Entes worker.
|
||||
//! Un panic en un thread de PID 1 = kernel panic.
|
||||
//! 3. Errores de hijos son *eventos* en `graph_tx`, no `Result` propagado.
|
||||
//!
|
||||
//! Este archivo es sólo wireup. La lógica vive en:
|
||||
//! - `seed` : construcción/restauración de la Tarjeta Semilla
|
||||
//! - `bus` : listener Unix + auth via SO_PEERCRED
|
||||
//! - `graph::*` : estado del fractal (lifecycle, topology, shutdown,
|
||||
//! bus_mediator, devices, capabilities)
|
||||
//! - `events` : tipos de eventos del bucle primordial
|
||||
//! - crates externos del workspace para CAS, soma, wasm, snapshot, kernel.
|
||||
|
||||
mod brain_glue;
|
||||
mod bus;
|
||||
mod events;
|
||||
mod graph;
|
||||
mod seed;
|
||||
|
||||
use anyhow::Context;
|
||||
use ente_brain::{BrainState, IntrospectServer};
|
||||
use ente_kernel::{become_child_subreaper, bootstrap_kernel_surface, spawn_sigchld_stream, spawn_uevent_stream};
|
||||
use events::{ExitStatus, GraphEvent, ShutdownReason};
|
||||
use graph::EnteGraph;
|
||||
use nix::errno::Errno;
|
||||
use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
|
||||
use nix::unistd::{getpid, Pid};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
struct CliArgs {
|
||||
checkpoint: Option<PathBuf>,
|
||||
restore: Option<PathBuf>,
|
||||
rules: Option<PathBuf>,
|
||||
rules_out: Option<PathBuf>,
|
||||
metrics_addr: Option<String>,
|
||||
}
|
||||
|
||||
fn parse_args() -> CliArgs {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let mut checkpoint = None;
|
||||
let mut restore = None;
|
||||
let mut rules = None;
|
||||
let mut rules_out = None;
|
||||
let mut metrics_addr = None;
|
||||
while let Some(a) = args.next() {
|
||||
match a.as_str() {
|
||||
"--checkpoint" => checkpoint = args.next().map(PathBuf::from),
|
||||
"--restore" => restore = args.next().map(PathBuf::from),
|
||||
"--rules" => rules = args.next().map(PathBuf::from),
|
||||
"--rules-out" => rules_out = args.next().map(PathBuf::from),
|
||||
"--metrics-addr" => metrics_addr = args.next(),
|
||||
other => warn!(arg = %other, "argumento desconocido, ignorado"),
|
||||
}
|
||||
}
|
||||
CliArgs { checkpoint, restore, rules, rules_out, metrics_addr }
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
init_tracing();
|
||||
let cli = parse_args();
|
||||
let pid = getpid();
|
||||
let dev_mode = pid != Pid::from_raw(1);
|
||||
|
||||
if dev_mode {
|
||||
warn!(?pid, "ente-zero corriendo en DEV MODE (no PID 1) — kernel surface no se monta");
|
||||
} else {
|
||||
info!("ente-zero despierta como PID 1");
|
||||
bootstrap_kernel_surface().context("bootstrap kernel surface")?;
|
||||
become_child_subreaper().context("PR_SET_CHILD_SUBREAPER")?;
|
||||
}
|
||||
|
||||
let card = seed::load(dev_mode, cli.restore.as_deref())?;
|
||||
|
||||
// current_thread runtime: ver doctrina al inicio del módulo.
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.enable_time()
|
||||
.build()?;
|
||||
|
||||
rt.block_on(primordial_loop(card, dev_mode, cli.checkpoint, cli.rules, cli.rules_out, cli.metrics_addr))
|
||||
}
|
||||
|
||||
async fn primordial_loop(
|
||||
seed_card: ente_card::EntityCard,
|
||||
dev_mode: bool,
|
||||
checkpoint_path: Option<PathBuf>,
|
||||
rules_path: Option<PathBuf>,
|
||||
rules_out: Option<PathBuf>,
|
||||
metrics_addr: Option<String>,
|
||||
) -> anyhow::Result<()> {
|
||||
info!(seed_id = %seed_card.id, label = %seed_card.label, "Ente #0 entra al bucle primordial");
|
||||
|
||||
let (graph_tx, mut graph_rx) = mpsc::channel::<GraphEvent>(64);
|
||||
let mut sigchld = spawn_sigchld_stream()?;
|
||||
// Uevents puede fallar en dev (sin CAP_NET_ADMIN). Degradamos a un
|
||||
// canal nunca-listo en lugar de abortar el bucle primordial.
|
||||
let mut uevents = match spawn_uevent_stream() {
|
||||
Ok(rx) => rx,
|
||||
Err(e) => {
|
||||
warn!(?e, "uevents deshabilitados (probablemente falta CAP_NET_ADMIN)");
|
||||
let (_keep_tx, rx) = mpsc::channel::<ente_kernel::UEvent>(1);
|
||||
std::mem::forget(_keep_tx);
|
||||
rx
|
||||
}
|
||||
};
|
||||
|
||||
// Bus interno: listener antes de spawn de hijos para que su Announce
|
||||
// tenga adónde llegar. Su path se inyecta en ENTE_BUS_SOCK por soma.
|
||||
let bus_sock = bus::default_socket_path();
|
||||
let bus_path = bus::spawn_bus(bus_sock, graph_tx.clone())?;
|
||||
ente_soma::set_bus_sock(bus_path.to_string_lossy().into_owned());
|
||||
|
||||
let mut graph = EnteGraph::new(seed_card);
|
||||
graph.instantiate_seed_dependencies(&graph_tx).await?;
|
||||
|
||||
// Cerebro: BrainState compartido + servidor de introspección.
|
||||
// Window de 1024 eventos — suficiente para correlaciones interesantes
|
||||
// sin gastar memoria de PID 1. En dev bajamos el umbral de cristalización
|
||||
// para que el demo (pocos eventos) produzca cristales observables.
|
||||
let mut brain = if dev_mode {
|
||||
// Umbrales relajados para que el demo (pocos eventos) produzca
|
||||
// cristales observables. Con P(b|a) normalizada a [0,1], los
|
||||
// valores típicos en muestras pequeñas son 0.2-0.5.
|
||||
BrainState::with_params(1024, ente_brain::CrystallizationParams {
|
||||
min_support: 2,
|
||||
min_conditional_prob: 0.3,
|
||||
min_pmi: 1.0,
|
||||
})
|
||||
} else {
|
||||
BrainState::new(1024)
|
||||
};
|
||||
if let Some(out_path) = rules_out {
|
||||
brain = brain.with_rules_out(out_path);
|
||||
}
|
||||
|
||||
// Carga inicial de reglas vía KCL o JSON, si --rules path proporcionado.
|
||||
if let Some(path) = &rules_path {
|
||||
match ente_brain::load_rules_file(path) {
|
||||
Ok(rules) => {
|
||||
let mut engine = brain.engine.write().await;
|
||||
for r in rules {
|
||||
engine.insert(r);
|
||||
}
|
||||
info!(count = engine.len(), path = %path.display(), "reglas cargadas");
|
||||
}
|
||||
Err(e) => warn!(?e, path = %path.display(), "carga de reglas falló"),
|
||||
}
|
||||
}
|
||||
|
||||
// Endpoint Prometheus opcional. En dev por defecto en 127.0.0.1:9911 si
|
||||
// el flag no se pasó.
|
||||
let metrics_addr = metrics_addr.or_else(|| {
|
||||
if dev_mode { Some("127.0.0.1:9911".to_string()) } else { None }
|
||||
});
|
||||
if let Some(addr_s) = metrics_addr {
|
||||
match addr_s.parse::<std::net::SocketAddr>() {
|
||||
Ok(addr) => {
|
||||
let s = brain.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = ente_brain::serve_metrics(s, addr).await {
|
||||
warn!(?e, "metrics server cayó");
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => warn!(?e, addr = %addr_s, "metrics-addr inválido"),
|
||||
}
|
||||
}
|
||||
spawn_brain_introspect(brain.clone());
|
||||
let brain_sink = brain_glue::GraphSink {
|
||||
graph_tx: graph_tx.clone(),
|
||||
// Spawns auto-disparados desde reglas usan la identidad de la Semilla
|
||||
// (único Ente con Capability::Spawn por construcción).
|
||||
requester: graph.seed_id(),
|
||||
};
|
||||
|
||||
// Demo automático del forwarding (sólo dev, sólo si el binario existe).
|
||||
if dev_mode && std::path::Path::new("target/debug/ente-echo").exists() {
|
||||
spawn_echo_smoke_test(bus_path.clone());
|
||||
}
|
||||
|
||||
// En dev mode no tenemos hijos por defecto y el bucle se quedaría inerte.
|
||||
let dev_exit = if dev_mode {
|
||||
Some(tokio::time::sleep(Duration::from_secs(4)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
tokio::pin!(dev_exit);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
|
||||
Some(_) = sigchld.recv() => {
|
||||
reap_until_empty(&mut graph, &graph_tx).await;
|
||||
}
|
||||
|
||||
Some(uevt) = uevents.recv() => {
|
||||
graph.on_uevent(uevt, &graph_tx).await;
|
||||
}
|
||||
|
||||
Some(evt) = graph_rx.recv() => {
|
||||
// Cerebro observa antes que el grafo mute. Snapshot del
|
||||
// SubjectInfo se hace contra el estado pre-mutación.
|
||||
feed_brain(&brain, &brain_sink, &graph, &evt).await;
|
||||
if dispatch_graph_event(&mut graph, evt, &graph_tx, &checkpoint_path).await {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
_ = async { dev_exit.as_mut().as_pin_mut().unwrap().await }, if dev_mode => {
|
||||
info!("dev mode: timer expirado, cerrando bucle primordial");
|
||||
let _ = graph_tx.send(GraphEvent::Shutdown {
|
||||
reason: ShutdownReason::SeedRequested,
|
||||
}).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Devuelve `true` si el bucle primordial debe terminar.
|
||||
async fn dispatch_graph_event(
|
||||
graph: &mut EnteGraph,
|
||||
evt: GraphEvent,
|
||||
tx: &mpsc::Sender<GraphEvent>,
|
||||
checkpoint: &Option<PathBuf>,
|
||||
) -> bool {
|
||||
match evt {
|
||||
GraphEvent::EnteDied { id, status } => {
|
||||
graph.on_death(id, status, tx).await;
|
||||
}
|
||||
GraphEvent::CapabilityRequested { from, cap, reply } => {
|
||||
graph.mediate_capability(from, cap, reply).await;
|
||||
}
|
||||
GraphEvent::SpawnRequest { card, requester } => {
|
||||
if let Err(e) = graph.authorize_and_spawn(card, requester).await {
|
||||
warn!(?e, "spawn request error");
|
||||
}
|
||||
}
|
||||
GraphEvent::BusRequest { peer, from, request, outbound, reply } => {
|
||||
graph.on_bus_request(peer, from, request, outbound, reply).await;
|
||||
}
|
||||
GraphEvent::BusResponse { seq, response } => {
|
||||
graph.on_bus_response(seq, response).await;
|
||||
}
|
||||
GraphEvent::BusConnClosed { ente_id } => {
|
||||
graph.on_bus_conn_closed(ente_id).await;
|
||||
}
|
||||
GraphEvent::Shutdown { reason } => {
|
||||
warn!(?reason, "shutdown del fractal");
|
||||
if let Some(path) = checkpoint.as_ref() {
|
||||
let snap = graph.snapshot();
|
||||
match snap.write(path) {
|
||||
Ok(()) => info!(path = %path.display(), entes = snap.entes.len(), "snapshot persistido"),
|
||||
Err(e) => warn!(?e, "snapshot write falló"),
|
||||
}
|
||||
}
|
||||
graph.cascade_shutdown().await;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
async fn reap_until_empty(graph: &mut EnteGraph, tx: &mpsc::Sender<GraphEvent>) {
|
||||
loop {
|
||||
match waitpid(None, Some(WaitPidFlag::WNOHANG)) {
|
||||
Ok(WaitStatus::StillAlive) => return,
|
||||
Ok(WaitStatus::Exited(pid, code)) => {
|
||||
emit_death(graph, tx, pid, ExitStatus::Exit(code)).await;
|
||||
}
|
||||
Ok(WaitStatus::Signaled(pid, sig, _core)) => {
|
||||
emit_death(graph, tx, pid, ExitStatus::Killed(sig)).await;
|
||||
}
|
||||
Ok(_) => continue, // Stopped/Continued — irrelevantes
|
||||
Err(Errno::ECHILD) => return,
|
||||
Err(e) => {
|
||||
error!(?e, "waitpid fallo no recuperable en bucle de reaping");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn emit_death(
|
||||
graph: &EnteGraph,
|
||||
tx: &mpsc::Sender<GraphEvent>,
|
||||
pid: Pid,
|
||||
status: ExitStatus,
|
||||
) {
|
||||
let id = match graph.lookup_pid(pid) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
// Proceso adoptado (subreaper): no está en nuestro grafo.
|
||||
info!(?pid, ?status, "huérfano cosechado (no en grafo)");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let _ = tx.send(GraphEvent::EnteDied { id, status }).await;
|
||||
}
|
||||
|
||||
fn spawn_echo_smoke_test(bus_path: PathBuf) {
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
match ente_bus::BusClient::connect(&bus_path).await {
|
||||
Ok(mut client) => {
|
||||
let req = ente_bus::BusRequest::Invoke {
|
||||
cap: ente_echo::echo_capability(),
|
||||
blob: b"hola fractal forwardeado".to_vec(),
|
||||
};
|
||||
match client.call(req).await {
|
||||
Ok(ente_bus::BusResponse::Invoked { result }) => {
|
||||
info!(echo = %String::from_utf8_lossy(&result), "Invoke ECHO round-trip OK");
|
||||
}
|
||||
Ok(other) => warn!(?other, "Invoke ECHO respuesta inesperada"),
|
||||
Err(e) => warn!(?e, "Invoke ECHO falló"),
|
||||
}
|
||||
}
|
||||
Err(e) => warn!(?e, "no se pudo conectar al bus para test"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn init_tracing() {
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
let filter = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new("ente_zero=debug,info"));
|
||||
fmt().with_env_filter(filter).with_target(true).init();
|
||||
}
|
||||
|
||||
fn brain_introspect_path() -> PathBuf {
|
||||
if let Ok(p) = std::env::var("ENTE_BRAIN_SOCK") {
|
||||
return p.into();
|
||||
}
|
||||
let runtime = std::env::var("XDG_RUNTIME_DIR")
|
||||
.unwrap_or_else(|_| std::env::var("TMPDIR").unwrap_or_else(|_| "/tmp".into()));
|
||||
format!("{runtime}/ente-brain.sock").into()
|
||||
}
|
||||
|
||||
fn spawn_brain_introspect(state: BrainState) {
|
||||
let path = brain_introspect_path();
|
||||
tokio::spawn(async move {
|
||||
let server = IntrospectServer::new(state);
|
||||
if let Err(e) = server.serve(&path).await {
|
||||
warn!(?e, "introspect server cayó");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Registra el evento en el observer y dispatcha cualquier regla matched.
|
||||
/// Para reglas Sequence: pasamos los últimos N eventos del observer como
|
||||
/// history al engine.
|
||||
async fn feed_brain(
|
||||
brain: &BrainState,
|
||||
sink: &brain_glue::GraphSink,
|
||||
graph: &EnteGraph,
|
||||
evt: &GraphEvent,
|
||||
) {
|
||||
let Some((kind, subj)) = brain_glue::graph_event_to_brain(evt, graph) else { return };
|
||||
let history: Vec<ente_brain::TimedEvent> = {
|
||||
let mut obs = brain.observer.write().await;
|
||||
obs.record(kind.clone());
|
||||
// Snapshot de los últimos 16 eventos — suficiente para cualquier
|
||||
// Sequence pattern razonable. Clone hace una sola alocación.
|
||||
obs.recent(16).cloned().collect()
|
||||
};
|
||||
let rules = {
|
||||
let engine = brain.engine.read().await;
|
||||
engine.dispatch(&kind, &subj, &history)
|
||||
};
|
||||
if !rules.is_empty() {
|
||||
ente_brain::dispatch_actions(&rules, sink).await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
//! Construcción de la Tarjeta Semilla.
|
||||
//!
|
||||
//! Tres caminos:
|
||||
//! 1. `--restore <path>`: leer `FractalSnapshot` y reconstruir Semilla
|
||||
//! con seed_id preservado + entes anteriores como genesis.
|
||||
//! 2. `seed.card` en disco: deserialize directo (prod o dev).
|
||||
//! 3. Fallback dev: sintetizar Semilla + 6 genesis Entes que ejercitan
|
||||
//! todas las capacidades del fractal.
|
||||
|
||||
use anyhow::Context;
|
||||
use ente_card::{
|
||||
Capability, CardError, CgroupSpec, EntityCard, NamespaceSet, Payload,
|
||||
ResourceLimits, SomaSpec, Supervision, CARD_SCHEMA_VERSION,
|
||||
};
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
use tracing::{info, warn};
|
||||
use ulid::Ulid;
|
||||
|
||||
const SEED_PATH_PROD: &str = "/ente/seed.card";
|
||||
const SEED_PATH_DEV: &str = "seed.card";
|
||||
|
||||
pub fn load(dev_mode: bool, restore: Option<&Path>) -> anyhow::Result<EntityCard> {
|
||||
let card = if let Some(path) = restore {
|
||||
load_from_snapshot(path)?
|
||||
} else {
|
||||
load_or_synthesize(dev_mode)?
|
||||
};
|
||||
card.validate()
|
||||
.map_err(|e: CardError| anyhow::anyhow!("semilla inválida: {e}"))?;
|
||||
Ok(card)
|
||||
}
|
||||
|
||||
fn load_from_snapshot(path: &Path) -> anyhow::Result<EntityCard> {
|
||||
let snap = ente_snapshot::FractalSnapshot::read(path)
|
||||
.with_context(|| format!("read snapshot {}", path.display()))?;
|
||||
info!(
|
||||
path = %path.display(),
|
||||
seed_id = %snap.seed_id,
|
||||
entes = snap.entes.len(),
|
||||
timestamp_ms = snap.timestamp_ms,
|
||||
"snapshot cargado, restaurando fractal"
|
||||
);
|
||||
// Reconstruimos la Semilla con su Ulid original. Las Cards persistidas
|
||||
// van a `genesis` con sus Ulids preservados — son las mismas identidades
|
||||
// que vivieron antes del checkpoint.
|
||||
let mut provides = BTreeSet::new();
|
||||
provides.insert(Capability::Spawn);
|
||||
provides.insert(Capability::Journal);
|
||||
Ok(EntityCard {
|
||||
schema_version: CARD_SCHEMA_VERSION,
|
||||
id: snap.seed_id,
|
||||
lineage: None,
|
||||
label: snap.seed_label,
|
||||
provides,
|
||||
requires: BTreeSet::new(),
|
||||
soma: SomaSpec::default(),
|
||||
payload: Payload::Virtual,
|
||||
supervision: Supervision::OneShot,
|
||||
genesis: snap.entes,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_or_synthesize(dev_mode: bool) -> anyhow::Result<EntityCard> {
|
||||
// Buscamos primero `.k` (KCL canónico, validado por su schema), luego
|
||||
// `.json` para compatibilidad. La puerta genética se cruza vía
|
||||
// `ente_brain::load_card_file` que pasa por `validate()` extendido.
|
||||
let candidates: &[&str] = if dev_mode {
|
||||
&["seed.card.k", SEED_PATH_DEV]
|
||||
} else {
|
||||
&["/ente/seed.card.k", SEED_PATH_PROD]
|
||||
};
|
||||
for cand in candidates {
|
||||
let path = PathBuf::from(cand);
|
||||
if !path.exists() { continue; }
|
||||
let card = ente_brain::load_card_file(&path)
|
||||
.with_context(|| format!("load {}", path.display()))?;
|
||||
info!(path = %path.display(), "Tarjeta Semilla cargada y validada");
|
||||
return Ok(card);
|
||||
}
|
||||
if dev_mode {
|
||||
info!("sin seed.card — sintetizando semilla mínima (dev)");
|
||||
return Ok(synthesize_dev_seed());
|
||||
}
|
||||
anyhow::bail!("seed.card no encontrada en /ente/seed.card[.k]")
|
||||
}
|
||||
|
||||
fn synthesize_dev_seed() -> EntityCard {
|
||||
let mut provides = BTreeSet::new();
|
||||
provides.insert(Capability::Spawn);
|
||||
provides.insert(Capability::Journal);
|
||||
|
||||
// Pre-registramos el módulo Wasm demo en el CAS y obtenemos su SHA real.
|
||||
// Si el CAS no es escribible (raro en dev) caemos a un SHA cero — la
|
||||
// resolución fallará y el Wasm no encarnará, pero el resto queda intacto.
|
||||
let demo_wasm_sha = match ente_wasm::demo_module_bytes()
|
||||
.and_then(|b| ente_cas::store(&b))
|
||||
{
|
||||
Ok(sha) => sha,
|
||||
Err(e) => {
|
||||
warn!(?e, "CAS no disponible — demo-wasm no encarnará");
|
||||
[0u8; 32]
|
||||
}
|
||||
};
|
||||
|
||||
let mut genesis = Vec::new();
|
||||
genesis.push(make_card("demo-sleep", Payload::Native {
|
||||
exec: "/bin/sleep".into(), argv: vec!["1".into()], envp: vec![],
|
||||
}, Supervision::OneShot));
|
||||
|
||||
genesis.push(make_card("demo-persist", Payload::Native {
|
||||
exec: "/bin/sleep".into(), argv: vec!["60".into()], envp: vec![],
|
||||
}, restart_supervision()));
|
||||
|
||||
// Card namespaced: padre escribe uid_map, hijo cat /proc/self/uid_map.
|
||||
let mut ns_card = make_card("demo-userns", Payload::Native {
|
||||
exec: "/bin/cat".into(),
|
||||
argv: vec!["/proc/self/uid_map".into()],
|
||||
envp: vec![],
|
||||
}, Supervision::OneShot);
|
||||
ns_card.soma = SomaSpec {
|
||||
namespaces: NamespaceSet { user: true, ..Default::default() },
|
||||
..Default::default()
|
||||
};
|
||||
genesis.push(ns_card);
|
||||
|
||||
genesis.push(make_card("demo-wasm", Payload::Wasm {
|
||||
module_sha256: demo_wasm_sha,
|
||||
entry: "_start".into(),
|
||||
}, Supervision::OneShot));
|
||||
|
||||
if let Some(card) = optional_native_card(
|
||||
"demo-echo", "target/debug/ente-echo",
|
||||
[ente_echo::echo_capability()].into_iter().collect(),
|
||||
restart_supervision(),
|
||||
) {
|
||||
genesis.push(card);
|
||||
}
|
||||
|
||||
if let Some(card) = optional_native_card(
|
||||
"compat-logind", "target/debug/ente-logind-compat",
|
||||
[Capability::LegacyLogind].into_iter().collect(),
|
||||
restart_supervision(),
|
||||
) {
|
||||
genesis.push(card);
|
||||
}
|
||||
|
||||
EntityCard {
|
||||
schema_version: CARD_SCHEMA_VERSION,
|
||||
id: Ulid::new(),
|
||||
lineage: None,
|
||||
label: "ente-zero-dev".into(),
|
||||
provides,
|
||||
requires: BTreeSet::new(),
|
||||
soma: SomaSpec {
|
||||
namespaces: NamespaceSet::default(),
|
||||
rlimits: ResourceLimits::default(),
|
||||
cgroup: CgroupSpec {
|
||||
path: "ente.slice/zero".into(),
|
||||
cpu_weight: None,
|
||||
io_weight: None,
|
||||
},
|
||||
cpu_affinity: None,
|
||||
},
|
||||
payload: Payload::Virtual,
|
||||
supervision: Supervision::OneShot,
|
||||
genesis,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_card(label: &str, payload: Payload, supervision: Supervision) -> EntityCard {
|
||||
EntityCard {
|
||||
schema_version: CARD_SCHEMA_VERSION,
|
||||
id: Ulid::new(),
|
||||
lineage: None,
|
||||
label: label.into(),
|
||||
provides: BTreeSet::new(),
|
||||
requires: BTreeSet::new(),
|
||||
soma: SomaSpec::default(),
|
||||
payload,
|
||||
supervision,
|
||||
genesis: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn optional_native_card(
|
||||
label: &str,
|
||||
bin_path: &str,
|
||||
provides: BTreeSet<Capability>,
|
||||
supervision: Supervision,
|
||||
) -> Option<EntityCard> {
|
||||
let path = Path::new(bin_path);
|
||||
if !path.exists() {
|
||||
return None;
|
||||
}
|
||||
Some(EntityCard {
|
||||
schema_version: CARD_SCHEMA_VERSION,
|
||||
id: Ulid::new(),
|
||||
lineage: None,
|
||||
label: label.into(),
|
||||
provides,
|
||||
requires: BTreeSet::new(),
|
||||
soma: SomaSpec::default(),
|
||||
payload: Payload::Native {
|
||||
exec: path.to_string_lossy().into_owned(),
|
||||
argv: vec![],
|
||||
envp: vec![],
|
||||
},
|
||||
supervision,
|
||||
genesis: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
fn restart_supervision() -> Supervision {
|
||||
Supervision::Restart {
|
||||
initial: Duration::from_millis(100),
|
||||
max: Duration::from_secs(30),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user