chore: monorepo inicial con arje + minga + yahweh absorbidos
Workspace en 4 ejes (core/modules/apps/shared):
- core/: 24 crates de arje (Init systemd-compatible: ente-card, ente-zero,
ente-kernel, ente-bus, ente-cas, ente-soma, ente-wasm, ente-snapshot,
ente-brain, ente-echo, ente-policy-provider, + 12 crates *-compat)
- modules/semantic_dht/: 5 crates de minga (minga-core con AST/CAS/MST,
minga-p2p con libp2p Kad, minga-store, minga-vfs, minga-cli)
- modules/ui_engine/: 11 crates de yahweh (libs/{core,theme,bus,providers},
widgets/{tree,splitter,tabs,tiled,container_core,text_input})
- apps/: 5 crates de yahweh (file_explorer, database_explorer, text_viewer,
image_viewer, yahweh-shell)
- shared_wit/protocol.wit: handshake/lifecycle inicial
Cargo.toml unificado: thiserror bumped a 2 (transparente para arje), tokio
"full", paths intra-workspace de yahweh redirigidos a su nueva ubicación.
cargo check --workspace: 0 errores, 17 warnings (dead code preexistente).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
//! ente-card: definición de la Tarjeta de Identidad del Ente.
|
||||
//!
|
||||
//! 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.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
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 const CARD_SCHEMA_VERSION: u16 = 1;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EntityCard {
|
||||
pub schema_version: u16,
|
||||
pub id: Ulid,
|
||||
pub lineage: Option<Ulid>,
|
||||
pub label: String,
|
||||
pub provides: BTreeSet<Capability>,
|
||||
pub requires: BTreeSet<Capability>,
|
||||
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<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)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user