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:
Generated
+1
-3
@@ -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]]
|
||||||
|
|||||||
@@ -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
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|||||||
@@ -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 }
|
|
||||||
|
|||||||
@@ -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)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user