⎇ f4eb7dd94404c2fadd8099c61ca11d2860bcff7a
crates/ente-zero/src/main.rs
Historia
Quién tocó qué
crudo
Capability quotas, CAS gc, patrones burst/silence, snapshot incremental
1//! Ente #0 — el primer Ente. PID 1 del fractal.
2//!
3//! Reglas no negociables:
4//! 1. NUNCA lógica de servicio aquí. Sólo: leer Semilla, cosechar zombis,
5//! mediar capacidades, propagar eventos.
6//! 2. Single-threaded. Cualquier paralelismo se delega a Entes worker.
7//! Un panic en un thread de PID 1 = kernel panic.
8//! 3. Errores de hijos son *eventos* en `graph_tx`, no `Result` propagado.
9//!
10//! Este archivo es sólo wireup. La lógica vive en:
11//! - `seed` : construcción/restauración de la Tarjeta Semilla
12//! - `bus` : listener Unix + auth via SO_PEERCRED
13//! - `graph::*` : estado del fractal (lifecycle, topology, shutdown,
14//! bus_mediator, devices, capabilities)
15//! - `events` : tipos de eventos del bucle primordial
16//! - crates externos del workspace para CAS, soma, wasm, snapshot, kernel.
17
18mod brain_glue;
19mod bus;
20mod events;
21mod graph;
22mod seed;
23
24use anyhow::Context;
25use ente_brain::{BrainState, IntrospectServer};
26use ente_kernel::{become_child_subreaper, bootstrap_kernel_surface, spawn_sigchld_stream, spawn_uevent_stream};
27use events::{ExitStatus, GraphEvent, ShutdownReason};
28use graph::EnteGraph;
29use nix::errno::Errno;
30use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
31use nix::unistd::{getpid, Pid};
32use std::path::PathBuf;
33use std::time::Duration;
34use tokio::sync::mpsc;
35use tracing::{error, info, warn};
36
37struct CliArgs {
38 checkpoint: Option<PathBuf>,
39 restore: Option<PathBuf>,
40 rules: Option<PathBuf>,
41 rules_out: Option<PathBuf>,
42 audit_head: Option<PathBuf>,
43 metrics_addr: Option<String>,
44 brain_half_life: Option<f64>,
45 autopromote_secs: Option<u64>,
46}
47
48fn parse_args() -> CliArgs {
49 let mut args = std::env::args().skip(1);
50 let mut checkpoint = None;
51 let mut restore = None;
52 let mut rules = None;
53 let mut rules_out = None;
54 let mut audit_head = None;
55 let mut metrics_addr = None;
56 let mut brain_half_life = None;
57 let mut autopromote_secs = None;
58 while let Some(a) = args.next() {
59 match a.as_str() {
60 "--checkpoint" => checkpoint = args.next().map(PathBuf::from),
61 "--restore" => restore = args.next().map(PathBuf::from),
62 "--rules" => rules = args.next().map(PathBuf::from),
63 "--rules-out" => rules_out = args.next().map(PathBuf::from),
64 "--audit-head" => audit_head = args.next().map(PathBuf::from),
65 "--metrics-addr" => metrics_addr = args.next(),
66 "--brain-half-life" => brain_half_life = args.next().and_then(|s| s.parse().ok()),
67 "--autopromote-secs" => autopromote_secs = args.next().and_then(|s| s.parse().ok()),
68 other => warn!(arg = %other, "argumento desconocido, ignorado"),
69 }
70 }
71 CliArgs {
72 checkpoint, restore, rules, rules_out, audit_head,
73 metrics_addr, brain_half_life, autopromote_secs,
74 }
75}
76
77fn main() -> anyhow::Result<()> {
78 init_tracing();
79 let cli = parse_args();
80 let pid = getpid();
81 let dev_mode = pid != Pid::from_raw(1);
82
83 if dev_mode {
84 warn!(?pid, "ente-zero corriendo en DEV MODE (no PID 1) — kernel surface no se monta");
85 } else {
86 info!("ente-zero despierta como PID 1");
87 bootstrap_kernel_surface().context("bootstrap kernel surface")?;
88 become_child_subreaper().context("PR_SET_CHILD_SUBREAPER")?;
89 }
90
91 let card = seed::load(dev_mode, cli.restore.as_deref())?;
92
93 // current_thread runtime: ver doctrina al inicio del módulo.
94 let rt = tokio::runtime::Builder::new_current_thread()
95 .enable_io()
96 .enable_time()
97 .build()?;
98
99 rt.block_on(primordial_loop(
100 card, dev_mode,
101 cli.checkpoint, cli.restore, cli.rules, cli.rules_out,
102 cli.audit_head, cli.metrics_addr, cli.brain_half_life,
103 cli.autopromote_secs,
104 ))
105}
106
107async fn primordial_loop(
108 seed_card: ente_card::EntityCard,
109 dev_mode: bool,
110 checkpoint_path: Option<PathBuf>,
111 restore_path: Option<PathBuf>,
112 rules_path: Option<PathBuf>,
113 rules_out: Option<PathBuf>,
114 audit_head: Option<PathBuf>,
115 metrics_addr: Option<String>,
116 brain_half_life: Option<f64>,
117 autopromote_secs: Option<u64>,
118) -> anyhow::Result<()> {
119 info!(seed_id = %seed_card.id, label = %seed_card.label, "Ente #0 entra al bucle primordial");
120
121 let (graph_tx, mut graph_rx) = mpsc::channel::<GraphEvent>(64);
122 let mut sigchld = spawn_sigchld_stream()?;
123 // Uevents puede fallar en dev (sin CAP_NET_ADMIN). Degradamos a un
124 // canal nunca-listo en lugar de abortar el bucle primordial.
125 let mut uevents = match spawn_uevent_stream() {
126 Ok(rx) => rx,
127 Err(e) => {
128 warn!(?e, "uevents deshabilitados (probablemente falta CAP_NET_ADMIN)");
129 let (_keep_tx, rx) = mpsc::channel::<ente_kernel::UEvent>(1);
130 std::mem::forget(_keep_tx);
131 rx
132 }
133 };
134
135 // Bus interno: listener antes de spawn de hijos para que su Announce
136 // tenga adónde llegar. Su path se inyecta en ENTE_BUS_SOCK por soma.
137 let bus_sock = bus::default_socket_path();
138 let bus_path = bus::spawn_bus(bus_sock, graph_tx.clone())?;
139 ente_soma::set_bus_sock(bus_path.to_string_lossy().into_owned());
140
141 let mut graph = EnteGraph::new(seed_card);
142 graph.instantiate_seed_dependencies(&graph_tx).await?;
143
144 // Cerebro: BrainState compartido + servidor de introspección.
145 // Window de 1024 eventos — suficiente para correlaciones interesantes
146 // sin gastar memoria de PID 1. En dev bajamos el umbral de cristalización
147 // para que el demo (pocos eventos) produzca cristales observables.
148 let mut brain = if dev_mode {
149 // Umbrales relajados para que el demo (pocos eventos) produzca
150 // cristales observables. Con P(b|a) normalizada a [0,1], los
151 // valores típicos en muestras pequeñas son 0.2-0.5.
152 BrainState::with_params(1024, ente_brain::CrystallizationParams {
153 min_support: 2,
154 min_conditional_prob: 0.3,
155 min_pmi: 1.0,
156 })
157 } else {
158 BrainState::new(1024)
159 };
160 if let Some(out_path) = rules_out {
161 brain = brain.with_rules_out(out_path);
162 }
163 if let Some(hl) = brain_half_life {
164 let mut obs = brain.observer.write().await;
165 // Reemplazar con un observer nuevo que tenga half-life. Estado
166 // anterior (vacío en este punto) descartado.
167 *obs = ente_brain::Observer::new(1024).with_half_life(hl);
168 info!(hl_secs = hl, "observer con time-decay activo");
169 }
170 if let Some(secs) = autopromote_secs {
171 ente_brain::spawn_autopromote_loop(
172 brain.clone(),
173 ente_brain::AutopromoteParams {
174 interval_secs: secs,
175 threshold: brain.params, // mismo threshold que crystals manuales
176 },
177 );
178 }
179
180 // Brain restore: si hay --restore <path>, cargamos el snapshot adjunto
181 // <path>.brain.json. Counters preservados across reboots.
182 if let Some(rpath) = &restore_path {
183 let brain_path = rpath.with_extension("brain.json");
184 if brain_path.exists() {
185 match read_brain_snapshot(&brain_path) {
186 Ok(snap) => {
187 let total = snap.total;
188 let kinds = snap.marginal.len();
189 let restored = ente_brain::Observer::from_snapshot(snap);
190 *brain.observer.write().await = restored;
191 info!(
192 path = %brain_path.display(),
193 total, kinds,
194 "brain snapshot restaurado"
195 );
196 }
197 Err(e) => warn!(?e, path = %brain_path.display(), "brain snapshot read falló"),
198 }
199 }
200 }
201 // Si --audit-head, configuramos el head pointer y arrancamos auto-flush.
202 if let Some(head_path) = audit_head {
203 // Re-creamos el AuditLog con head pointer.
204 let new_audit = ente_brain::audit::AuditLog::new()
205 .with_head_pointer(head_path);
206 *brain.audit.write().await = new_audit;
207 spawn_audit_auto_flush(brain.clone());
208 }
209
210 // Carga inicial de reglas vía KCL o JSON, si --rules path proporcionado.
211 if let Some(path) = &rules_path {
212 match ente_brain::load_rules_file(path) {
213 Ok(rules) => {
214 let mut engine = brain.engine.write().await;
215 for r in rules {
216 engine.insert(r);
217 }
218 info!(count = engine.len(), path = %path.display(), "reglas cargadas");
219 }
220 Err(e) => warn!(?e, path = %path.display(), "carga de reglas falló"),
221 }
222 }
223
224 // Endpoint Prometheus opcional. En dev por defecto en 127.0.0.1:9911 si
225 // el flag no se pasó.
226 let metrics_addr = metrics_addr.or_else(|| {
227 if dev_mode { Some("127.0.0.1:9911".to_string()) } else { None }
228 });
229 if let Some(addr_s) = metrics_addr {
230 match addr_s.parse::<std::net::SocketAddr>() {
231 Ok(addr) => {
232 let s = brain.clone();
233 tokio::spawn(async move {
234 if let Err(e) = ente_brain::serve_metrics(s, addr).await {
235 warn!(?e, "metrics server cayó");
236 }
237 });
238 }
239 Err(e) => warn!(?e, addr = %addr_s, "metrics-addr inválido"),
240 }
241 }
242 spawn_brain_introspect(brain.clone());
243 let brain_sink = brain_glue::GraphSink {
244 graph_tx: graph_tx.clone(),
245 // Spawns auto-disparados desde reglas usan la identidad de la Semilla
246 // (único Ente con Capability::Spawn por construcción).
247 requester: graph.seed_id(),
248 };
249
250 // Demo automático del forwarding (sólo dev, sólo si el binario existe).
251 if dev_mode && std::path::Path::new("target/debug/ente-echo").exists() {
252 spawn_echo_smoke_test(bus_path.clone());
253 }
254
255 // En dev mode no tenemos hijos por defecto y el bucle se quedaría inerte.
256 let dev_exit = if dev_mode {
257 Some(tokio::time::sleep(Duration::from_secs(4)))
258 } else {
259 None
260 };
261 tokio::pin!(dev_exit);
262
263 // GC de capability grants expirados — corre cada 10 segundos.
264 let mut grant_purge = tokio::time::interval(Duration::from_secs(10));
265 grant_purge.tick().await; // descartar primer tick inmediato
266
267 loop {
268 tokio::select! {
269 biased;
270
271 Some(_) = sigchld.recv() => {
272 reap_until_empty(&mut graph, &graph_tx).await;
273 }
274
275 Some(uevt) = uevents.recv() => {
276 graph.on_uevent(uevt, &graph_tx).await;
277 }
278
279 Some(evt) = graph_rx.recv() => {
280 // Cerebro observa antes que el grafo mute. Snapshot del
281 // SubjectInfo se hace contra el estado pre-mutación.
282 feed_brain(&brain, &brain_sink, &graph, &evt).await;
283 if dispatch_graph_event(&mut graph, evt, &graph_tx, &checkpoint_path, &brain).await {
284 return Ok(());
285 }
286 }
287
288 _ = grant_purge.tick() => {
289 let n = graph.purge_expired_grants();
290 if n > 0 {
291 info!(purged = n, active = graph.active_grants_count(), "GC capability grants");
292 }
293 }
294
295 _ = async { dev_exit.as_mut().as_pin_mut().unwrap().await }, if dev_mode => {
296 info!("dev mode: timer expirado, cerrando bucle primordial");
297 let _ = graph_tx.send(GraphEvent::Shutdown {
298 reason: ShutdownReason::SeedRequested,
299 }).await;
300 }
301 }
302 }
303}
304
305/// Devuelve `true` si el bucle primordial debe terminar.
306async fn dispatch_graph_event(
307 graph: &mut EnteGraph,
308 evt: GraphEvent,
309 tx: &mpsc::Sender<GraphEvent>,
310 checkpoint: &Option<PathBuf>,
311 brain: &BrainState,
312) -> bool {
313 match evt {
314 GraphEvent::EnteDied { id, status } => {
315 graph.on_death(id, status, tx).await;
316 }
317 GraphEvent::CapabilityRequested { from, cap, reply } => {
318 graph.mediate_capability(from, cap, reply).await;
319 }
320 GraphEvent::SpawnRequest { card, requester } => {
321 if let Err(e) = graph.authorize_and_spawn(card, requester).await {
322 warn!(?e, "spawn request error");
323 }
324 }
325 GraphEvent::BusRequest { peer, from, request, outbound, reply } => {
326 graph.on_bus_request(peer, from, request, outbound, reply).await;
327 }
328 GraphEvent::BusResponse { seq, response } => {
329 graph.on_bus_response(seq, response).await;
330 }
331 GraphEvent::BusConnClosed { ente_id } => {
332 graph.on_bus_conn_closed(ente_id).await;
333 }
334 GraphEvent::Shutdown { reason } => {
335 warn!(?reason, "shutdown del fractal");
336 if let Some(path) = checkpoint.as_ref() {
337 // Snapshot del grafo
338 let snap = graph.snapshot();
339 match snap.write(path) {
340 Ok(()) => info!(path = %path.display(), entes = snap.entes.len(), "snapshot fractal persistido"),
341 Err(e) => warn!(?e, "snapshot write falló"),
342 }
343 // Snapshot del cerebro (observer state) en archivo adjunto
344 let brain_path = path.with_extension("brain.json");
345 let obs_snap = brain.observer.write().await.snapshot();
346 match write_brain_snapshot(&brain_path, &obs_snap) {
347 Ok(()) => info!(
348 path = %brain_path.display(),
349 total = obs_snap.total,
350 kinds = obs_snap.marginal.len(),
351 "snapshot brain persistido"
352 ),
353 Err(e) => warn!(?e, "brain snapshot write falló"),
354 }
355 }
356 graph.cascade_shutdown().await;
357 return true;
358 }
359 }
360 false
361}
362
363async fn reap_until_empty(graph: &mut EnteGraph, tx: &mpsc::Sender<GraphEvent>) {
364 loop {
365 match waitpid(None, Some(WaitPidFlag::WNOHANG)) {
366 Ok(WaitStatus::StillAlive) => return,
367 Ok(WaitStatus::Exited(pid, code)) => {
368 emit_death(graph, tx, pid, ExitStatus::Exit(code)).await;
369 }
370 Ok(WaitStatus::Signaled(pid, sig, _core)) => {
371 emit_death(graph, tx, pid, ExitStatus::Killed(sig)).await;
372 }
373 Ok(_) => continue, // Stopped/Continued — irrelevantes
374 Err(Errno::ECHILD) => return,
375 Err(e) => {
376 error!(?e, "waitpid fallo no recuperable en bucle de reaping");
377 return;
378 }
379 }
380 }
381}
382
383async fn emit_death(
384 graph: &EnteGraph,
385 tx: &mpsc::Sender<GraphEvent>,
386 pid: Pid,
387 status: ExitStatus,
388) {
389 let id = match graph.lookup_pid(pid) {
390 Some(id) => id,
391 None => {
392 // Proceso adoptado (subreaper): no está en nuestro grafo.
393 info!(?pid, ?status, "huérfano cosechado (no en grafo)");
394 return;
395 }
396 };
397 let _ = tx.send(GraphEvent::EnteDied { id, status }).await;
398}
399
400fn spawn_echo_smoke_test(bus_path: PathBuf) {
401 tokio::spawn(async move {
402 tokio::time::sleep(Duration::from_millis(300)).await;
403 match ente_bus::BusClient::connect(&bus_path).await {
404 Ok(mut client) => {
405 let req = ente_bus::BusRequest::Invoke {
406 cap: ente_echo::echo_capability(),
407 blob: b"hola fractal forwardeado".to_vec(),
408 };
409 match client.call(req).await {
410 Ok(ente_bus::BusResponse::Invoked { result }) => {
411 info!(echo = %String::from_utf8_lossy(&result), "Invoke ECHO round-trip OK");
412 }
413 Ok(other) => warn!(?other, "Invoke ECHO respuesta inesperada"),
414 Err(e) => warn!(?e, "Invoke ECHO falló"),
415 }
416 }
417 Err(e) => warn!(?e, "no se pudo conectar al bus para test"),
418 }
419 });
420}
421
422fn write_brain_snapshot(path: &std::path::Path, snap: &ente_brain::observer::ObserverSnapshot) -> anyhow::Result<()> {
423 let bytes = serde_json::to_vec_pretty(snap)?;
424 if let Some(parent) = path.parent() { let _ = std::fs::create_dir_all(parent); }
425 let tmp = path.with_extension("tmp");
426 std::fs::write(&tmp, &bytes)?;
427 std::fs::rename(&tmp, path)?;
428 Ok(())
429}
430
431fn read_brain_snapshot(path: &std::path::Path) -> anyhow::Result<ente_brain::observer::ObserverSnapshot> {
432 let bytes = std::fs::read(path)?;
433 let snap: ente_brain::observer::ObserverSnapshot = serde_json::from_slice(&bytes)?;
434 Ok(snap)
435}
436
437fn init_tracing() {
438 use tracing_subscriber::{fmt, EnvFilter};
439 let filter = EnvFilter::try_from_default_env()
440 .unwrap_or_else(|_| EnvFilter::new("ente_zero=debug,info"));
441 fmt().with_env_filter(filter).with_target(true).init();
442}
443
444fn brain_introspect_path() -> PathBuf {
445 if let Ok(p) = std::env::var("ENTE_BRAIN_SOCK") {
446 return p.into();
447 }
448 let runtime = std::env::var("XDG_RUNTIME_DIR")
449 .unwrap_or_else(|_| std::env::var("TMPDIR").unwrap_or_else(|_| "/tmp".into()));
450 format!("{runtime}/ente-brain.sock").into()
451}
452
453/// Auto-flush del audit log a CAS cada 10 segundos. Ejecuta best-effort:
454/// si el flush falla lo logeamos pero no abortamos. La integridad del log
455/// queda garantizada por su hash chain — re-flushar es idempotente.
456fn spawn_audit_auto_flush(state: BrainState) {
457 tokio::spawn(async move {
458 let mut tick = tokio::time::interval(std::time::Duration::from_secs(10));
459 tick.tick().await; // descartar primer tick inmediato
460 loop {
461 tick.tick().await;
462 let mut audit = state.audit.write().await;
463 match audit.flush_to_cas() {
464 Ok(0) => {} // nada nuevo
465 Ok(n) => info!(written = n, total = audit.flushed_count(), "audit auto-flush"),
466 Err(e) => warn!(?e, "audit auto-flush falló"),
467 }
468 }
469 });
470}
471
472fn spawn_brain_introspect(state: BrainState) {
473 let path = brain_introspect_path();
474 tokio::spawn(async move {
475 let server = IntrospectServer::new(state);
476 if let Err(e) = server.serve(&path).await {
477 warn!(?e, "introspect server cayó");
478 }
479 });
480}
481
482/// Registra el evento en el observer y dispatcha cualquier regla matched.
483/// Para reglas Sequence: pasamos los últimos N eventos del observer como
484/// history al engine.
485async fn feed_brain(
486 brain: &BrainState,
487 sink: &brain_glue::GraphSink,
488 graph: &EnteGraph,
489 evt: &GraphEvent,
490) {
491 let Some((kind, subj)) = brain_glue::graph_event_to_brain(evt, graph) else { return };
492 let history: Vec<ente_brain::TimedEvent> = {
493 let mut obs = brain.observer.write().await;
494 obs.record(kind.clone());
495 // Snapshot de los últimos 16 eventos — suficiente para cualquier
496 // Sequence pattern razonable. Clone hace una sola alocación.
497 obs.recent(16).cloned().collect()
498 };
499 let rules = {
500 let engine = brain.engine.read().await;
501 engine.dispatch(&kind, &subj, &history)
502 };
503 if !rules.is_empty() {
504 ente_brain::dispatch_actions(&rules, sink).await;
505 }
506}