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…
⎇ d6b8f18b43c9bd9bf7268e48b61392a2d62bb229
crates/ente-card/src/lib.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
Pausa: 11 crates del fractal Ente #0 con cerebro completo
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}