⎇ d1b700eb2bda9fa5f6f913d36f6ce1fb1e6d290b
crates/init/arje-zero/src/main.rs
History
Who touched what
raw
fix(init): el reboot-loop de Fedora — remount rw + /run tmpfs + shell de rescate
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 keypair_store;
23mod seed;
24
25use anyhow::Context;
26use arje_brain::{BrainState, IntrospectServer};
27use arje_kernel::{become_child_subreaper, bootstrap_kernel_surface, spawn_sigchld_stream, spawn_uevent_stream};
28use events::{ExitStatus, GraphEvent, ShutdownReason};
29use graph::EnteGraph;
30use nix::errno::Errno;
31use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
32use nix::unistd::{getpid, Pid};
33use std::path::PathBuf;
34use std::time::Duration;
35use tokio::sync::mpsc;
36use tracing::{error, info, warn};
37
38struct CliArgs {
39 checkpoint: Option<PathBuf>,
40 restore: Option<PathBuf>,
41 rules: Option<PathBuf>,
42 rules_out: Option<PathBuf>,
43 audit_head: Option<PathBuf>,
44 metrics_addr: Option<String>,
45 brain_half_life: Option<f64>,
46 autopromote_secs: Option<u64>,
47}
48
49fn parse_args() -> CliArgs {
50 let mut args = std::env::args().skip(1);
51 let mut checkpoint = None;
52 let mut restore = None;
53 let mut rules = None;
54 let mut rules_out = None;
55 let mut audit_head = None;
56 let mut metrics_addr = None;
57 let mut brain_half_life = None;
58 let mut autopromote_secs = None;
59 while let Some(a) = args.next() {
60 match a.as_str() {
61 "--checkpoint" => checkpoint = args.next().map(PathBuf::from),
62 "--restore" => restore = args.next().map(PathBuf::from),
63 "--rules" => rules = args.next().map(PathBuf::from),
64 "--rules-out" => rules_out = args.next().map(PathBuf::from),
65 "--audit-head" => audit_head = args.next().map(PathBuf::from),
66 "--metrics-addr" => metrics_addr = args.next(),
67 "--brain-half-life" => brain_half_life = args.next().and_then(|s| s.parse().ok()),
68 "--autopromote-secs" => autopromote_secs = args.next().and_then(|s| s.parse().ok()),
69 other => warn!(arg = %other, "argumento desconocido, ignorado"),
70 }
71 }
72 CliArgs {
73 checkpoint, restore, rules, rules_out, audit_head,
74 metrics_addr, brain_half_life, autopromote_secs,
75 }
76}
77
78fn main() -> anyhow::Result<()> {
79 init_tracing();
80 let cli = parse_args();
81 let pid = getpid();
82 let dev_mode = pid != Pid::from_raw(1);
83
84 if dev_mode {
85 warn!(?pid, "ente-zero corriendo en DEV MODE (no PID 1) — kernel surface no se monta");
86 return run(cli, true);
87 }
88
89 info!("ente-zero despierta como PID 1");
90 // Doctrina dura: PID 1 NUNCA puede salir — el kernel haría panic
91 // ("Attempted to kill init") y, con `panic=N` en el cmdline, la
92 // máquina cae en un reboot-loop. Por eso cualquier fallo de arranque
93 // se desvía a una shell de rescate: deja diagnosticar y reparar en
94 // vez de reiniciar a ciegas cada diez segundos.
95 match run(cli, false) {
96 Ok(()) => emergency_shell(
97 "el bucle primordial terminó — el fractal pidió shutdown",
98 ),
99 Err(e) => emergency_shell(&format!("{e:#}")),
100 }
101}
102
103/// Arranque + bucle primordial. En PID 1, cualquier `Err` que devuelva
104/// lo intercepta `main` y lo convierte en shell de rescate.
105fn run(cli: CliArgs, dev_mode: bool) -> anyhow::Result<()> {
106 if !dev_mode {
107 bootstrap_kernel_surface().context("bootstrap kernel surface")?;
108 become_child_subreaper().context("PR_SET_CHILD_SUBREAPER")?;
109 }
110
111 let card = seed::load(dev_mode, cli.restore.as_deref())?;
112
113 // current_thread runtime: ver doctrina al inicio del módulo.
114 let rt = tokio::runtime::Builder::new_current_thread()
115 .enable_io()
116 .enable_time()
117 .build()?;
118
119 rt.block_on(primordial_loop(
120 card, dev_mode,
121 cli.checkpoint, cli.restore, cli.rules, cli.rules_out,
122 cli.audit_head, cli.metrics_addr, cli.brain_half_life,
123 cli.autopromote_secs,
124 ))
125}
126
127/// Último recurso de PID 1: imprime el diagnóstico en la consola y abre
128/// una shell de rescate. **Nunca retorna** — si lo hiciera, el proceso
129/// saldría y el kernel haría panic.
130fn emergency_shell(reason: &str) -> ! {
131 let banner = format!(
132 "\n\n\
133 =============== arje-zero — ARRANQUE FALLIDO ================\n\
134 {reason}\n\
135 ---------------------------------------------------------------\n\
136 Se abre una shell de rescate sobre la consola. Revisá el sistema\n\
137 (p. ej. /ente/seed.card.json y los binarios en /usr/sbin) y\n\
138 reiniciá con `reboot -f`. Salir de la shell la vuelve a abrir.\n\n",
139 );
140 write_to_console(&banner);
141 error!(reason = %reason.replace('\n', " "), "arranque de PID 1 fallido");
142 loop {
143 match spawn_console_shell() {
144 Ok(status) => write_to_console(&format!(
145 "\n[arje-zero] la shell de rescate terminó ({status}) — reabriendo.\n",
146 )),
147 Err(e) => {
148 write_to_console(&format!(
149 "\n[arje-zero] no hay shell de rescate disponible: {e}\n\
150 PID 1 queda en espera pasiva — usá la consola del proveedor.\n",
151 ));
152 loop {
153 std::thread::sleep(Duration::from_secs(3600));
154 }
155 }
156 }
157 }
158}
159
160/// Abre una shell interactiva con stdin/stdout/stderr sobre
161/// `/dev/console` y espera a que termine.
162fn spawn_console_shell() -> std::io::Result<std::process::ExitStatus> {
163 let shell = ["/bin/sh", "/bin/bash", "/usr/bin/sh", "/usr/bin/bash"]
164 .into_iter()
165 .find(|p| std::path::Path::new(p).exists())
166 .ok_or_else(|| {
167 std::io::Error::new(
168 std::io::ErrorKind::NotFound,
169 "ninguna shell en /bin ni /usr/bin",
170 )
171 })?;
172 let console = std::fs::OpenOptions::new()
173 .read(true)
174 .write(true)
175 .open("/dev/console")?;
176 std::process::Command::new(shell)
177 .stdin(console.try_clone()?)
178 .stdout(console.try_clone()?)
179 .stderr(console)
180 .env("HOME", "/root")
181 .env("PATH", "/usr/sbin:/usr/bin:/sbin:/bin")
182 .env("PS1", "arje-rescate# ")
183 .env("TERM", "linux")
184 .status()
185}
186
187/// Escribe un mensaje directo a `/dev/console`, con stderr de respaldo.
188fn write_to_console(msg: &str) {
189 use std::io::Write;
190 if let Ok(mut f) = std::fs::OpenOptions::new().write(true).open("/dev/console") {
191 let _ = f.write_all(msg.as_bytes());
192 }
193 eprint!("{msg}");
194}
195
196async fn primordial_loop(
197 seed_card: arje_card::EntityCard,
198 dev_mode: bool,
199 checkpoint_path: Option<PathBuf>,
200 restore_path: Option<PathBuf>,
201 rules_path: Option<PathBuf>,
202 rules_out: Option<PathBuf>,
203 audit_head: Option<PathBuf>,
204 metrics_addr: Option<String>,
205 brain_half_life: Option<f64>,
206 autopromote_secs: Option<u64>,
207) -> anyhow::Result<()> {
208 info!(seed_id = %seed_card.id, label = %seed_card.label, "Ente #0 entra al bucle primordial");
209
210 let (graph_tx, mut graph_rx) = mpsc::channel::<GraphEvent>(64);
211 let mut sigchld = spawn_sigchld_stream()?;
212 // Uevents puede fallar en dev (sin CAP_NET_ADMIN). Degradamos a un
213 // canal nunca-listo en lugar de abortar el bucle primordial.
214 let mut uevents = match spawn_uevent_stream() {
215 Ok(rx) => rx,
216 Err(e) => {
217 warn!(?e, "uevents deshabilitados (probablemente falta CAP_NET_ADMIN)");
218 let (_keep_tx, rx) = mpsc::channel::<arje_kernel::UEvent>(1);
219 std::mem::forget(_keep_tx);
220 rx
221 }
222 };
223
224 // Bus interno: listener antes de spawn de hijos para que su Announce
225 // tenga adónde llegar. Su path se inyecta en ENTE_BUS_SOCK por soma.
226 let bus_sock = bus::default_socket_path();
227 let bus_path = bus::spawn_bus(bus_sock, graph_tx.clone())?;
228 arje_soma::set_bus_sock(bus_path.to_string_lossy().into_owned());
229
230 // Brahman protocol: handshake socket + broker compartido.
231 //
232 // Es un canal paralelo al ente-bus, dedicado a módulos "brahman
233 // conscientes" que se presentan con una Card y declaran flujos
234 // tipados. Si el bind falla (socket en uso, FS no escribible),
235 // degradamos a "modo bus-only" — la doctrina de PID 1 no rompe
236 // por subsistemas opcionales.
237 // Contexto operativo del broker: configurable por env var. Útil para
238 // distinguir test/prod/foreground sin recompilar. Sin la var, los
239 // biases per-contexto declarados en las Cards quedan inactivos.
240 let broker_context = std::env::var("BRAHMAN_BROKER_CONTEXT").ok();
241 if let Some(ctx) = &broker_context {
242 info!(context = %ctx, "brahman broker bajo contexto operativo");
243 }
244 let brahman_broker = std::sync::Arc::new(tokio::sync::Mutex::new(
245 brahman_broker::Broker::new(brahman_broker::BrokerConfig {
246 strategy: brahman_broker::MatchStrategy::default(),
247 current_context: broker_context.clone(),
248 }),
249 ));
250
251 // Brahman-net opcional: si BRAHMAN_LISTEN_MULTIADDR está set,
252 // levantamos la malla P2P y la pasamos como ServerConfig.net (Fase
253 // 2 wire) para que cada Card con outputs se anuncie al DHT y
254 // pueda ser descubierta por nodos remotos. Identidad libp2p
255 // persistida en disco vía keypair_store (peer_id estable across
256 // reboots).
257 let brahman_net = setup_brahman_net(dev_mode).await;
258
259 // Política opcional de peers libp2p: allowlist + denylist + hot
260 // reload. Activada si BRAHMAN_PEER_ALLOWLIST o BRAHMAN_PEER_DENYLIST
261 // están set. Sin ninguna, modo totalmente abierto (Fase 3 sin
262 // restricción adicional). El watcher se queda vivo en background
263 // observando los archivos para hot reload.
264 let (brahman_policy, _policy_watcher) = setup_brahman_policy();
265
266 // Si tenemos AMBOS net y policy, attachamos: el deny de la
267 // policy se proyecta al block_list del swarm para rechazar
268 // conexiones ANTES del Noise handshake (más eficiente que
269 // rechazar en el handshake brahman). Cada hot-reload de la
270 // policy también re-sincroniza vía diff.
271 if let (Some(net), Some(policy)) = (&brahman_net, &brahman_policy) {
272 policy.attach_to_net(net.clone());
273 let (allow, deny) = policy.sizes();
274 info!(
275 allow = ?allow,
276 deny = deny,
277 "policy attached al swarm — denies enforcedeados a nivel libp2p"
278 );
279 }
280
281 let brahman_sock = brahman_handshake::transport::default_socket_path();
282 match brahman_handshake::server::Server::bind(
283 &brahman_sock,
284 brahman_handshake::server::ServerConfig {
285 init_attached: true,
286 broker: Some(brahman_broker.clone()),
287 net: brahman_net.clone(),
288 policy: brahman_policy.clone(),
289 },
290 ) {
291 Ok(server) => {
292 info!(socket = %brahman_sock.display(), "brahman handshake escuchando (Unix)");
293 // Si hay malla P2P, además del Unix accept loop levantamos
294 // el accept loop libp2p sobre el mismo Server compartido.
295 // Las sesiones locales y remotas conviven en las mismas
296 // tablas (sessions, push_table, broker).
297 let server = std::sync::Arc::new(server);
298 if let Some(net) = brahman_net.clone() {
299 let s_libp2p = server.clone();
300 let n_libp2p = net.clone();
301 tokio::spawn(async move {
302 if let Err(e) = brahman_handshake::network::run_libp2p_accept_loop(
303 s_libp2p, n_libp2p,
304 )
305 .await
306 {
307 warn!(?e, "brahman handshake libp2p accept loop cayó");
308 }
309 });
310 info!(
311 "brahman handshake escuchando también vía libp2p (peer_id {})",
312 net.peer_id
313 );
314 }
315 // Unix accept loop: usa Arc<Server> en lugar del consume
316 // de run() para coexistir con el libp2p accept loop.
317 let s_unix = server.clone();
318 tokio::spawn(async move {
319 loop {
320 match s_unix.accept_one().await {
321 Ok(session) => {
322 tokio::spawn(async move {
323 if let Err(e) = session.handle().await {
324 warn!(?e, "session Unix terminó con error");
325 }
326 });
327 }
328 Err(e) => {
329 warn!(?e, "brahman handshake accept_one Unix falló");
330 break;
331 }
332 }
333 }
334 });
335 }
336 Err(e) => {
337 warn!(?e, socket = %brahman_sock.display(), "brahman handshake deshabilitado");
338 }
339 }
340
341 // Brahman admin: socket separado para snapshots de estado (sesiones +
342 // matches del broker). Misma política de degradación grácil.
343 let admin_sock = brahman_admin::transport::default_socket_path();
344 match brahman_admin::server::AdminServer::bind(
345 &admin_sock,
346 brahman_broker.clone(),
347 brahman_admin::server::AdminConfig {
348 init_attached: true,
349 current_context: broker_context.clone(),
350 },
351 ) {
352 Ok(admin) => {
353 info!(socket = %admin_sock.display(), "brahman admin escuchando");
354 tokio::spawn(async move {
355 if let Err(e) = admin.run().await {
356 warn!(?e, "brahman admin server cayó");
357 }
358 });
359 }
360 Err(e) => {
361 warn!(?e, socket = %admin_sock.display(), "brahman admin deshabilitado");
362 }
363 }
364
365 let mut graph = EnteGraph::new(seed_card);
366 graph.instantiate_seed_dependencies(&graph_tx).await?;
367
368 // Cerebro: BrainState compartido + servidor de introspección.
369 // Window de 1024 eventos — suficiente para correlaciones interesantes
370 // sin gastar memoria de PID 1. En dev bajamos el umbral de cristalización
371 // para que el demo (pocos eventos) produzca cristales observables.
372 let mut brain = if dev_mode {
373 // Umbrales relajados para que el demo (pocos eventos) produzca
374 // cristales observables. Con P(b|a) normalizada a [0,1], los
375 // valores típicos en muestras pequeñas son 0.2-0.5.
376 BrainState::with_params(1024, arje_brain::CrystallizationParams {
377 min_support: 2,
378 min_conditional_prob: 0.3,
379 min_pmi: 1.0,
380 })
381 } else {
382 BrainState::new(1024)
383 };
384 if let Some(out_path) = rules_out {
385 brain = brain.with_rules_out(out_path);
386 }
387 if let Some(hl) = brain_half_life {
388 let mut obs = brain.observer.write().await;
389 // Reemplazar con un observer nuevo que tenga half-life. Estado
390 // anterior (vacío en este punto) descartado.
391 *obs = arje_brain::Observer::new(1024).with_half_life(hl);
392 info!(hl_secs = hl, "observer con time-decay activo");
393 }
394 if let Some(secs) = autopromote_secs {
395 arje_brain::spawn_autopromote_loop(
396 brain.clone(),
397 arje_brain::AutopromoteParams {
398 interval_secs: secs,
399 threshold: brain.params, // mismo threshold que crystals manuales
400 },
401 );
402 }
403
404 // Brain restore: si hay --restore <path>, cargamos el snapshot adjunto
405 // <path>.brain.json. Counters preservados across reboots.
406 if let Some(rpath) = &restore_path {
407 let brain_path = rpath.with_extension("brain.json");
408 if brain_path.exists() {
409 match read_brain_snapshot(&brain_path) {
410 Ok(snap) => {
411 let total = snap.total;
412 let kinds = snap.marginal.len();
413 let restored = arje_brain::Observer::from_snapshot(snap);
414 *brain.observer.write().await = restored;
415 info!(
416 path = %brain_path.display(),
417 total, kinds,
418 "brain snapshot restaurado"
419 );
420 }
421 Err(e) => warn!(?e, path = %brain_path.display(), "brain snapshot read falló"),
422 }
423 }
424 }
425 // Si --audit-head, configuramos el head pointer y arrancamos auto-flush.
426 if let Some(head_path) = audit_head {
427 // Re-creamos el AuditLog con head pointer.
428 let new_audit = arje_brain::audit::AuditLog::new()
429 .with_head_pointer(head_path);
430 *brain.audit.write().await = new_audit;
431 spawn_audit_auto_flush(brain.clone());
432 }
433
434 // Carga inicial de reglas desde JSON/JSONL si --rules path proporcionado.
435 if let Some(path) = &rules_path {
436 match arje_brain::load_rules_file(path) {
437 Ok(rules) => {
438 let mut engine = brain.engine.write().await;
439 for r in rules {
440 engine.insert(r);
441 }
442 info!(count = engine.len(), path = %path.display(), "reglas cargadas");
443 }
444 Err(e) => warn!(?e, path = %path.display(), "carga de reglas falló"),
445 }
446 }
447
448 // Endpoint Prometheus opcional. En dev por defecto en 127.0.0.1:9911 si
449 // el flag no se pasó.
450 let metrics_addr = metrics_addr.or_else(|| {
451 if dev_mode { Some("127.0.0.1:9911".to_string()) } else { None }
452 });
453 if let Some(addr_s) = metrics_addr {
454 match addr_s.parse::<std::net::SocketAddr>() {
455 Ok(addr) => {
456 let s = brain.clone();
457 tokio::spawn(async move {
458 if let Err(e) = arje_brain::serve_metrics(s, addr).await {
459 warn!(?e, "metrics server cayó");
460 }
461 });
462 }
463 Err(e) => warn!(?e, addr = %addr_s, "metrics-addr inválido"),
464 }
465 }
466 spawn_brain_introspect(brain.clone());
467 let brain_sink = brain_glue::GraphSink {
468 graph_tx: graph_tx.clone(),
469 // Spawns auto-disparados desde reglas usan la identidad de la Semilla
470 // (único Ente con Capability::Spawn por construcción).
471 requester: graph.seed_id(),
472 };
473
474 // Demo automático del forwarding (sólo dev, sólo si el binario existe).
475 if dev_mode && std::path::Path::new("target/debug/ente-echo").exists() {
476 spawn_echo_smoke_test(bus_path.clone());
477 }
478
479 // En dev mode no tenemos hijos por defecto y el bucle se quedaría inerte.
480 let dev_exit = if dev_mode {
481 Some(tokio::time::sleep(Duration::from_secs(4)))
482 } else {
483 None
484 };
485 tokio::pin!(dev_exit);
486
487 // GC de capability grants expirados — corre cada 10 segundos.
488 let mut grant_purge = tokio::time::interval(Duration::from_secs(10));
489 grant_purge.tick().await; // descartar primer tick inmediato
490
491 loop {
492 tokio::select! {
493 biased;
494
495 Some(_) = sigchld.recv() => {
496 reap_until_empty(&mut graph, &graph_tx).await;
497 }
498
499 Some(uevt) = uevents.recv() => {
500 graph.on_uevent(uevt, &graph_tx).await;
501 }
502
503 Some(evt) = graph_rx.recv() => {
504 // Cerebro observa antes que el grafo mute. Snapshot del
505 // SubjectInfo se hace contra el estado pre-mutación.
506 feed_brain(&brain, &brain_sink, &graph, &evt).await;
507 if dispatch_graph_event(&mut graph, evt, &graph_tx, &checkpoint_path, &brain).await {
508 return Ok(());
509 }
510 }
511
512 _ = grant_purge.tick() => {
513 let n = graph.purge_expired_grants();
514 if n > 0 {
515 info!(purged = n, active = graph.active_grants_count(), "GC capability grants");
516 }
517 }
518
519 _ = async { dev_exit.as_mut().as_pin_mut().unwrap().await }, if dev_mode => {
520 info!("dev mode: timer expirado, cerrando bucle primordial");
521 let _ = graph_tx.send(GraphEvent::Shutdown {
522 reason: ShutdownReason::SeedRequested,
523 }).await;
524 }
525 }
526 }
527}
528
529/// Devuelve `true` si el bucle primordial debe terminar.
530async fn dispatch_graph_event(
531 graph: &mut EnteGraph,
532 evt: GraphEvent,
533 tx: &mpsc::Sender<GraphEvent>,
534 checkpoint: &Option<PathBuf>,
535 brain: &BrainState,
536) -> bool {
537 match evt {
538 GraphEvent::EnteDied { id, status } => {
539 graph.on_death(id, status, tx).await;
540 }
541 GraphEvent::CapabilityRequested { from, cap, reply } => {
542 graph.mediate_capability(from, cap, reply).await;
543 }
544 GraphEvent::SpawnRequest { card, requester } => {
545 if let Err(e) = graph.authorize_and_spawn(card, requester).await {
546 warn!(?e, "spawn request error");
547 }
548 }
549 GraphEvent::BusRequest { peer, from, request, outbound, reply } => {
550 graph.on_bus_request(peer, from, request, outbound, reply).await;
551 }
552 GraphEvent::BusResponse { seq, response } => {
553 graph.on_bus_response(seq, response).await;
554 }
555 GraphEvent::BusConnClosed { ente_id } => {
556 graph.on_bus_conn_closed(ente_id).await;
557 }
558 GraphEvent::Shutdown { reason } => {
559 warn!(?reason, "shutdown del fractal");
560 if let Some(path) = checkpoint.as_ref() {
561 // Snapshot del grafo
562 let snap = graph.snapshot();
563 match snap.write(path) {
564 Ok(()) => info!(path = %path.display(), entes = snap.entes.len(), "snapshot fractal persistido"),
565 Err(e) => warn!(?e, "snapshot write falló"),
566 }
567 // Snapshot del cerebro (observer state) en archivo adjunto
568 let brain_path = path.with_extension("brain.json");
569 let obs_snap = brain.observer.write().await.snapshot();
570 match write_brain_snapshot(&brain_path, &obs_snap) {
571 Ok(()) => info!(
572 path = %brain_path.display(),
573 total = obs_snap.total,
574 kinds = obs_snap.marginal.len(),
575 "snapshot brain persistido"
576 ),
577 Err(e) => warn!(?e, "brain snapshot write falló"),
578 }
579 }
580 graph.cascade_shutdown().await;
581 return true;
582 }
583 }
584 false
585}
586
587async fn reap_until_empty(graph: &mut EnteGraph, tx: &mpsc::Sender<GraphEvent>) {
588 loop {
589 match waitpid(None, Some(WaitPidFlag::WNOHANG)) {
590 Ok(WaitStatus::StillAlive) => return,
591 Ok(WaitStatus::Exited(pid, code)) => {
592 emit_death(graph, tx, pid, ExitStatus::Exit(code)).await;
593 }
594 Ok(WaitStatus::Signaled(pid, sig, _core)) => {
595 emit_death(graph, tx, pid, ExitStatus::Killed(sig)).await;
596 }
597 Ok(_) => continue, // Stopped/Continued — irrelevantes
598 Err(Errno::ECHILD) => return,
599 Err(e) => {
600 error!(?e, "waitpid fallo no recuperable en bucle de reaping");
601 return;
602 }
603 }
604 }
605}
606
607async fn emit_death(
608 graph: &EnteGraph,
609 tx: &mpsc::Sender<GraphEvent>,
610 pid: Pid,
611 status: ExitStatus,
612) {
613 let id = match graph.lookup_pid(pid) {
614 Some(id) => id,
615 None => {
616 // Proceso adoptado (subreaper): no está en nuestro grafo.
617 info!(?pid, ?status, "huérfano cosechado (no en grafo)");
618 return;
619 }
620 };
621 let _ = tx.send(GraphEvent::EnteDied { id, status }).await;
622}
623
624fn spawn_echo_smoke_test(bus_path: PathBuf) {
625 tokio::spawn(async move {
626 tokio::time::sleep(Duration::from_millis(300)).await;
627 match arje_bus::BusClient::connect(&bus_path).await {
628 Ok(mut client) => {
629 let req = arje_bus::BusRequest::Invoke {
630 cap: arje_echo::echo_capability(),
631 blob: b"hola fractal forwardeado".to_vec(),
632 };
633 match client.call(req).await {
634 Ok(arje_bus::BusResponse::Invoked { result }) => {
635 info!(echo = %String::from_utf8_lossy(&result), "Invoke ECHO round-trip OK");
636 }
637 Ok(other) => warn!(?other, "Invoke ECHO respuesta inesperada"),
638 Err(e) => warn!(?e, "Invoke ECHO falló"),
639 }
640 }
641 Err(e) => warn!(?e, "no se pudo conectar al bus para test"),
642 }
643 });
644}
645
646fn write_brain_snapshot(path: &std::path::Path, snap: &arje_brain::observer::ObserverSnapshot) -> anyhow::Result<()> {
647 let bytes = serde_json::to_vec_pretty(snap)?;
648 if let Some(parent) = path.parent() { let _ = std::fs::create_dir_all(parent); }
649 let tmp = path.with_extension("tmp");
650 std::fs::write(&tmp, &bytes)?;
651 std::fs::rename(&tmp, path)?;
652 Ok(())
653}
654
655fn read_brain_snapshot(path: &std::path::Path) -> anyhow::Result<arje_brain::observer::ObserverSnapshot> {
656 let bytes = std::fs::read(path)?;
657 let snap: arje_brain::observer::ObserverSnapshot = serde_json::from_slice(&bytes)?;
658 Ok(snap)
659}
660
661fn init_tracing() {
662 use tracing_subscriber::{fmt, EnvFilter};
663 let filter = EnvFilter::try_from_default_env()
664 .unwrap_or_else(|_| EnvFilter::new("arje_zero=debug,info"));
665 fmt().with_env_filter(filter).with_target(true).init();
666}
667
668fn brain_introspect_path() -> PathBuf {
669 if let Ok(p) = std::env::var("ENTE_BRAIN_SOCK") {
670 return p.into();
671 }
672 let runtime = std::env::var("XDG_RUNTIME_DIR")
673 .unwrap_or_else(|_| std::env::var("TMPDIR").unwrap_or_else(|_| "/tmp".into()));
674 format!("{runtime}/ente-brain.sock").into()
675}
676
677/// Auto-flush del audit log a CAS cada 10 segundos. Ejecuta best-effort:
678/// si el flush falla lo logeamos pero no abortamos. La integridad del log
679/// queda garantizada por su hash chain — re-flushar es idempotente.
680fn spawn_audit_auto_flush(state: BrainState) {
681 tokio::spawn(async move {
682 let mut tick = tokio::time::interval(std::time::Duration::from_secs(10));
683 tick.tick().await; // descartar primer tick inmediato
684 loop {
685 tick.tick().await;
686 let mut audit = state.audit.write().await;
687 match audit.flush_to_cas() {
688 Ok(0) => {} // nada nuevo
689 Ok(n) => info!(written = n, total = audit.flushed_count(), "audit auto-flush"),
690 Err(e) => warn!(?e, "audit auto-flush falló"),
691 }
692 }
693 });
694}
695
696fn spawn_brain_introspect(state: BrainState) {
697 let path = brain_introspect_path();
698 tokio::spawn(async move {
699 let server = IntrospectServer::new(state);
700 if let Err(e) = server.serve(&path).await {
701 warn!(?e, "introspect server cayó");
702 }
703 });
704}
705
706/// Registra el evento en el observer y dispatcha cualquier regla matched.
707/// Para reglas Sequence: pasamos los últimos N eventos del observer como
708/// history al engine.
709async fn feed_brain(
710 brain: &BrainState,
711 sink: &brain_glue::GraphSink,
712 graph: &EnteGraph,
713 evt: &GraphEvent,
714) {
715 let Some((kind, subj)) = brain_glue::graph_event_to_brain(evt, graph) else { return };
716 let history: Vec<arje_brain::TimedEvent> = {
717 let mut obs = brain.observer.write().await;
718 obs.record(kind.clone());
719 // Snapshot de los últimos 16 eventos — suficiente para cualquier
720 // Sequence pattern razonable. Clone hace una sola alocación.
721 obs.recent(16).cloned().collect()
722 };
723 let rules = {
724 let engine = brain.engine.read().await;
725 engine.dispatch(&kind, &subj, &history)
726 };
727 if !rules.is_empty() {
728 arje_brain::dispatch_actions(&rules, sink).await;
729 }
730}
731
732/// Inicializa la malla `brahman-net` opcional. Activa sólo si
733/// `BRAHMAN_LISTEN_MULTIADDR` está set. Identidad libp2p persistente
734/// vía `keypair_store`. Bootstrap del DHT vía `BRAHMAN_BOOTSTRAP_PEERS`
735/// (lista coma-separada de multiaddrs, opcional).
736///
737/// Toda fase de setup degrada grácilmente: si la keypair no carga,
738/// si el listen falla, si bootstrap dial falla — loggea y devuelve
739/// `None`. El Init sigue funcionando en modo Unix-only.
740async fn setup_brahman_net(
741 dev_mode: bool,
742) -> Option<std::sync::Arc<brahman_net::BrahmanNet>> {
743 let listen_addr = match std::env::var("BRAHMAN_LISTEN_MULTIADDR") {
744 Ok(s) if !s.is_empty() => s,
745 _ => {
746 tracing::debug!(
747 "brahman-net deshabilitado (sin BRAHMAN_LISTEN_MULTIADDR)"
748 );
749 return None;
750 }
751 };
752
753 let multiaddr: brahman_net::Multiaddr = match listen_addr.parse() {
754 Ok(m) => m,
755 Err(e) => {
756 warn!(addr = %listen_addr, ?e, "BRAHMAN_LISTEN_MULTIADDR inválido — net deshabilitado");
757 return None;
758 }
759 };
760
761 let keypair_path = keypair_store::default_path(dev_mode);
762 let (keypair, loaded) = match keypair_store::load_or_generate(&keypair_path) {
763 Ok(kp) => kp,
764 Err(e) => {
765 warn!(path = %keypair_path.display(), ?e, "no pude cargar/generar keypair libp2p — net deshabilitado");
766 return None;
767 }
768 };
769 info!(
770 path = %keypair_path.display(),
771 peer_id = %keypair.public().to_peer_id(),
772 loaded = loaded,
773 "identidad libp2p {}",
774 if loaded { "cargada" } else { "generada y persistida" }
775 );
776
777 let net = match brahman_net::BrahmanNet::with_keypair(keypair) {
778 Ok(n) => std::sync::Arc::new(n),
779 Err(e) => {
780 warn!(?e, "BrahmanNet::with_keypair falló — net deshabilitado");
781 return None;
782 }
783 };
784
785 let actual = net.listen(multiaddr).await;
786 info!(addr = %actual, peer_id = %net.peer_id, "brahman-net escuchando");
787
788 // Bootstrap opcional: dial-ar a peers conocidos para entrar al
789 // DHT. Sin bootstrap, el nodo arranca aislado hasta que alguien
790 // se conecte a él.
791 if let Ok(bootstrap) = std::env::var("BRAHMAN_BOOTSTRAP_PEERS") {
792 let mut dialed = 0usize;
793 for entry in bootstrap.split(',').filter(|s| !s.is_empty()) {
794 match entry.parse::<brahman_net::Multiaddr>() {
795 Ok(addr) => {
796 net.dial(addr.clone());
797 dialed += 1;
798 tracing::debug!(peer = %addr, "dial bootstrap");
799 }
800 Err(e) => {
801 warn!(entry = %entry, ?e, "bootstrap multiaddr inválido — saltado");
802 }
803 }
804 }
805 if dialed > 0 {
806 info!(count = dialed, "bootstrap peers dial-eados");
807 }
808 }
809
810 Some(net)
811}
812
813/// Carga la política de peers libp2p desde los archivos apuntados por
814/// `BRAHMAN_PEER_ALLOWLIST` y/o `BRAHMAN_PEER_DENYLIST`, y arranca un
815/// watcher para hot reload sobre cualquier cambio.
816///
817/// - Sin ninguna env: `(None, None)` → modo totalmente abierto.
818/// - Con cualquiera (o ambas) set: política activa + watcher vivo.
819/// - Si los archivos fallan al cargar: degrada a `(None, None)`,
820/// loggea, NO rompe el bucle primordial (doctrina PID 1).
821///
822/// Devuelve la política y el `JoinHandle` del watcher (que el caller
823/// debe mantener para que el thread no se aborte). Si no hay paths,
824/// el watcher es un no-op que termina inmediato.
825fn setup_brahman_policy() -> (
826 Option<brahman_handshake::peer_policy::PeerPolicy>,
827 Option<std::thread::JoinHandle<()>>,
828) {
829 let allow_path = std::env::var("BRAHMAN_PEER_ALLOWLIST")
830 .ok()
831 .filter(|s| !s.is_empty());
832 let deny_path = std::env::var("BRAHMAN_PEER_DENYLIST")
833 .ok()
834 .filter(|s| !s.is_empty());
835
836 if allow_path.is_none() && deny_path.is_none() {
837 tracing::debug!(
838 "BRAHMAN_PEER_ALLOWLIST y BRAHMAN_PEER_DENYLIST no set — modo abierto (todo peer pasa)"
839 );
840 return (None, None);
841 }
842
843 let allow_pb = allow_path.as_deref().map(std::path::Path::new);
844 let deny_pb = deny_path.as_deref().map(std::path::Path::new);
845
846 let policy = match brahman_handshake::peer_policy::PeerPolicy::from_files(allow_pb, deny_pb) {
847 Ok(p) => p,
848 Err(e) => {
849 warn!(
850 ?e,
851 allow = ?allow_path,
852 deny = ?deny_path,
853 "policy de peers inválida — degradando a modo abierto (sin restricción)"
854 );
855 return (None, None);
856 }
857 };
858
859 let (allow_count, deny_count) = policy.sizes();
860 info!(
861 allow = ?allow_count,
862 deny = deny_count,
863 allow_path = ?allow_path,
864 deny_path = ?deny_path,
865 "policy de peers libp2p cargada"
866 );
867
868 // Spawn watcher para hot reload. Errores aquí no son fatales —
869 // tendrías política sin reload, que es razonable.
870 let watcher = match policy.spawn_watcher() {
871 Ok(h) => Some(h),
872 Err(e) => {
873 warn!(?e, "policy watcher no se pudo crear — hot reload deshabilitado");
874 None
875 }
876 };
877
878 (Some(policy), watcher)
879}