gioser
⎇ d6b8f18b43c9bd9bf7268e48b61392a2d62bb229 crates/ente-zero/src/seed.rs Rust · 220 líneas · 7.0 KB Historia Quién tocó qué crudo

Pausa: 11 crates del fractal Ente #0 con cerebro completo d6b8f18b4 · Sergio ·

1//! Construcción de la Tarjeta Semilla.
2//!
3//! Tres caminos:
4//!   1. `--restore <path>`: leer `FractalSnapshot` y reconstruir Semilla
5//!      con seed_id preservado + entes anteriores como genesis.
6//!   2. `seed.card` en disco: deserialize directo (prod o dev).
7//!   3. Fallback dev: sintetizar Semilla + 6 genesis Entes que ejercitan
8//!      todas las capacidades del fractal.
9
10use anyhow::Context;
11use ente_card::{
12    Capability, CardError, CgroupSpec, EntityCard, NamespaceSet, Payload,
13    ResourceLimits, SomaSpec, Supervision, CARD_SCHEMA_VERSION,
14};
15use std::collections::BTreeSet;
16use std::path::{Path, PathBuf};
17use std::time::Duration;
18use tracing::{info, warn};
19use ulid::Ulid;
20
21const SEED_PATH_PROD: &str = "/ente/seed.card";
22const SEED_PATH_DEV: &str = "seed.card";
23
24pub fn load(dev_mode: bool, restore: Option<&Path>) -> anyhow::Result<EntityCard> {
25    let card = if let Some(path) = restore {
26        load_from_snapshot(path)?
27    } else {
28        load_or_synthesize(dev_mode)?
29    };
30    card.validate()
31        .map_err(|e: CardError| anyhow::anyhow!("semilla inválida: {e}"))?;
32    Ok(card)
33}
34
35fn load_from_snapshot(path: &Path) -> anyhow::Result<EntityCard> {
36    let snap = ente_snapshot::FractalSnapshot::read(path)
37        .with_context(|| format!("read snapshot {}", path.display()))?;
38    info!(
39        path = %path.display(),
40        seed_id = %snap.seed_id,
41        entes = snap.entes.len(),
42        timestamp_ms = snap.timestamp_ms,
43        "snapshot cargado, restaurando fractal"
44    );
45    // Reconstruimos la Semilla con su Ulid original. Las Cards persistidas
46    // van a `genesis` con sus Ulids preservados — son las mismas identidades
47    // que vivieron antes del checkpoint.
48    let mut provides = BTreeSet::new();
49    provides.insert(Capability::Spawn);
50    provides.insert(Capability::Journal);
51    Ok(EntityCard {
52        schema_version: CARD_SCHEMA_VERSION,
53        id: snap.seed_id,
54        lineage: None,
55        label: snap.seed_label,
56        provides,
57        requires: BTreeSet::new(),
58        soma: SomaSpec::default(),
59        payload: Payload::Virtual,
60        supervision: Supervision::OneShot,
61        genesis: snap.entes,
62    })
63}
64
65fn load_or_synthesize(dev_mode: bool) -> anyhow::Result<EntityCard> {
66    // Buscamos primero `.k` (KCL canónico, validado por su schema), luego
67    // `.json` para compatibilidad. La puerta genética se cruza vía
68    // `ente_brain::load_card_file` que pasa por `validate()` extendido.
69    let candidates: &[&str] = if dev_mode {
70        &["seed.card.k", SEED_PATH_DEV]
71    } else {
72        &["/ente/seed.card.k", SEED_PATH_PROD]
73    };
74    for cand in candidates {
75        let path = PathBuf::from(cand);
76        if !path.exists() { continue; }
77        let card = ente_brain::load_card_file(&path)
78            .with_context(|| format!("load {}", path.display()))?;
79        info!(path = %path.display(), "Tarjeta Semilla cargada y validada");
80        return Ok(card);
81    }
82    if dev_mode {
83        info!("sin seed.card — sintetizando semilla mínima (dev)");
84        return Ok(synthesize_dev_seed());
85    }
86    anyhow::bail!("seed.card no encontrada en /ente/seed.card[.k]")
87}
88
89fn synthesize_dev_seed() -> EntityCard {
90    let mut provides = BTreeSet::new();
91    provides.insert(Capability::Spawn);
92    provides.insert(Capability::Journal);
93
94    // Pre-registramos el módulo Wasm demo en el CAS y obtenemos su SHA real.
95    // Si el CAS no es escribible (raro en dev) caemos a un SHA cero — la
96    // resolución fallará y el Wasm no encarnará, pero el resto queda intacto.
97    let demo_wasm_sha = match ente_wasm::demo_module_bytes()
98        .and_then(|b| ente_cas::store(&b))
99    {
100        Ok(sha) => sha,
101        Err(e) => {
102            warn!(?e, "CAS no disponible — demo-wasm no encarnará");
103            [0u8; 32]
104        }
105    };
106
107    let mut genesis = Vec::new();
108    genesis.push(make_card("demo-sleep", Payload::Native {
109        exec: "/bin/sleep".into(), argv: vec!["1".into()], envp: vec![],
110    }, Supervision::OneShot));
111
112    genesis.push(make_card("demo-persist", Payload::Native {
113        exec: "/bin/sleep".into(), argv: vec!["60".into()], envp: vec![],
114    }, restart_supervision()));
115
116    // Card namespaced: padre escribe uid_map, hijo cat /proc/self/uid_map.
117    let mut ns_card = make_card("demo-userns", Payload::Native {
118        exec: "/bin/cat".into(),
119        argv: vec!["/proc/self/uid_map".into()],
120        envp: vec![],
121    }, Supervision::OneShot);
122    ns_card.soma = SomaSpec {
123        namespaces: NamespaceSet { user: true, ..Default::default() },
124        ..Default::default()
125    };
126    genesis.push(ns_card);
127
128    genesis.push(make_card("demo-wasm", Payload::Wasm {
129        module_sha256: demo_wasm_sha,
130        entry: "_start".into(),
131    }, Supervision::OneShot));
132
133    if let Some(card) = optional_native_card(
134        "demo-echo", "target/debug/ente-echo",
135        [ente_echo::echo_capability()].into_iter().collect(),
136        restart_supervision(),
137    ) {
138        genesis.push(card);
139    }
140
141    if let Some(card) = optional_native_card(
142        "compat-logind", "target/debug/ente-logind-compat",
143        [Capability::LegacyLogind].into_iter().collect(),
144        restart_supervision(),
145    ) {
146        genesis.push(card);
147    }
148
149    EntityCard {
150        schema_version: CARD_SCHEMA_VERSION,
151        id: Ulid::new(),
152        lineage: None,
153        label: "ente-zero-dev".into(),
154        provides,
155        requires: BTreeSet::new(),
156        soma: SomaSpec {
157            namespaces: NamespaceSet::default(),
158            rlimits: ResourceLimits::default(),
159            cgroup: CgroupSpec {
160                path: "ente.slice/zero".into(),
161                cpu_weight: None,
162                io_weight: None,
163            },
164            cpu_affinity: None,
165        },
166        payload: Payload::Virtual,
167        supervision: Supervision::OneShot,
168        genesis,
169    }
170}
171
172fn make_card(label: &str, payload: Payload, supervision: Supervision) -> EntityCard {
173    EntityCard {
174        schema_version: CARD_SCHEMA_VERSION,
175        id: Ulid::new(),
176        lineage: None,
177        label: label.into(),
178        provides: BTreeSet::new(),
179        requires: BTreeSet::new(),
180        soma: SomaSpec::default(),
181        payload,
182        supervision,
183        genesis: vec![],
184    }
185}
186
187fn optional_native_card(
188    label: &str,
189    bin_path: &str,
190    provides: BTreeSet<Capability>,
191    supervision: Supervision,
192) -> Option<EntityCard> {
193    let path = Path::new(bin_path);
194    if !path.exists() {
195        return None;
196    }
197    Some(EntityCard {
198        schema_version: CARD_SCHEMA_VERSION,
199        id: Ulid::new(),
200        lineage: None,
201        label: label.into(),
202        provides,
203        requires: BTreeSet::new(),
204        soma: SomaSpec::default(),
205        payload: Payload::Native {
206            exec: path.to_string_lossy().into_owned(),
207            argv: vec![],
208            envp: vec![],
209        },
210        supervision,
211        genesis: vec![],
212    })
213}
214
215fn restart_supervision() -> Supervision {
216    Supervision::Restart {
217        initial: Duration::from_millis(100),
218        max: Duration::from_secs(30),
219    }
220}