//! `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, pub(in crate::graph) by_pid: HashMap, /// Quién provee qué capacidad. Resuelve `requires` y `pick_invokable`. pub(in crate::graph) providers: BTreeMap>, /// Tokens de capability emitidos. Revocables al morir el proveedor. pub(in crate::graph) next_token: u64, pub(in crate::graph) grants: HashMap, /// Dispositivos del kernel presentes (devpath → última UEvent). pub(in crate::graph) devices: HashMap, /// Cards genesis pendientes de instanciar (extraídas de la Semilla). pub(in crate::graph) pending_genesis: Vec, /// Hijos directos por lineage. parent → [child, ...]. pub(in crate::graph) children: HashMap>, /// 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>, /// Invokes forwardeados pendientes de respuesta del proveedor. pub(in crate::graph) pending_invokes: HashMap>, pub(in crate::graph) next_invoke_seq: u64, } pub(in crate::graph) struct Incarnated { pub card: EntityCard, pub pid: Option, /// Capacidades añadidas en runtime vía BusRequest::UpdateCapabilities. /// La Card original es immutable; la "vista efectiva" del Ente es /// `card.provides ∪ dynamic_provides`. pub dynamic_provides: BTreeSet, } pub(in crate::graph) struct GrantedCapability { pub cap: Capability, pub provider: Ulid, pub holder: Ulid, /// Instante en el que el grant deja de ser válido. El garbage collector /// del cerebro purga grants con `Instant::now() > expires_at`. pub expires_at: std::time::Instant, } /// TTL default para grants cuando la cap no tiene override. 60s es un /// compromiso: largo enough para evitar churn en patrones interactivos, /// corto enough para que credenciales filtradas expiren rápidamente. pub const DEFAULT_GRANT_TTL: std::time::Duration = std::time::Duration::from_secs(60); /// Quota máxima de tokens activos por (holder, cap). Caps escaladas tienen /// quota baja para limitar fugas de credenciales; caps de uso frecuente /// (Endpoint, Journal) son más laxas. pub fn quota_for_capability(cap: &Capability) -> u32 { match cap { // Caps escaladas: pocos tokens, fuerza patrón request-act-release. Capability::Spawn => 2, Capability::FilesystemRoot => 2, Capability::Device { .. } => 4, // Caps de propósito general. Capability::Endpoint { .. } => 16, Capability::KernelNetlink(_) => 4, Capability::LegacyLogind => 8, // Logging: hasta 32 streams. Capability::Journal => 32, } } /// TTL específico por variante de Capability. Caps de mayor riesgo / costo /// (Spawn, FilesystemRoot) tienen TTL más corto; caps "logging" como /// Journal pueden vivir más. /// /// Cualquier cap no listada cae al `DEFAULT_GRANT_TTL`. pub fn ttl_for_capability(cap: &Capability) -> std::time::Duration { use std::time::Duration; match cap { // Caps escaladas: TTL corto para forzar renovación frecuente. Capability::Spawn => Duration::from_secs(30), Capability::FilesystemRoot => Duration::from_secs(30), Capability::Device { .. } => Duration::from_secs(60), // Caps de propósito general. Capability::Endpoint { .. } => Duration::from_secs(300), // 5 min Capability::KernelNetlink(_) => Duration::from_secs(300), Capability::LegacyLogind => Duration::from_secs(300), // Logging puede vivir mucho. Capability::Journal => Duration::from_secs(3600), // 1h } } 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, dynamic_provides: BTreeSet::new(), }); g } pub fn lookup_pid(&self, pid: Pid) -> Option { 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 = 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 = self.grants.iter() .filter(|(_, g)| g.provider == card.id) .map(|(t, _)| *t) .collect(); for t in revoked { self.grants.remove(&t); } } /// Quita una capacidad dinámica del índice de providers para un Ente /// específico. Usado al recibir UpdateCapabilities con `removes`. pub(in crate::graph) fn unregister_dynamic_cap(&mut self, ente_id: Ulid, cap: &Capability) { if let Some(set) = self.providers.get_mut(cap) { set.remove(&ente_id); } } /// Inserta una capacidad dinámica al índice de providers para un Ente. pub(in crate::graph) fn register_dynamic_cap(&mut self, ente_id: Ulid, cap: Capability) { self.providers.entry(cap).or_default().insert(ente_id); } /// 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) } }