tawasuyu

tawasuyu/arje

> Standalone repository, extracted from the tawasuyu monorepo at tag v0.3.3. The domain's code lives under 03ukupacha/arje/; the foundational crates it shares with the rest of the suite come in as git dependencies pinned to that tag. cargo…

Rust 38TOML 12Markdown 1395 Definiciones

⎇ d6b8f18b43c9bd9bf7268e48b61392a2d62bb229 crates/ente-card/src/lib.rs Rust · 326 líneas · 10.7 KB Historia Quién tocó qué crudo

Pausa: 11 crates del fractal Ente #0 con cerebro completo d6b8f18b4 · Sergio ·

1//! ente-card: definición de la Tarjeta de Identidad del Ente.
2//!
3//! Una `EntityCard` no describe un proceso — describe una identidad en el
4//! grafo del fractal. El Init la lee y decide cómo *encarnarla*: como ELF
5//! nativo, módulo Wasm, wrapper legacy, o nodo virtual sin proceso.
6
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeSet;
9use std::fmt;
10use std::time::Duration;
11use ulid::Ulid;
12
13/// Versión del esquema de la Card. Cambiar = romper compatibilidad del fractal.
14pub const CARD_SCHEMA_VERSION: u16 = 1;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct EntityCard {
18    pub schema_version: u16,
19    pub id: Ulid,
20    pub lineage: Option<Ulid>,
21    pub label: String,
22    pub provides: BTreeSet<Capability>,
23    pub requires: BTreeSet<Capability>,
24    pub soma: SomaSpec,
25    pub payload: Payload,
26    pub supervision: Supervision,
27    /// Hijos a instanciar inmediatamente cuando esta Card se encarna. Se
28    /// consumen una vez (no se replican en restarts del padre — el grafo
29    /// re-emerge desde la Semilla viva, no desde la persistencia de la Card).
30    #[serde(default)]
31    pub genesis: Vec<EntityCard>,
32}
33
34impl EntityCard {
35    pub fn validate(&self) -> Result<(), CardError> {
36        if self.schema_version != CARD_SCHEMA_VERSION {
37            return Err(CardError::SchemaMismatch {
38                got: self.schema_version,
39                expected: CARD_SCHEMA_VERSION,
40            });
41        }
42        if self.label.is_empty() {
43            return Err(CardError::EmptyLabel);
44        }
45        if self.label.len() > 256 {
46            return Err(CardError::LabelTooLong(self.label.len()));
47        }
48        // Una capacidad simultáneamente en `requires` y `provides` indica un
49        // ciclo de auto-dependencia que el grafo no puede resolver.
50        for cap in &self.requires {
51            if self.provides.contains(cap) {
52                return Err(CardError::SelfDependency(cap.clone()));
53            }
54        }
55        // Coherencia del payload con sus invariantes.
56        validate_payload(&self.payload)?;
57        // ResourceLimits: rangos sanos.
58        validate_rlimits(&self.soma.rlimits)?;
59        // Cgroup weights: 1..10000 según docs del kernel cgroup v2.
60        validate_cgroup(&self.soma.cgroup)?;
61        // Validación recursiva de genesis. Si una hija es inválida, la
62        // Semilla entera se rechaza — falla rápida en boot.
63        for child in &self.genesis {
64            child.validate()?;
65        }
66        Ok(())
67    }
68}
69
70#[derive(Debug)]
71pub enum CardError {
72    SchemaMismatch { got: u16, expected: u16 },
73    EmptyLabel,
74    LabelTooLong(usize),
75    SelfDependency(Capability),
76    EmptyExec,
77    SentinelWasmHash,
78    InvalidRlimit(&'static str),
79    InvalidCgroupWeight(&'static str),
80}
81
82impl fmt::Display for CardError {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        match self {
85            Self::SchemaMismatch { got, expected } => {
86                write!(f, "schema version mismatch: got {got}, expected {expected}")
87            }
88            Self::EmptyLabel => write!(f, "card label is empty"),
89            Self::LabelTooLong(n) => write!(f, "label demasiado largo ({n} bytes, max 256)"),
90            Self::SelfDependency(c) => write!(f, "card both requires and provides {c:?}"),
91            Self::EmptyExec => write!(f, "Native/Legacy payload con exec vacío"),
92            Self::SentinelWasmHash => write!(f, "Wasm payload con sha256 sentinel (todo ceros)"),
93            Self::InvalidRlimit(s) => write!(f, "rlimit inválido: {s}"),
94            Self::InvalidCgroupWeight(s) => write!(f, "cgroup weight fuera de rango [1,10000]: {s}"),
95        }
96    }
97}
98
99impl std::error::Error for CardError {}
100
101fn validate_payload(p: &Payload) -> Result<(), CardError> {
102    match p {
103        Payload::Native { exec, .. } | Payload::Legacy { exec, .. } => {
104            if exec.trim().is_empty() {
105                return Err(CardError::EmptyExec);
106            }
107        }
108        Payload::Wasm { module_sha256, .. } => {
109            // Sentinel [0u8; 32] indica "no resoluble" — usado en dev como
110            // fallback. En prod debe ser un hash real.
111            if module_sha256.iter().all(|&b| b == 0) {
112                return Err(CardError::SentinelWasmHash);
113            }
114        }
115        Payload::Virtual => {}
116    }
117    Ok(())
118}
119
120fn validate_rlimits(rl: &ResourceLimits) -> Result<(), CardError> {
121    if let Some(m) = rl.mem_bytes {
122        if m == 0 { return Err(CardError::InvalidRlimit("mem_bytes=0")); }
123        if m > 1u64 << 40 { return Err(CardError::InvalidRlimit("mem_bytes>1TiB")); }
124    }
125    if let Some(n) = rl.nproc {
126        if n == 0 || n > 65535 { return Err(CardError::InvalidRlimit("nproc fuera de [1,65535]")); }
127    }
128    if let Some(n) = rl.nofile {
129        if n == 0 || n > 1_048_576 { return Err(CardError::InvalidRlimit("nofile fuera de [1,1M]")); }
130    }
131    Ok(())
132}
133
134fn validate_cgroup(cg: &CgroupSpec) -> Result<(), CardError> {
135    if let Some(w) = cg.cpu_weight {
136        if !(1..=10000).contains(&w) { return Err(CardError::InvalidCgroupWeight("cpu_weight")); }
137    }
138    if let Some(w) = cg.io_weight {
139        if !(1..=10000).contains(&w) { return Err(CardError::InvalidCgroupWeight("io_weight")); }
140    }
141    Ok(())
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
145pub enum Capability {
146    /// Provee un punto de montaje root para Entes hijos.
147    FilesystemRoot,
148    /// Acceso a una familia de netlink.
149    KernelNetlink(NetlinkFamily),
150    /// Endpoint del bus interno del fractal — equivalente tipado de un nombre
151    /// D-Bus, sin la string libre.
152    Endpoint { interface: InterfaceId, version: u16 },
153    /// Reemplazo del shim de systemd-logind. Solo Ente #compat-logind lo provee.
154    LegacyLogind,
155    /// Acceso crudo a una clase de dispositivo. Capacidad escalada.
156    Device { class: DeviceClass },
157    /// Permiso de instanciar Entes hijos. Por defecto solo PID 1 lo tiene.
158    Spawn,
159    /// Acceso a logging estructurado del fractal.
160    Journal,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
164pub enum NetlinkFamily { Uevent, Route, Generic, Audit }
165
166#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
167pub enum DeviceClass { Block, Tty, Input, Drm, Net, Hidraw }
168
169/// Identificador de interfaz del bus interno. UUID, no string. Para extender
170/// el protocolo del fractal, generas un UUID nuevo y versionas.
171#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
172pub struct InterfaceId(pub [u8; 16]);
173
174#[derive(Debug, Clone, Default, Serialize, Deserialize)]
175pub struct SomaSpec {
176    pub namespaces: NamespaceSet,
177    pub rlimits: ResourceLimits,
178    pub cgroup: CgroupSpec,
179    pub cpu_affinity: Option<Vec<u32>>,
180}
181
182#[derive(Debug, Clone, Default, Serialize, Deserialize)]
183pub struct NamespaceSet {
184    pub mount: bool,
185    pub pid: bool,
186    pub net: bool,
187    pub uts: bool,
188    pub ipc: bool,
189    pub user: bool,
190    pub cgroup: bool,
191}
192
193#[derive(Debug, Clone, Default, Serialize, Deserialize)]
194pub struct ResourceLimits {
195    pub mem_bytes: Option<u64>,
196    pub nproc: Option<u32>,
197    pub nofile: Option<u32>,
198}
199
200#[derive(Debug, Clone, Default, Serialize, Deserialize)]
201pub struct CgroupSpec {
202    pub path: String,
203    pub cpu_weight: Option<u32>,
204    pub io_weight: Option<u32>,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub enum Payload {
209    Wasm { module_sha256: [u8; 32], entry: String },
210    Native {
211        exec: String,
212        argv: Vec<String>,
213        envp: Vec<(String, String)>,
214    },
215    /// Sin proceso. Nodo lógico del grafo (agregadores, mediators).
216    Virtual,
217    /// Wrapper de daemon legacy. `fakes` activa shims D-Bus / sd_notify.
218    Legacy {
219        exec: String,
220        argv: Vec<String>,
221        fakes: BTreeSet<LegacyFacade>,
222    },
223}
224
225#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
226pub enum LegacyFacade {
227    SystemdLogind,
228    SystemdHostnamed,
229    SystemdNotify,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub enum Supervision {
234    Restart {
235        #[serde(with = "duration_millis")]
236        initial: Duration,
237        #[serde(with = "duration_millis")]
238        max: Duration,
239    },
240    OneShot,
241    Delegate,
242}
243
244mod duration_millis {
245    use serde::{Deserialize, Deserializer, Serializer};
246    use std::time::Duration;
247
248    pub fn serialize<S: Serializer>(d: &Duration, s: S) -> Result<S::Ok, S::Error> {
249        s.serialize_u64(d.as_millis() as u64)
250    }
251
252    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Duration, D::Error> {
253        let ms = u64::deserialize(d)?;
254        Ok(Duration::from_millis(ms))
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn seed_card_validates() {
264        let card = EntityCard {
265            schema_version: CARD_SCHEMA_VERSION,
266            id: Ulid::new(),
267            lineage: None,
268            label: "ente-zero".into(),
269            provides: [Capability::Spawn, Capability::Journal].into_iter().collect(),
270            requires: BTreeSet::new(),
271            soma: SomaSpec::default(),
272            payload: Payload::Virtual,
273            supervision: Supervision::OneShot,
274            genesis: vec![],
275        };
276        card.validate().unwrap();
277    }
278
279    #[test]
280    fn self_dependency_rejected() {
281        let mut s = BTreeSet::new();
282        s.insert(Capability::Journal);
283        let card = EntityCard {
284            schema_version: CARD_SCHEMA_VERSION,
285            id: Ulid::new(),
286            lineage: None,
287            label: "bad".into(),
288            provides: s.clone(),
289            requires: s,
290            soma: SomaSpec::default(),
291            payload: Payload::Virtual,
292            supervision: Supervision::OneShot,
293            genesis: vec![],
294        };
295        assert!(matches!(card.validate(), Err(CardError::SelfDependency(_))));
296    }
297
298    #[test]
299    fn invalid_genesis_propagates() {
300        let bad_child = EntityCard {
301            schema_version: CARD_SCHEMA_VERSION,
302            id: Ulid::new(),
303            lineage: None,
304            label: "".into(),
305            provides: BTreeSet::new(),
306            requires: BTreeSet::new(),
307            soma: SomaSpec::default(),
308            payload: Payload::Virtual,
309            supervision: Supervision::OneShot,
310            genesis: vec![],
311        };
312        let parent = EntityCard {
313            schema_version: CARD_SCHEMA_VERSION,
314            id: Ulid::new(),
315            lineage: None,
316            label: "parent".into(),
317            provides: BTreeSet::new(),
318            requires: BTreeSet::new(),
319            soma: SomaSpec::default(),
320            payload: Payload::Virtual,
321            supervision: Supervision::OneShot,
322            genesis: vec![bad_child],
323        };
324        assert!(matches!(parent.validate(), Err(CardError::EmptyLabel)));
325    }
326}