Pausa: 11 crates del fractal Ente #0 con cerebro completo
PID 1 boot + bus interno autenticado + cerebro KCL/Rust: - 6 lib crates de infra (card, bus, cas, kernel, soma, wasm, snapshot) - ente-brain: motor de reglas O(1), observer Shannon, cristalización, audit hash-chain, persistencia rules.k, Prometheus /metrics - KCL schemas card.k + rule.k como gramática autoritativa - compat-logind D-Bus, ente-echo demo provider, ente-zero PID 1 - 22 tests OK, ~3.8k LOC Rust + ~300 LOC KCL Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
//! Construcción de la Tarjeta Semilla.
|
||||
//!
|
||||
//! Tres caminos:
|
||||
//! 1. `--restore <path>`: leer `FractalSnapshot` y reconstruir Semilla
|
||||
//! con seed_id preservado + entes anteriores como genesis.
|
||||
//! 2. `seed.card` en disco: deserialize directo (prod o dev).
|
||||
//! 3. Fallback dev: sintetizar Semilla + 6 genesis Entes que ejercitan
|
||||
//! todas las capacidades del fractal.
|
||||
|
||||
use anyhow::Context;
|
||||
use ente_card::{
|
||||
Capability, CardError, CgroupSpec, EntityCard, NamespaceSet, Payload,
|
||||
ResourceLimits, SomaSpec, Supervision, CARD_SCHEMA_VERSION,
|
||||
};
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
use tracing::{info, warn};
|
||||
use ulid::Ulid;
|
||||
|
||||
const SEED_PATH_PROD: &str = "/ente/seed.card";
|
||||
const SEED_PATH_DEV: &str = "seed.card";
|
||||
|
||||
pub fn load(dev_mode: bool, restore: Option<&Path>) -> anyhow::Result<EntityCard> {
|
||||
let card = if let Some(path) = restore {
|
||||
load_from_snapshot(path)?
|
||||
} else {
|
||||
load_or_synthesize(dev_mode)?
|
||||
};
|
||||
card.validate()
|
||||
.map_err(|e: CardError| anyhow::anyhow!("semilla inválida: {e}"))?;
|
||||
Ok(card)
|
||||
}
|
||||
|
||||
fn load_from_snapshot(path: &Path) -> anyhow::Result<EntityCard> {
|
||||
let snap = ente_snapshot::FractalSnapshot::read(path)
|
||||
.with_context(|| format!("read snapshot {}", path.display()))?;
|
||||
info!(
|
||||
path = %path.display(),
|
||||
seed_id = %snap.seed_id,
|
||||
entes = snap.entes.len(),
|
||||
timestamp_ms = snap.timestamp_ms,
|
||||
"snapshot cargado, restaurando fractal"
|
||||
);
|
||||
// Reconstruimos la Semilla con su Ulid original. Las Cards persistidas
|
||||
// van a `genesis` con sus Ulids preservados — son las mismas identidades
|
||||
// que vivieron antes del checkpoint.
|
||||
let mut provides = BTreeSet::new();
|
||||
provides.insert(Capability::Spawn);
|
||||
provides.insert(Capability::Journal);
|
||||
Ok(EntityCard {
|
||||
schema_version: CARD_SCHEMA_VERSION,
|
||||
id: snap.seed_id,
|
||||
lineage: None,
|
||||
label: snap.seed_label,
|
||||
provides,
|
||||
requires: BTreeSet::new(),
|
||||
soma: SomaSpec::default(),
|
||||
payload: Payload::Virtual,
|
||||
supervision: Supervision::OneShot,
|
||||
genesis: snap.entes,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_or_synthesize(dev_mode: bool) -> anyhow::Result<EntityCard> {
|
||||
// Buscamos primero `.k` (KCL canónico, validado por su schema), luego
|
||||
// `.json` para compatibilidad. La puerta genética se cruza vía
|
||||
// `ente_brain::load_card_file` que pasa por `validate()` extendido.
|
||||
let candidates: &[&str] = if dev_mode {
|
||||
&["seed.card.k", SEED_PATH_DEV]
|
||||
} else {
|
||||
&["/ente/seed.card.k", SEED_PATH_PROD]
|
||||
};
|
||||
for cand in candidates {
|
||||
let path = PathBuf::from(cand);
|
||||
if !path.exists() { continue; }
|
||||
let card = ente_brain::load_card_file(&path)
|
||||
.with_context(|| format!("load {}", path.display()))?;
|
||||
info!(path = %path.display(), "Tarjeta Semilla cargada y validada");
|
||||
return Ok(card);
|
||||
}
|
||||
if dev_mode {
|
||||
info!("sin seed.card — sintetizando semilla mínima (dev)");
|
||||
return Ok(synthesize_dev_seed());
|
||||
}
|
||||
anyhow::bail!("seed.card no encontrada en /ente/seed.card[.k]")
|
||||
}
|
||||
|
||||
fn synthesize_dev_seed() -> EntityCard {
|
||||
let mut provides = BTreeSet::new();
|
||||
provides.insert(Capability::Spawn);
|
||||
provides.insert(Capability::Journal);
|
||||
|
||||
// Pre-registramos el módulo Wasm demo en el CAS y obtenemos su SHA real.
|
||||
// Si el CAS no es escribible (raro en dev) caemos a un SHA cero — la
|
||||
// resolución fallará y el Wasm no encarnará, pero el resto queda intacto.
|
||||
let demo_wasm_sha = match ente_wasm::demo_module_bytes()
|
||||
.and_then(|b| ente_cas::store(&b))
|
||||
{
|
||||
Ok(sha) => sha,
|
||||
Err(e) => {
|
||||
warn!(?e, "CAS no disponible — demo-wasm no encarnará");
|
||||
[0u8; 32]
|
||||
}
|
||||
};
|
||||
|
||||
let mut genesis = Vec::new();
|
||||
genesis.push(make_card("demo-sleep", Payload::Native {
|
||||
exec: "/bin/sleep".into(), argv: vec!["1".into()], envp: vec![],
|
||||
}, Supervision::OneShot));
|
||||
|
||||
genesis.push(make_card("demo-persist", Payload::Native {
|
||||
exec: "/bin/sleep".into(), argv: vec!["60".into()], envp: vec![],
|
||||
}, restart_supervision()));
|
||||
|
||||
// Card namespaced: padre escribe uid_map, hijo cat /proc/self/uid_map.
|
||||
let mut ns_card = make_card("demo-userns", Payload::Native {
|
||||
exec: "/bin/cat".into(),
|
||||
argv: vec!["/proc/self/uid_map".into()],
|
||||
envp: vec![],
|
||||
}, Supervision::OneShot);
|
||||
ns_card.soma = SomaSpec {
|
||||
namespaces: NamespaceSet { user: true, ..Default::default() },
|
||||
..Default::default()
|
||||
};
|
||||
genesis.push(ns_card);
|
||||
|
||||
genesis.push(make_card("demo-wasm", Payload::Wasm {
|
||||
module_sha256: demo_wasm_sha,
|
||||
entry: "_start".into(),
|
||||
}, Supervision::OneShot));
|
||||
|
||||
if let Some(card) = optional_native_card(
|
||||
"demo-echo", "target/debug/ente-echo",
|
||||
[ente_echo::echo_capability()].into_iter().collect(),
|
||||
restart_supervision(),
|
||||
) {
|
||||
genesis.push(card);
|
||||
}
|
||||
|
||||
if let Some(card) = optional_native_card(
|
||||
"compat-logind", "target/debug/ente-logind-compat",
|
||||
[Capability::LegacyLogind].into_iter().collect(),
|
||||
restart_supervision(),
|
||||
) {
|
||||
genesis.push(card);
|
||||
}
|
||||
|
||||
EntityCard {
|
||||
schema_version: CARD_SCHEMA_VERSION,
|
||||
id: Ulid::new(),
|
||||
lineage: None,
|
||||
label: "ente-zero-dev".into(),
|
||||
provides,
|
||||
requires: BTreeSet::new(),
|
||||
soma: SomaSpec {
|
||||
namespaces: NamespaceSet::default(),
|
||||
rlimits: ResourceLimits::default(),
|
||||
cgroup: CgroupSpec {
|
||||
path: "ente.slice/zero".into(),
|
||||
cpu_weight: None,
|
||||
io_weight: None,
|
||||
},
|
||||
cpu_affinity: None,
|
||||
},
|
||||
payload: Payload::Virtual,
|
||||
supervision: Supervision::OneShot,
|
||||
genesis,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_card(label: &str, payload: Payload, supervision: Supervision) -> EntityCard {
|
||||
EntityCard {
|
||||
schema_version: CARD_SCHEMA_VERSION,
|
||||
id: Ulid::new(),
|
||||
lineage: None,
|
||||
label: label.into(),
|
||||
provides: BTreeSet::new(),
|
||||
requires: BTreeSet::new(),
|
||||
soma: SomaSpec::default(),
|
||||
payload,
|
||||
supervision,
|
||||
genesis: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn optional_native_card(
|
||||
label: &str,
|
||||
bin_path: &str,
|
||||
provides: BTreeSet<Capability>,
|
||||
supervision: Supervision,
|
||||
) -> Option<EntityCard> {
|
||||
let path = Path::new(bin_path);
|
||||
if !path.exists() {
|
||||
return None;
|
||||
}
|
||||
Some(EntityCard {
|
||||
schema_version: CARD_SCHEMA_VERSION,
|
||||
id: Ulid::new(),
|
||||
lineage: None,
|
||||
label: label.into(),
|
||||
provides,
|
||||
requires: BTreeSet::new(),
|
||||
soma: SomaSpec::default(),
|
||||
payload: Payload::Native {
|
||||
exec: path.to_string_lossy().into_owned(),
|
||||
argv: vec![],
|
||||
envp: vec![],
|
||||
},
|
||||
supervision,
|
||||
genesis: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
fn restart_supervision() -> Supervision {
|
||||
Supervision::Restart {
|
||||
initial: Duration::from_millis(100),
|
||||
max: Duration::from_secs(30),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user