//! ente-card: definición de la Tarjeta de Identidad del Ente. //! //! Una `EntityCard` no describe un proceso — describe una identidad en el //! grafo del fractal. El Init la lee y decide cómo *encarnarla*: como ELF //! nativo, módulo Wasm, wrapper legacy, o nodo virtual sin proceso. use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; use std::fmt; use std::time::Duration; use ulid::Ulid; /// Versión del esquema de la Card. Cambiar = romper compatibilidad del fractal. pub const CARD_SCHEMA_VERSION: u16 = 1; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EntityCard { pub schema_version: u16, pub id: Ulid, pub lineage: Option, pub label: String, pub provides: BTreeSet, pub requires: BTreeSet, pub soma: SomaSpec, pub payload: Payload, pub supervision: Supervision, /// Hijos a instanciar inmediatamente cuando esta Card se encarna. Se /// consumen una vez (no se replican en restarts del padre — el grafo /// re-emerge desde la Semilla viva, no desde la persistencia de la Card). #[serde(default)] pub genesis: Vec, } impl EntityCard { pub fn validate(&self) -> Result<(), CardError> { if self.schema_version != CARD_SCHEMA_VERSION { return Err(CardError::SchemaMismatch { got: self.schema_version, expected: CARD_SCHEMA_VERSION, }); } if self.label.is_empty() { return Err(CardError::EmptyLabel); } if self.label.len() > 256 { return Err(CardError::LabelTooLong(self.label.len())); } // Una capacidad simultáneamente en `requires` y `provides` indica un // ciclo de auto-dependencia que el grafo no puede resolver. for cap in &self.requires { if self.provides.contains(cap) { return Err(CardError::SelfDependency(cap.clone())); } } // Coherencia del payload con sus invariantes. validate_payload(&self.payload)?; // ResourceLimits: rangos sanos. validate_rlimits(&self.soma.rlimits)?; // Cgroup weights: 1..10000 según docs del kernel cgroup v2. validate_cgroup(&self.soma.cgroup)?; // Validación recursiva de genesis. Si una hija es inválida, la // Semilla entera se rechaza — falla rápida en boot. for child in &self.genesis { child.validate()?; } Ok(()) } } #[derive(Debug)] pub enum CardError { SchemaMismatch { got: u16, expected: u16 }, EmptyLabel, LabelTooLong(usize), SelfDependency(Capability), EmptyExec, SentinelWasmHash, InvalidRlimit(&'static str), InvalidCgroupWeight(&'static str), } impl fmt::Display for CardError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::SchemaMismatch { got, expected } => { write!(f, "schema version mismatch: got {got}, expected {expected}") } Self::EmptyLabel => write!(f, "card label is empty"), Self::LabelTooLong(n) => write!(f, "label demasiado largo ({n} bytes, max 256)"), Self::SelfDependency(c) => write!(f, "card both requires and provides {c:?}"), Self::EmptyExec => write!(f, "Native/Legacy payload con exec vacío"), Self::SentinelWasmHash => write!(f, "Wasm payload con sha256 sentinel (todo ceros)"), Self::InvalidRlimit(s) => write!(f, "rlimit inválido: {s}"), Self::InvalidCgroupWeight(s) => write!(f, "cgroup weight fuera de rango [1,10000]: {s}"), } } } impl std::error::Error for CardError {} fn validate_payload(p: &Payload) -> Result<(), CardError> { match p { Payload::Native { exec, .. } | Payload::Legacy { exec, .. } => { if exec.trim().is_empty() { return Err(CardError::EmptyExec); } } Payload::Wasm { module_sha256, .. } => { // Sentinel [0u8; 32] indica "no resoluble" — usado en dev como // fallback. En prod debe ser un hash real. if module_sha256.iter().all(|&b| b == 0) { return Err(CardError::SentinelWasmHash); } } Payload::Virtual => {} } Ok(()) } fn validate_rlimits(rl: &ResourceLimits) -> Result<(), CardError> { if let Some(m) = rl.mem_bytes { if m == 0 { return Err(CardError::InvalidRlimit("mem_bytes=0")); } if m > 1u64 << 40 { return Err(CardError::InvalidRlimit("mem_bytes>1TiB")); } } if let Some(n) = rl.nproc { if n == 0 || n > 65535 { return Err(CardError::InvalidRlimit("nproc fuera de [1,65535]")); } } if let Some(n) = rl.nofile { if n == 0 || n > 1_048_576 { return Err(CardError::InvalidRlimit("nofile fuera de [1,1M]")); } } Ok(()) } fn validate_cgroup(cg: &CgroupSpec) -> Result<(), CardError> { if let Some(w) = cg.cpu_weight { if !(1..=10000).contains(&w) { return Err(CardError::InvalidCgroupWeight("cpu_weight")); } } if let Some(w) = cg.io_weight { if !(1..=10000).contains(&w) { return Err(CardError::InvalidCgroupWeight("io_weight")); } } Ok(()) } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum Capability { /// Provee un punto de montaje root para Entes hijos. FilesystemRoot, /// Acceso a una familia de netlink. KernelNetlink(NetlinkFamily), /// Endpoint del bus interno del fractal — equivalente tipado de un nombre /// D-Bus, sin la string libre. Endpoint { interface: InterfaceId, version: u16 }, /// Reemplazo del shim de systemd-logind. Solo Ente #compat-logind lo provee. LegacyLogind, /// Acceso crudo a una clase de dispositivo. Capacidad escalada. Device { class: DeviceClass }, /// Permiso de instanciar Entes hijos. Por defecto solo PID 1 lo tiene. Spawn, /// Acceso a logging estructurado del fractal. Journal, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum NetlinkFamily { Uevent, Route, Generic, Audit } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum DeviceClass { Block, Tty, Input, Drm, Net, Hidraw } /// Identificador de interfaz del bus interno. UUID, no string. Para extender /// el protocolo del fractal, generas un UUID nuevo y versionas. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct InterfaceId(pub [u8; 16]); #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct SomaSpec { pub namespaces: NamespaceSet, pub rlimits: ResourceLimits, pub cgroup: CgroupSpec, pub cpu_affinity: Option>, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct NamespaceSet { pub mount: bool, pub pid: bool, pub net: bool, pub uts: bool, pub ipc: bool, pub user: bool, pub cgroup: bool, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ResourceLimits { pub mem_bytes: Option, pub nproc: Option, pub nofile: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct CgroupSpec { pub path: String, pub cpu_weight: Option, pub io_weight: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Payload { Wasm { module_sha256: [u8; 32], entry: String }, Native { exec: String, argv: Vec, envp: Vec<(String, String)>, }, /// Sin proceso. Nodo lógico del grafo (agregadores, mediators). Virtual, /// Wrapper de daemon legacy. `fakes` activa shims D-Bus / sd_notify. Legacy { exec: String, argv: Vec, fakes: BTreeSet, }, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum LegacyFacade { SystemdLogind, SystemdHostnamed, SystemdNotify, } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Supervision { Restart { #[serde(with = "duration_millis")] initial: Duration, #[serde(with = "duration_millis")] max: Duration, }, OneShot, Delegate, } mod duration_millis { use serde::{Deserialize, Deserializer, Serializer}; use std::time::Duration; pub fn serialize(d: &Duration, s: S) -> Result { s.serialize_u64(d.as_millis() as u64) } pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { let ms = u64::deserialize(d)?; Ok(Duration::from_millis(ms)) } } #[cfg(test)] mod tests { use super::*; #[test] fn seed_card_validates() { let card = EntityCard { schema_version: CARD_SCHEMA_VERSION, id: Ulid::new(), lineage: None, label: "ente-zero".into(), provides: [Capability::Spawn, Capability::Journal].into_iter().collect(), requires: BTreeSet::new(), soma: SomaSpec::default(), payload: Payload::Virtual, supervision: Supervision::OneShot, genesis: vec![], }; card.validate().unwrap(); } #[test] fn self_dependency_rejected() { let mut s = BTreeSet::new(); s.insert(Capability::Journal); let card = EntityCard { schema_version: CARD_SCHEMA_VERSION, id: Ulid::new(), lineage: None, label: "bad".into(), provides: s.clone(), requires: s, soma: SomaSpec::default(), payload: Payload::Virtual, supervision: Supervision::OneShot, genesis: vec![], }; assert!(matches!(card.validate(), Err(CardError::SelfDependency(_)))); } #[test] fn invalid_genesis_propagates() { let bad_child = EntityCard { schema_version: CARD_SCHEMA_VERSION, id: Ulid::new(), lineage: None, label: "".into(), provides: BTreeSet::new(), requires: BTreeSet::new(), soma: SomaSpec::default(), payload: Payload::Virtual, supervision: Supervision::OneShot, genesis: vec![], }; let parent = EntityCard { schema_version: CARD_SCHEMA_VERSION, id: Ulid::new(), lineage: None, label: "parent".into(), provides: BTreeSet::new(), requires: BTreeSet::new(), soma: SomaSpec::default(), payload: Payload::Virtual, supervision: Supervision::OneShot, genesis: vec![bad_child], }; assert!(matches!(parent.validate(), Err(CardError::EmptyLabel))); } }