refactor(naming): A1 — ente→arje, vista→revista, pluma→fana
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.json` 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 arje_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 = arje_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 ..Default::default()
63 })
64}
65
66fn load_or_synthesize(dev_mode: bool) -> anyhow::Result<EntityCard> {
67 // Buscamos primero `.json` (canónico), luego sin extensión por
68 // compatibilidad con instalaciones que dejan el archivo crudo. La puerta
69 // genética se cruza vía `arje_brain::load_card_file` que pasa por
70 // `validate()` extendido.
71 let candidates: &[&str] = if dev_mode {
72 &["seed.card.json", SEED_PATH_DEV]
73 } else {
74 &["/ente/seed.card.json", SEED_PATH_PROD]
75 };
76 for cand in candidates {
77 let path = PathBuf::from(cand);
78 if !path.exists() { continue; }
79 let card = arje_brain::load_card_file(&path)
80 .with_context(|| format!("load {}", path.display()))?;
81 info!(path = %path.display(), "Tarjeta Semilla cargada y validada");
82 return Ok(card);
83 }
84 if dev_mode {
85 info!("sin seed.card — sintetizando semilla mínima (dev)");
86 return Ok(synthesize_dev_seed());
87 }
88 anyhow::bail!("seed.card no encontrada en /ente/seed.card.json ni /ente/seed.card")
89}
90
91fn synthesize_dev_seed() -> EntityCard {
92 let mut provides = BTreeSet::new();
93 provides.insert(Capability::Spawn);
94 provides.insert(Capability::Journal);
95
96 // Pre-registramos el módulo Wasm demo en el CAS y obtenemos su SHA real.
97 // Si el CAS no es escribible (raro en dev) caemos a un SHA cero — la
98 // resolución fallará y el Wasm no encarnará, pero el resto queda intacto.
99 let demo_wasm_sha = match arje_wasm::demo_module_bytes()
100 .and_then(|b| arje_cas::store(&b))
101 {
102 Ok(sha) => sha,
103 Err(e) => {
104 warn!(?e, "CAS no disponible — demo-wasm no encarnará");
105 [0u8; 32]
106 }
107 };
108
109 let mut genesis = Vec::new();
110 genesis.push(make_card("demo-sleep", Payload::Native {
111 exec: "/bin/sleep".into(), argv: vec!["1".into()], envp: vec![],
112 }, Supervision::OneShot));
113
114 genesis.push(make_card("demo-persist", Payload::Native {
115 exec: "/bin/sleep".into(), argv: vec!["60".into()], envp: vec![],
116 }, restart_supervision()));
117
118 // Card namespaced: padre escribe uid_map, hijo cat /proc/self/uid_map.
119 let mut ns_card = make_card("demo-userns", Payload::Native {
120 exec: "/bin/cat".into(),
121 argv: vec!["/proc/self/uid_map".into()],
122 envp: vec![],
123 }, Supervision::OneShot);
124 ns_card.soma = SomaSpec {
125 namespaces: NamespaceSet { user: true, ..Default::default() },
126 ..Default::default()
127 };
128 genesis.push(ns_card);
129
130 genesis.push(make_card("demo-wasm", Payload::Wasm {
131 module_sha256: demo_wasm_sha,
132 entry: "_start".into(),
133 }, Supervision::OneShot));
134
135 if let Some(card) = optional_native_card(
136 "demo-echo", "target/debug/ente-echo",
137 [arje_echo::echo_capability()].into_iter().collect(),
138 restart_supervision(),
139 ) {
140 genesis.push(card);
141 }
142
143 if let Some(card) = optional_native_card(
144 "compat-logind", "target/debug/ente-logind-compat",
145 [Capability::LegacyLogind].into_iter().collect(),
146 restart_supervision(),
147 ) {
148 genesis.push(card);
149 }
150
151 // Constelación de shims D-Bus que reemplazan systemd: cada uno provee
152 // un nombre `org.freedesktop.X1` que GNOME/KDE consultan al boot.
153 for (label, bin) in &[
154 ("compat-hostnamed", "target/debug/ente-hostnamed-compat"),
155 ("compat-timedated", "target/debug/ente-timedated-compat"),
156 ("compat-localed", "target/debug/ente-localed-compat"),
157 ("compat-journald", "target/debug/ente-journald-compat"),
158 ("compat-resolved", "target/debug/ente-resolved-compat"),
159 ("compat-polkit", "target/debug/ente-polkit-compat"),
160 ("compat-machined", "target/debug/ente-machined-compat"),
161 ("policy-provider", "target/debug/ente-policy-provider"),
162 ("compat-systemd1", "target/debug/ente-systemd1-compat"),
163 ("compat-notify", "target/debug/ente-notify-compat"),
164 ("compat-timer", "target/debug/ente-timer-compat"),
165 ] {
166 if let Some(card) = optional_native_card(
167 label, bin,
168 std::collections::BTreeSet::new(),
169 restart_supervision(),
170 ) {
171 genesis.push(card);
172 }
173 }
174
175 EntityCard {
176 schema_version: CARD_SCHEMA_VERSION,
177 id: Ulid::new(),
178 lineage: None,
179 label: "ente-zero-dev".into(),
180 provides,
181 requires: BTreeSet::new(),
182 soma: SomaSpec {
183 namespaces: NamespaceSet::default(),
184 rlimits: ResourceLimits::default(),
185 cgroup: CgroupSpec {
186 path: "ente.slice/zero".into(),
187 cpu_weight: None,
188 io_weight: None,
189 },
190 cpu_affinity: None,
191 },
192 payload: Payload::Virtual,
193 supervision: Supervision::OneShot,
194 genesis,
195 ..Default::default()
196 }
197}
198
199fn make_card(label: &str, payload: Payload, supervision: Supervision) -> EntityCard {
200 EntityCard {
201 schema_version: CARD_SCHEMA_VERSION,
202 id: Ulid::new(),
203 lineage: None,
204 label: label.into(),
205 provides: BTreeSet::new(),
206 requires: BTreeSet::new(),
207 soma: SomaSpec::default(),
208 payload,
209 supervision,
210 genesis: vec![],
211 ..Default::default()
212 }
213}
214
215fn optional_native_card(
216 label: &str,
217 bin_path: &str,
218 provides: BTreeSet<Capability>,
219 supervision: Supervision,
220) -> Option<EntityCard> {
221 let path = Path::new(bin_path);
222 if !path.exists() {
223 return None;
224 }
225 Some(EntityCard {
226 schema_version: CARD_SCHEMA_VERSION,
227 id: Ulid::new(),
228 lineage: None,
229 label: label.into(),
230 provides,
231 requires: BTreeSet::new(),
232 soma: SomaSpec::default(),
233 payload: Payload::Native {
234 exec: path.to_string_lossy().into_owned(),
235 argv: vec![],
236 envp: vec![],
237 },
238 supervision,
239 genesis: vec![],
240 ..Default::default()
241 })
242}
243
244fn restart_supervision() -> Supervision {
245 Supervision::Restart {
246 initial: Duration::from_millis(100),
247 max: Duration::from_secs(30),
248 }
249}