⎇ 6ad6d08fa80fc6796e79f3f522830914077728f7
crates/ente-zero/src/seed.rs
History
Who touched what
raw
4 D-Bus shims sustituyendo systemd: hostnamed, timedated, localed, journald
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 // Constelación de shims D-Bus que reemplazan systemd: cada uno provee
150 // un nombre `org.freedesktop.X1` que GNOME/KDE consultan al boot.
151 for (label, bin) in &[
152 ("compat-hostnamed", "target/debug/ente-hostnamed-compat"),
153 ("compat-timedated", "target/debug/ente-timedated-compat"),
154 ("compat-localed", "target/debug/ente-localed-compat"),
155 ("compat-journald", "target/debug/ente-journald-compat"),
156 ] {
157 if let Some(card) = optional_native_card(
158 label, bin,
159 std::collections::BTreeSet::new(),
160 restart_supervision(),
161 ) {
162 genesis.push(card);
163 }
164 }
165
166 EntityCard {
167 schema_version: CARD_SCHEMA_VERSION,
168 id: Ulid::new(),
169 lineage: None,
170 label: "ente-zero-dev".into(),
171 provides,
172 requires: BTreeSet::new(),
173 soma: SomaSpec {
174 namespaces: NamespaceSet::default(),
175 rlimits: ResourceLimits::default(),
176 cgroup: CgroupSpec {
177 path: "ente.slice/zero".into(),
178 cpu_weight: None,
179 io_weight: None,
180 },
181 cpu_affinity: None,
182 },
183 payload: Payload::Virtual,
184 supervision: Supervision::OneShot,
185 genesis,
186 }
187}
188
189fn make_card(label: &str, payload: Payload, supervision: Supervision) -> EntityCard {
190 EntityCard {
191 schema_version: CARD_SCHEMA_VERSION,
192 id: Ulid::new(),
193 lineage: None,
194 label: label.into(),
195 provides: BTreeSet::new(),
196 requires: BTreeSet::new(),
197 soma: SomaSpec::default(),
198 payload,
199 supervision,
200 genesis: vec![],
201 }
202}
203
204fn optional_native_card(
205 label: &str,
206 bin_path: &str,
207 provides: BTreeSet<Capability>,
208 supervision: Supervision,
209) -> Option<EntityCard> {
210 let path = Path::new(bin_path);
211 if !path.exists() {
212 return None;
213 }
214 Some(EntityCard {
215 schema_version: CARD_SCHEMA_VERSION,
216 id: Ulid::new(),
217 lineage: None,
218 label: label.into(),
219 provides,
220 requires: BTreeSet::new(),
221 soma: SomaSpec::default(),
222 payload: Payload::Native {
223 exec: path.to_string_lossy().into_owned(),
224 argv: vec![],
225 envp: vec![],
226 },
227 supervision,
228 genesis: vec![],
229 })
230}
231
232fn restart_supervision() -> Supervision {
233 Supervision::Restart {
234 initial: Duration::from_millis(100),
235 max: Duration::from_secs(30),
236 }
237}