refactor(arje): migra ente-card a re-export de brahman-card

ente-card pasa a ser un crate-shim que re-exporta los tipos de
brahman-card bajo sus nombres legacy:

- EntityCard ≡ brahman_card::Card (alias)
- Capability, Payload, SomaSpec, Supervision, etc. — pub use directo

Cambios concretos:

- crates/core/brahman-card/src/lib.rs: añade impl Default for Card.
  Permite usar `..Default::default()` en struct-literals para los
  campos aditivos (permissions, lifecycle, priority, flow, extensions).
- crates/core/ente-card/src/lib.rs: reescrito como shim de re-export
  (~25 líneas). Las definiciones, validaciones y tests viven en
  brahman-card.
- crates/core/ente-card/Cargo.toml: deps reducidas a brahman-card; se
  eliminan serde/serde_json/ulid (vienen transitivos vía re-export).
- crates/core/ente-zero/src/seed.rs: 4 struct-literals de EntityCard
  ahora terminan con `..Default::default()` para cubrir los nuevos
  campos del schema híbrido.

Los 21 consumidores de ente-card (ente-zero, ente-bus, ente-brain,
ente-soma, ente-cas, los 12 *-compat, etc.) compilan sin cambios —
sus `use ente_card::EntityCard` y demás imports siguen resolviendo,
ahora a tipos de brahman-card.

cargo test -p brahman-card: 8/8.
cargo build -p ente-zero: OK.
cargo check --workspace: 0 errores.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-08 12:10:35 +00:00
parent 0feba74503
commit ed0e973c81
5 changed files with 59 additions and 329 deletions
Generated
+1 -3
View File
@@ -2494,9 +2494,7 @@ dependencies = [
name = "ente-card" name = "ente-card"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"serde", "brahman-card",
"serde_json",
"ulid",
] ]
[[package]] [[package]]
+25
View File
@@ -131,6 +131,31 @@ pub struct Card {
pub extensions: HashMap<String, Value>, pub extensions: HashMap<String, Value>,
} }
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 // Capacidades — heredadas de arje, tipadas, no strings
// ===================================================================== // =====================================================================
+2 -3
View File
@@ -4,8 +4,7 @@ version = "0.0.1"
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
publish.workspace = true publish.workspace = true
description = "Alias histórico de brahman-card. Re-exporta tipos legacy (EntityCard ≡ Card)."
[dependencies] [dependencies]
serde = { workspace = true } brahman-card = { path = "../brahman-card" }
serde_json = { workspace = true }
ulid = { workspace = true }
+26 -322
View File
@@ -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 //! Mantenido como compatibilidad para los crates `ente-*` del Init que
//! grafo del fractal. El Init la lee y decide cómo *encarnarla*: como ELF //! importan `EntityCard`, `Capability`, `Payload`, etc. La fuente de verdad
//! nativo, módulo Wasm, wrapper legacy, o nodo virtual sin proceso. //! 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}; #![forbid(unsafe_code)]
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 use brahman_card::{
pub const CARD_SCHEMA_VERSION: u16 = 1; Capability,
CardError,
#[derive(Debug, Clone, Serialize, Deserialize)] Card as EntityCard,
pub struct EntityCard { CgroupSpec,
pub schema_version: u16, DeviceClass,
pub id: Ulid, InterfaceId,
pub lineage: Option<Ulid>, LegacyFacade,
pub label: String, NamespaceSet,
pub provides: BTreeSet<Capability>, NetlinkFamily,
pub requires: BTreeSet<Capability>, Payload,
pub soma: SomaSpec, ResourceLimits,
pub payload: Payload, SomaSpec,
pub supervision: Supervision, Supervision,
/// Hijos a instanciar inmediatamente cuando esta Card se encarna. Se CARD_SCHEMA_VERSION,
/// 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<EntityCard>,
}
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<Vec<u32>>,
}
#[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<u64>,
pub nproc: Option<u32>,
pub nofile: Option<u32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CgroupSpec {
pub path: String,
pub cpu_weight: Option<u32>,
pub io_weight: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Payload {
Wasm { module_sha256: [u8; 32], entry: String },
Native {
exec: String,
argv: Vec<String>,
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<String>,
fakes: BTreeSet<LegacyFacade>,
},
}
#[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<S: Serializer>(d: &Duration, s: S) -> Result<S::Ok, S::Error> {
s.serialize_u64(d.as_millis() as u64)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Duration, D::Error> {
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)));
}
}
+4
View File
@@ -59,6 +59,7 @@ fn load_from_snapshot(path: &Path) -> anyhow::Result<EntityCard> {
payload: Payload::Virtual, payload: Payload::Virtual,
supervision: Supervision::OneShot, supervision: Supervision::OneShot,
genesis: snap.entes, genesis: snap.entes,
..Default::default()
}) })
} }
@@ -191,6 +192,7 @@ fn synthesize_dev_seed() -> EntityCard {
payload: Payload::Virtual, payload: Payload::Virtual,
supervision: Supervision::OneShot, supervision: Supervision::OneShot,
genesis, genesis,
..Default::default()
} }
} }
@@ -206,6 +208,7 @@ fn make_card(label: &str, payload: Payload, supervision: Supervision) -> EntityC
payload, payload,
supervision, supervision,
genesis: vec![], genesis: vec![],
..Default::default()
} }
} }
@@ -234,6 +237,7 @@ fn optional_native_card(
}, },
supervision, supervision,
genesis: vec![], genesis: vec![],
..Default::default()
}) })
} }