diff --git a/Cargo.lock b/Cargo.lock index de3b45f..5f24f23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2494,9 +2494,7 @@ dependencies = [ name = "ente-card" version = "0.0.1" dependencies = [ - "serde", - "serde_json", - "ulid", + "brahman-card", ] [[package]] diff --git a/crates/core/brahman-card/src/lib.rs b/crates/core/brahman-card/src/lib.rs index 7f19910..0897075 100644 --- a/crates/core/brahman-card/src/lib.rs +++ b/crates/core/brahman-card/src/lib.rs @@ -131,6 +131,31 @@ pub struct Card { pub extensions: HashMap, } +impl Default for Card { + /// Default razonable para `..Default::default()` en struct-literals. + /// `id` queda en `Ulid::nil()` y `label` vacío — el consumidor debe + /// sobreescribirlos antes de validar. + fn default() -> Self { + Self { + schema_version: CARD_SCHEMA_VERSION, + id: Ulid::nil(), + lineage: None, + label: String::new(), + provides: BTreeSet::new(), + requires: BTreeSet::new(), + permissions: Permissions::default(), + soma: SomaSpec::default(), + payload: Payload::Virtual, + supervision: Supervision::OneShot, + lifecycle: Lifecycle::default(), + priority: Priority::default(), + flow: Flows::default(), + genesis: Vec::new(), + extensions: HashMap::new(), + } + } +} + // ===================================================================== // Capacidades — heredadas de arje, tipadas, no strings // ===================================================================== diff --git a/crates/core/ente-card/Cargo.toml b/crates/core/ente-card/Cargo.toml index f6352b3..b4dcdfc 100644 --- a/crates/core/ente-card/Cargo.toml +++ b/crates/core/ente-card/Cargo.toml @@ -4,8 +4,7 @@ version = "0.0.1" edition.workspace = true license.workspace = true publish.workspace = true +description = "Alias histórico de brahman-card. Re-exporta tipos legacy (EntityCard ≡ Card)." [dependencies] -serde = { workspace = true } -serde_json = { workspace = true } -ulid = { workspace = true } +brahman-card = { path = "../brahman-card" } diff --git a/crates/core/ente-card/src/lib.rs b/crates/core/ente-card/src/lib.rs index 4449071..887d4b8 100644 --- a/crates/core/ente-card/src/lib.rs +++ b/crates/core/ente-card/src/lib.rs @@ -1,326 +1,30 @@ -//! ente-card: definición de la Tarjeta de Identidad del Ente. +//! `ente-card` — alias histórico de [`brahman_card`]. //! -//! 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. +//! Mantenido como compatibilidad para los crates `ente-*` del Init que +//! importan `EntityCard`, `Capability`, `Payload`, etc. La fuente de verdad +//! del schema vive en [`brahman_card`]; este crate sólo re-exporta los tipos +//! bajo sus nombres legacy: +//! +//! - `EntityCard` ≡ [`brahman_card::Card`] +//! - El resto de tipos conservan el mismo nombre. +//! +//! Toda lógica nueva debe consumir directamente `brahman_card`. -use serde::{Deserialize, Serialize}; -use std::collections::BTreeSet; -use std::fmt; -use std::time::Duration; -use ulid::Ulid; +#![forbid(unsafe_code)] -/// 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))); - } -} +pub use brahman_card::{ + Capability, + CardError, + Card as EntityCard, + CgroupSpec, + DeviceClass, + InterfaceId, + LegacyFacade, + NamespaceSet, + NetlinkFamily, + Payload, + ResourceLimits, + SomaSpec, + Supervision, + CARD_SCHEMA_VERSION, +}; diff --git a/crates/core/ente-zero/src/seed.rs b/crates/core/ente-zero/src/seed.rs index 376bfcf..fe01689 100644 --- a/crates/core/ente-zero/src/seed.rs +++ b/crates/core/ente-zero/src/seed.rs @@ -59,6 +59,7 @@ fn load_from_snapshot(path: &Path) -> anyhow::Result { payload: Payload::Virtual, supervision: Supervision::OneShot, genesis: snap.entes, + ..Default::default() }) } @@ -191,6 +192,7 @@ fn synthesize_dev_seed() -> EntityCard { payload: Payload::Virtual, supervision: Supervision::OneShot, genesis, + ..Default::default() } } @@ -206,6 +208,7 @@ fn make_card(label: &str, payload: Payload, supervision: Supervision) -> EntityC payload, supervision, genesis: vec![], + ..Default::default() } } @@ -234,6 +237,7 @@ fn optional_native_card( }, supervision, genesis: vec![], + ..Default::default() }) }