tawasuyu

tawasuyu/brahman

Rust 492TOML 194Markdown 53JSON 176939 Definiciones

feat(renaser): Fase 20 — Akasha Over Ether (grafo distribuido)

Tres mensajes y un EtherType propio bastan para extender el grafo de
objetos —direccionado por contenido, ya BLAKE3— a otras maquinas
renaser que escuchen en la misma red de capa-2. Sin TCP, sin IP,
sin DNS.

Crate nueva 'akasha/' (no_std compartido, gemela de 'formato',
excluida del workspace):

  - MensajeAkasha enum con SolicitarObjeto(id), ProveedorObjeto(id,
    payload), AnunciarRaiz(id).
  - Codec: postcard (mismo que ya usa el grafo en disco).
  - EtherType: 0x88B5. MAX_PAYLOAD_AKASHA = 1486 (MTU sin fragmentar).
  - Helpers componer_frame(src, dst, msg) y analizar_frame(bytes) que
    distinguen EtherType ajeno, frame truncado y payload basura.
  - 6 pruebas unitarias en verde.

Modulo nuevo 'kernel/src/akasha.rs' con tres oficios:

  1. Demuxer (drenar_y_demultiplexar): drena la cola RX del dispositivo
     virtio-net y demultiplexa: frames AoE con payload valido los
     procesa el respondedor; el resto va a una cola del userspace que
     'sys_net_recibir' ahora lee. Frames 0x88B5 con payload
     no-postcard (saludo de pregon) se cuentan y tambien viajan al
     userspace.

  2. Atencion de mensajes (procesar):
     - SolicitarObjeto(id): consulta almacen::recuperar; si tenemos el
       objeto, respondemos ProveedorObjeto unicast con objeto.serializar()
       y re-hashing de defensa en profundidad.
     - ProveedorObjeto(id, payload): verifica blake3(payload)==id antes
       de absorber con almacen::almacenar.
     - AnunciarRaiz(id): si ignoramos el nodo, le solicitamos al emisor.

  3. Faro periodico (difundir_raiz cada 5 s): broadcast del hash del
     manifiesto actual. Cadencia medida contra reloj::milisegundos(),
     no contra los awaits — el interprete wasmi de los apps degrada
     la cadencia de EsperaFrame::await a varios cientos de ms, asi
     que se mide contra el reloj monotono y los oficios per-fotograma
     se enganchan al tic del compositor (cuyo latido es fiable).

Contadores ResumenAkasha (rx/tx por variante, descartados, cola del
usuario) listos para un futuro indicador AoE en la barra de tareas.

Cambios complementarios:

  - sys_net_recibir lee de akasha::pop_usuario, no de
    drivers::red::recibir_en (que queda #[allow(dead_code)] como
    primitiva del driver para diagnostico).
  - tarea_red queda corta: envia un ARP al gateway y termina. El
    demuxer y el faro viven en el tic del compositor.

Verificacion:

  - 'cargo test -p akasha' → 6 pruebas en verde.
  - QEMU headless 60 s con -object filter-dump → 14 frames: 11
    AnunciarRaiz (Δ promedio 5.86 s sobre 5.00 s de target), 2 ARP
    y el pregon hello. Cada AnunciarRaiz lleva el hash del manifiesto
    '2f3deadfcc7dae25..' en 33 bytes postcard sobre 47 bytes de frame.
  - COM1 vuelca 'akasha :: ANUNCIO emitido :: raiz=2f3deadfcc7dae25..'
    en cada disparo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

42fee6fcbc7eb98267f5abfad803018bf43224a9 · por sergio · (hace 5 meses) · padre 07ab095d4 · ver el árbol en este commit

Delta semántico · 55

renaser/akasha/src/lib.rs

renaser/kernel/src/akasha.rs

renaser/kernel/src/main.rs

renaser/kernel/src/wasm/env.rs

14 archivos · +1075 −24

diff --git a/renaser/CHANGELOG.md b/renaser/CHANGELOG.md
index 6e9b7b5..9d39b19 100644
--- a/renaser/CHANGELOG.md
+++ b/renaser/CHANGELOG.md
@@ -1222,3 +1222,89 @@ misma capacidad a los apps: tres capacidades nuevas (`sys_net_mac`,
 - Captura de pantalla: la ventana `pregon :: voz hacia la red` aparece
   teselada, su pestaña sale en la barra de tareas entre `bitacora` y
   `tonada`, y la consola anuncia `manifiesto :: 9 apps nacidas del grafo`.
+
+---
+
+## Fase 20 — Akasha Over Ether: el grafo distribuido — 2026-05-23
+
+La red dejo de ser un puro medio para mensajes ad-hoc y se vuelve el cable
+por donde el GRAFO de objetos se extiende a otras maquinas renaser. Tres
+mensajes bastan; `postcard` los serializa; el EtherType `0x88B5` los
+transporta; ningun byte de TCP, IP, DNS o cabecera transporte de los anos 80.
+
+### Añadido
+- **Crate `akasha/`** — nucleo `#![no_std]` compartido (gemela de `formato`),
+  excluida del workspace anfitrion. Define `MensajeAkasha` con tres
+  variantes —`SolicitarObjeto(id)`, `ProveedorObjeto(id, payload)`,
+  `AnunciarRaiz(id)`—, las helpers `componer_frame(src, dst, msg)` y
+  `analizar_frame(bytes) -> Result<(Mac, MensajeAkasha)>`, y las constantes
+  `ETHER_TYPE_AKASHA = 0x88B5`, `MAX_PAYLOAD_AKASHA = 1486`. Seis pruebas
+  unitarias cubren el ida-y-vuelta de cada variante, la distincion entre
+  EtherType ajeno, frame truncado y payload basura.
+- **Modulo `kernel/src/akasha.rs`** — el respondedor del nucleo. Tres oficios:
+  1. **Demuxer** (`drenar_y_demultiplexar`): drena la cola RX del dispositivo
+     virtio-net y demultiplexa: frames AoE con payload valido los procesa
+     `procesar`; el resto va a la cola del userspace.
+  2. **Atencion de mensajes** (`procesar`): `SolicitarObjeto` busca en
+     `almacen` y, si tiene el objeto, responde `ProveedorObjeto` unicast;
+     `ProveedorObjeto` verifica integridad (`blake3(payload) == id`) y
+     absorbe al grafo local con `almacen::almacenar`; `AnunciarRaiz` se
+     contabiliza y, si no tenemos el nodo, le solicita al emisor.
+  3. **Faro** (`difundir_raiz` cada 5 s): difunde por broadcast el hash del
+     manifiesto actual — el faro Akasha que delata nuestra presencia en la
+     red de capa-2.
+- **Cola del userspace** — el demuxer empuja los frames no-AoE a una
+  `VecDeque<Vec<u8>>` con profundidad maxima 64; `sys_net_recibir` ahora
+  pasa por `akasha::pop_usuario(buf)` en lugar de leer directamente del
+  driver. Asi el kernel filtra Akasha sin disputarle paquetes al userspace.
+- **`tic_compositor`** — el punto de entrada de los oficios per-fotograma de
+  AoE, llamado desde el tic del compositor (cuyo latido es fiable). El
+  demuxer corre en cada vuelta; el faro se dispara cuando el reloj monotono
+  cruza `INTERVALO_FARO_MS` (5 s).
+- **Contadores** (`ResumenAkasha`): `rx_solicitudes`, `rx_proveedores`,
+  `rx_anuncios`, `tx_*` equivalentes, `rx_descartados`, `usuario_encolados`,
+  `usuario_desbordados`. Quedaran de fundamento para un futuro indicador AoE
+  en la barra de tareas.
+
+### Cambiado
+- **`tarea_red`** vuelve a su rol simple: enviar UN ARP request al gateway de
+  QEMU al arrancar. El demuxer y el faro pasaron al tic del compositor —
+  cuyo latido cooperativo es el unico GARANTIZADO bajo carga del reactor.
+  Un bucle propio en `tarea_red` (intentado primero) escala mal porque la
+  cadencia de `EsperaFrame::await` se degrada cuando los apps WASM bajo
+  `wasmi` ocupan el reactor (cada tic de app son decenas de ms de
+  interprete); medirlo contra el reloj monotono y enganchar al compositor
+  resuelve la falla con limpieza.
+- **`sys_net_recibir`** lee de la cola del userspace que mantiene `akasha`,
+  no del driver. El bloque doc en `wasm/env.rs` lo refleja.
+- **`drivers::red::recibir_en`** queda como primitiva del driver con
+  `#[allow(dead_code)]`: ya no la consume el sistema, pero la conservamos
+  para futuras herramientas de diagnostico que quieran leer la cola RX cruda.
+- **Demuxer politico**: frames `0x88B5` con payload basura para postcard —
+  por ejemplo el saludo en texto plano de `pregon`— se cuentan en
+  `RX_DESCARTADOS` pero TAMBIEN viajan a la cola del userspace. Asi pregon
+  o cualquier otro app que comparta el EtherType experimental pueda
+  recibirlos sin que el kernel se los robe.
+
+### Verificado
+- `cargo test -p akasha` — seis pruebas en verde:
+  ```
+  test pruebas::anuncio_de_raiz_viaja_compacto ... ok
+  test pruebas::componer_y_analizar_solicitud_es_simetrico ... ok
+  test pruebas::ethertype_ajeno_se_distingue ... ok
+  test pruebas::frame_demasiado_corto_se_distingue ... ok
+  test pruebas::payload_invalido_se_distingue ... ok
+  test pruebas::proveedor_lleva_payload_arbitrario ... ok
+  ```
+- QEMU headless 60 s, `-object filter-dump,file=/tmp/renaser-aoe.pcap`. El
+  pcap captura 14 frames:
+  ```
+  ARP: 2  (request del kernel + reply del gateway)
+  pregon: 1  (saludo texto plano)
+  AnunciarRaiz: 11  (faros del kernel, Δ promedio 5.86 s sobre 60 s)
+  ```
+  Cada AnunciarRaiz lleva como payload el hash del manifiesto:
+  `2f3deadfcc7dae25..` (postcard de 33 bytes — variante + hash — sobre un
+  frame Ethernet total de 47 bytes).
+- COM1 vuelca `akasha :: ANUNCIO emitido :: raiz=2f3deadfcc7dae25..` por
+  cada disparo; en una corrida de 60 s aparecen 11 anuncios consecutivos.
diff --git a/renaser/CLAUDE.md b/renaser/CLAUDE.md
index d78cc73..1bbe374 100644
--- a/renaser/CLAUDE.md
+++ b/renaser/CLAUDE.md
@@ -92,10 +92,14 @@ lanzador a la izquierda y reloj `mm:ss` a la derecha que late cada
 segundo— la Fase 17 COMPLETA —`bitacora`, editor de texto que persiste entre
 arranques en el grafo de objetos (tipografía 8×8 embebida)— la Fase 18
 COMPLETA —red: virtio-net + ARP al gateway de QEMU + recepción de
-paquetes registrada por COM1— y la Fase 19 COMPLETA —voz del userspace
+paquetes registrada por COM1—, la Fase 19 COMPLETA —voz del userspace
 hacia la red: capacidades `sys_net_mac` / `sys_net_enviar` / `sys_net_recibir`
 + la app `pregon` que pregona su presencia con un broadcast Ethernet
-y muestra el tráfico entrante—. Todo verificado en QEMU. Ver `ROADMAP.md`.
+y muestra el tráfico entrante— y la Fase 20 COMPLETA —Akasha Over Ether:
+crate `akasha` (no_std compartido) con `MensajeAkasha` postcard sobre
+EtherType 0x88B5 + respondedor en el kernel (demuxer, atención de
+SolicitarObjeto / ProveedorObjeto / AnunciarRaiz, faro periódico cada
+5 s contra reloj monótono)—. Todo verificado en QEMU. Ver `ROADMAP.md`.
 
 ## Flujo de trabajo
 
diff --git a/renaser/Cargo.lock b/renaser/Cargo.lock
index 6dea5a4..76a61db 100644
--- a/renaser/Cargo.lock
+++ b/renaser/Cargo.lock
@@ -2,6 +2,14 @@
 # It is not intended for manual editing.
 version = 4
 
+[[package]]
+name = "akasha"
+version = "0.1.0"
+dependencies = [
+ "postcard",
+ "serde",
+]
+
 [[package]]
 name = "allocator-api2"
 version = "0.2.21"
@@ -456,6 +464,7 @@ dependencies = [
 name = "kernel"
 version = "0.1.0"
 dependencies = [
+ "akasha",
  "bootloader_api",
  "crossbeam-queue",
  "embedded-graphics",
diff --git a/renaser/Cargo.toml b/renaser/Cargo.toml
index 1560dd0..b470169 100644
--- a/renaser/Cargo.toml
+++ b/renaser/Cargo.toml
@@ -15,10 +15,11 @@ resolver = "2"
 members = ["boot"]
 # El kernel (bare-metal) y las apps WASM (target wasm32) se compilan aparte,
 # cada cual con su propio target; quedan fuera del espacio de trabajo.
-# `formato` —el formato del grafo en disco— tambien se excluye: es un nucleo
-# `no_std` que enlaza el kernel bare-metal, asi que se compila como dependencia
-# de cada lado (kernel y boot) y no como miembro del workspace anfitrion.
-exclude = ["kernel", "apps", "formato"]
+# `formato` —el formato del grafo en disco— y `akasha` —el protocolo Akasha
+# Over Ether (Fase 20)— tambien se excluyen: son nucleos `no_std` que enlaza
+# el kernel bare-metal, asi que se compilan como dependencia de cada lado
+# (kernel y boot) y no como miembros del workspace anfitrion.
+exclude = ["kernel", "apps", "formato", "akasha"]
 
 # -----------------------------------------------------------------------------
 #  Metadatos compartidos: cada miembro hereda esta identidad con `*.workspace`.
diff --git a/renaser/DIARIO.md b/renaser/DIARIO.md
index aff99c2..8a31aee 100644
--- a/renaser/DIARIO.md
+++ b/renaser/DIARIO.md
@@ -608,4 +608,31 @@ que la voz ya no es de uno solo. Ahora la casa habla en coro.
 
 ---
 
+## Akasha — el grafo se vuelve aire
+
+Pregón gritaba sin alfabeto: una frase corta dirigida a quien quisiera
+oír. Hoy la casa aprende su primer idioma propio para la red, y se le
+ocurre uno sencillo, con apenas tres palabras: *pide*, *toma*, *aquí
+estoy*. Pide un objeto del archivo por su huella; recibe el objeto en
+respuesta; o anuncia, sin destinatario, la huella de su biblioteca
+actual para que cualquiera que esté escuchando sepa por dónde puede
+acompañarla.
+
+El nombre es viejo. *Akasha* es la palabra sánscrita para el éter, el
+soporte de lo sutil; *Akasha Over Ether* es nuestro chiste a dos
+niveles: el grafo viaja por el éter del cable, y la huella del grafo
+—las treinta y dos cifras del BLAKE3— es la firma sutil de cada
+objeto. Con tres frases bastan: el archivo de la casa deja de ser
+exclusivamente local. Quien sintonice la red de la sala podrá,
+mañana, recibir un objeto suyo o pedirle uno.
+
+De momento la casa habla sola. Cada cinco segundos, sin esperar a
+nadie, suelta su faro: *aquí estoy, mi raíz es esta*. Si otra renaser
+se encendiera en la misma sala, escucharía y podría empezar a pedir
+los objetos que le faltan. Hoy no pasa eso porque sólo hay una casa,
+pero el alfabeto está. Y, cuando un día se encienda la segunda,
+empezará el diálogo sin que haya que añadir ni una palabra más.
+
+---
+
 *El diario continúa. La próxima página la escribirá la próxima jornada.*
diff --git a/renaser/ROADMAP.md b/renaser/ROADMAP.md
index a47af82..ce6f28c 100644
--- a/renaser/ROADMAP.md
+++ b/renaser/ROADMAP.md
@@ -357,6 +357,49 @@ Líneas abiertas posteriores: una pila mínima ARP/IP/UDP para los apps;
 un servidor de eco / un cliente DNS de juguete; reciclado de las
 ranuras de ventana cerradas; audio con varias voces (PCM).
 
+## Fase 20 — Akasha Over Ether: el grafo distribuido (completada)
+
+El grafo de objetos persistentes —direccionado por contenido, ya BLAKE3—
+deja de ser una propiedad LOCAL del disco y se vuelve una propiedad
+DISTRIBUIDA del cable. Sin TCP, sin IP, sin DNS: tres mensajes y un
+EtherType propio bastan.
+
+- Crate `akasha/` (no_std compartido, gemela de `formato`): el
+  protocolo. `MensajeAkasha` con `SolicitarObjeto(id)`,
+  `ProveedorObjeto(id, payload)`, `AnunciarRaiz(id)`. Serializado con
+  `postcard`. Helpers `componer_frame` y `analizar_frame`. EtherType
+  experimental `0x88B5`. Seis pruebas unitarias.
+- Módulo `kernel/src/akasha.rs`: el respondedor. Drena la cola RX del
+  dispositivo de red y **demultiplexa**: los frames AoE legítimos los
+  procesa en el núcleo; el resto va a una cola del userspace que
+  `sys_net_recibir` lee. Atiende `SolicitarObjeto` consultando
+  `almacen`, absorbe `ProveedorObjeto` verificando integridad, registra
+  `AnunciarRaiz` y, si ignoramos el nodo anunciado, le pedimos al emisor.
+- Faro periódico: cada 5 s, `AnunciarRaiz` con el hash del manifiesto se
+  difunde por broadcast. La cadencia se mide contra el reloj monótono —
+  no contra los awaits—, por lo que el ritmo del faro es independiente
+  de cuánto trabajo del reactor consume cada vuelta.
+- `tic_compositor`: el pulso fiable. Los oficios AoE se enganchan al tic
+  del compositor (cuyo latido cooperativo es el único garantizado bajo
+  carga del reactor); una tarea propia con `EsperaFrame::await` en bucle
+  no escalaba porque el intérprete `wasmi` de los apps degrada la
+  cadencia de los awaits a varios cientos de ms.
+- `sys_net_recibir` lee de la cola del userspace que mantiene `akasha`,
+  no del driver. `drivers::red::recibir_en` queda como primitiva del
+  driver para futuras herramientas de diagnóstico.
+
+Verificada con `cargo test -p akasha` (6 pruebas verdes) y con QEMU
+headless + `-object filter-dump`. En 60 s de captura: 11 `AnunciarRaiz`
+con Δ promedio 5.86 s, un ARP request + reply y el saludo en texto
+plano de `pregon`. La casa ya tiene su propio idioma de red — y un faro
+que late.
+
+Líneas abiertas posteriores: una pila ARP/IP/UDP mínima para los apps;
+un peer real en una segunda VM para ver la replicación end-to-end
+`AnunciarRaiz → SolicitarObjeto → ProveedorObjeto`; un indicador AoE en
+la barra de tareas que pinte los contadores de `ResumenAkasha`;
+reciclado de las ranuras de ventana cerradas; audio con varias voces.
+
 ## Principios que persisten entre fases
 
 - Reutilizar infraestructura madura de la comunidad antes que reinventar.
diff --git a/renaser/akasha/Cargo.lock b/renaser/akasha/Cargo.lock
new file mode 100644
index 0000000..43f3ec6
--- /dev/null
+++ b/renaser/akasha/Cargo.lock
@@ -0,0 +1,129 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "akasha"
+version = "0.1.0"
+dependencies = [
+ "postcard",
+ "serde",
+]
+
+[[package]]
+name = "cobs"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
+dependencies = [
+ "thiserror",
+]
+
+[[package]]
+name = "embedded-io"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
+
+[[package]]
+name = "embedded-io"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
+
+[[package]]
+name = "postcard"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
+dependencies = [
+ "cobs",
+ "embedded-io 0.4.0",
+ "embedded-io 0.6.1",
+ "serde",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
diff --git a/renaser/akasha/Cargo.toml b/renaser/akasha/Cargo.toml
new file mode 100644
index 0000000..6288fb7
--- /dev/null
+++ b/renaser/akasha/Cargo.toml
@@ -0,0 +1,38 @@
+# =============================================================================
+#  renaser :: akasha — protocolo Akasha Over Ether (AoE)
+# -----------------------------------------------------------------------------
+#  Nucleo `#![no_std]` COMPARTIDO: la verdad UNICA del protocolo de red nativo
+#  de renaser. Lo enlaza el kernel bare-metal (target `x86_64-unknown-none`) y,
+#  por ser no_std, lo compila tambien sin friccion el anfitrion `boot` y
+#  cualquier otra pieza que quiera dialogar en el mismo idioma — incluida
+#  alguna futura app del userspace.
+#
+#  El protocolo extiende el GRAFO de objetos persistentes (Fase 6) sobre una
+#  red Ethernet cruda, sin pasar por TCP/IP. Tres mensajes bastan: solicitar
+#  un objeto por su hash BLAKE3, proveerlo de vuelta, anunciar la raiz actual
+#  del sistema. Se serializa con `postcard` —compacto y deterministico— y
+#  viaja en frames de EtherType experimental `0x88B5`.
+#
+#  Queda EXCLUIDO del espacio de trabajo (ver el Cargo.toml raiz), como el
+#  kernel y como `formato`: lo consume un paquete bare-metal, asi que fija
+#  sus versiones de forma explicita.
+# =============================================================================
+
+[package]
+name = "akasha"
+version = "0.1.0"
+edition = "2021"
+license = "MPL-2.0"
+authors = ["JL Soltech <gerencia@jlsoltech.com>"]
+description = "renaser :: protocolo Akasha Over Ether (AoE) — grafo distribuido sobre Ethernet"
+
+[lib]
+bench = false
+doctest = false
+
+[dependencies]
+# `serde` aporta los rasgos de (de)serializacion; `postcard` los materializa en
+# un formato binario compacto, el mismo que ya usa `formato` para el disco.
+# Asi el grafo habla el mismo idioma en el disco y en el cable.
+serde = { version = "1", default-features = false, features = ["alloc", "derive"] }
+postcard = { version = "1", default-features = false, features = ["alloc"] }
diff --git a/renaser/akasha/src/lib.rs b/renaser/akasha/src/lib.rs
new file mode 100644
index 0000000..12ef6b1
--- /dev/null
+++ b/renaser/akasha/src/lib.rs
@@ -0,0 +1,252 @@
+// =============================================================================
+//  renaser :: akasha — Akasha Over Ether (AoE)
+// -----------------------------------------------------------------------------
+//  Fase 19 demostro que un app del userspace puede inyectar frames Ethernet
+//  crudos al cable —`pregon` gritando «hola» con su EtherType experimental—,
+//  pero ese saludo era texto plano, sin estructura. La Fase 20 da el siguiente
+//  paso natural: **fundar un protocolo nativo del ecosistema** para que
+//  renaser hable consigo mismo a traves de la red, sin TCP, sin IP, sin las
+//  sobrecargas de cabecera de los años 80.
+//
+//  AoE viaja DIRECTAMENTE sobre Ethernet (`EtherType = 0x88B5`, rango
+//  experimental reservado por IEEE para uso local). Cada frame transporta un
+//  `MensajeAkasha` serializado con `postcard` —el mismo codec que ya usa
+//  `formato` para el grafo en disco—. Tres mensajes bastan:
+//
+//    1. `SolicitarObjeto(id)`   — pide un nodo del grafo por su hash BLAKE3.
+//    2. `ProveedorObjeto(id, d)` — responde con el payload binario del nodo.
+//    3. `AnunciarRaiz(id)`      — difunde el hash de la raiz del sistema.
+//
+//  Con esto basta para extender el grafo de objetos —direccionado por
+//  contenido, inmutable, ya BLAKE3— a OTRAS maquinas renaser que escuchen en
+//  la misma red de capa-2. El receptor de un `AnunciarRaiz` puede comparar
+//  hashes, descubrir que le falta un nodo, pedirlo con `SolicitarObjeto` y
+//  ensamblar el grafo del par. El grafo deja de ser una propiedad LOCAL del
+//  disco y se vuelve una propiedad DISTRIBUIDA del cable.
+//
+//  Esta crate es la unica verdad del protocolo. Es un nucleo `#![no_std]` —el
+//  kernel bare-metal la enlaza—; cualquier app, daemon u orquestador que
+//  hable AoE consume estos mismos tipos. Ningun otro modulo redefine ni un
+//  byte del trazado del frame.
+// =============================================================================
+
+#![no_std]
+#![deny(unsafe_op_in_unsafe_fn)]
+
+extern crate alloc;
+
+use alloc::vec::Vec;
+
+use serde::{Deserialize, Serialize};
+
+// =============================================================================
+//  Constantes del protocolo en el cable
+// =============================================================================
+
+/// EtherType de AoE, rango experimental reservado por IEEE para uso local
+/// (0x88B5/0x88B6). renaser elige el primero. Cualquier frame del cable que
+/// no porte este EtherType NO es Akasha, y se entrega al userspace.
+pub const ETHER_TYPE_AKASHA: u16 = 0x88B5;
+
+/// Tamaño de la cabecera Ethernet, en bytes (dst + src + ethertype).
+pub const CABECERA_ETHERNET: usize = 14;
+
+/// Maximo del payload AoE serializado, en bytes. Acotado para que el frame
+/// completo (cabecera + payload) no exceda una MTU Ethernet sin VLAN
+/// (1500 - 14 = 1486) y se transmita SIN fragmentar. Si un mensaje no cabe,
+/// el llamante debe partirlo en varios objetos del grafo y referirse a ellos.
+pub const MAX_PAYLOAD_AKASHA: usize = 1486;
+
+/// MAC de broadcast — la difunde todo Ethernet, equivalente a `255.255.255.255`
+/// en IPv4 pero en capa-2 pura.
+pub const MAC_BROADCAST: [u8; 6] = [0xff; 6];
+
+/// El identificador de un objeto del grafo: el hash BLAKE3 de su forma
+/// serializada. En un almacen direccionado por contenido, la identidad ES el
+/// contenido. Coincide byte a byte con `formato::Hash` —AoE habla el mismo
+/// idioma de identidad que el grafo en disco—.
+pub type ObjectId = [u8; 32];
+
+/// Una direccion MAC, en seis bytes.
+pub type Mac = [u8; 6];
+
+// =============================================================================
+//  El mensaje
+// =============================================================================
+
+/// Un mensaje AoE — la unidad de protocolo que viaja en un frame de
+/// `ETHER_TYPE_AKASHA`. Tres variantes bastan para fundar un grafo distribuido:
+///
+/// - `SolicitarObjeto`  pregunta por un objeto identificado por su hash.
+/// - `ProveedorObjeto`  responde con el payload binario del objeto. El
+///   receptor recompone el `formato::Objeto` aplicando su deserializador a
+///   `payload` y verificando que `blake3(payload) == id`.
+/// - `AnunciarRaiz`     difunde el hash de la raiz —el ancla del sistema
+///   actual—. Sirve como faro: quien escuche y carezca de ese nodo en su
+///   grafo local puede iniciar una solicitud.
+///
+/// La codificacion `postcard` es estable y compacta: un `SolicitarObjeto`
+/// ocupa 33 bytes (1 de variante + 32 de hash) en el cable.
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub enum MensajeAkasha {
+    /// Solicito el objeto identificado por este hash.
+    SolicitarObjeto(ObjectId),
+    /// Aqui esta el payload binario del objeto identificado por este hash.
+    /// El payload es la forma serializada `postcard` de un `formato::Objeto`,
+    /// y el receptor DEBE verificar que `blake3(payload) == id` antes de
+    /// confiar en el contenido.
+    ProveedorObjeto(ObjectId, Vec<u8>),
+    /// Difundo el hash de mi raiz actual — quien me escuche y le falte este
+    /// nodo en su grafo local puede pedirmelo.
+    AnunciarRaiz(ObjectId),
+}
+
+/// El motivo por el que un frame AoE no se pudo componer o analizar.
+#[derive(Clone, Copy, Debug)]
+pub enum ErrorAkasha {
+    /// El payload serializado excede `MAX_PAYLOAD_AKASHA`.
+    PayloadDemasiadoLargo,
+    /// El frame en el cable es mas corto que la cabecera Ethernet.
+    FrameDemasiadoCorto,
+    /// El EtherType del frame no es `ETHER_TYPE_AKASHA`.
+    EtherTypeAjeno,
+    /// `postcard` no supo deserializar el payload (basura, version distinta,
+    /// otro protocolo experimental ajeno).
+    PayloadInvalido,
+}
+
+// =============================================================================
+//  Composicion y analisis de frames
+// =============================================================================
+
+/// Compone un frame Ethernet completo —cabecera (14 bytes) + payload AoE
+/// (`postcard(mensaje)`)— listo para entregar al driver. El llamante elige el
+/// destino (`MAC_BROADCAST` para difundir) y firma el frame con su propia
+/// MAC. Devuelve el frame completo, o un error si el mensaje serializado
+/// excede `MAX_PAYLOAD_AKASHA`.
+pub fn componer_frame(
+    src: Mac,
+    dst: Mac,
+    mensaje: &MensajeAkasha,
+) -> Result<Vec<u8>, ErrorAkasha> {
+    let payload =
+        postcard::to_allocvec(mensaje).map_err(|_| ErrorAkasha::PayloadDemasiadoLargo)?;
+    if payload.len() > MAX_PAYLOAD_AKASHA {
+        return Err(ErrorAkasha::PayloadDemasiadoLargo);
+    }
+    let mut frame = Vec::with_capacity(CABECERA_ETHERNET + payload.len());
+    frame.extend_from_slice(&dst);
+    frame.extend_from_slice(&src);
+    frame.extend_from_slice(&ETHER_TYPE_AKASHA.to_be_bytes());
+    frame.extend_from_slice(&payload);
+    Ok(frame)
+}
+
+/// Analiza un frame Ethernet entrante. Si su EtherType es `ETHER_TYPE_AKASHA`
+/// y el payload deserializa como un `MensajeAkasha`, devuelve la MAC de origen
+/// y el mensaje. En cualquier otro caso —EtherType ajeno, frame truncado,
+/// `postcard` no le encuentra sentido al payload— devuelve un `ErrorAkasha`
+/// que distingue el motivo: el llamante puede entonces decidir si lo entrega
+/// al userspace (otro protocolo) o lo descarta (basura).
+pub fn analizar_frame(frame: &[u8]) -> Result<(Mac, MensajeAkasha), ErrorAkasha> {
+    if frame.len() < CABECERA_ETHERNET {
+        return Err(ErrorAkasha::FrameDemasiadoCorto);
+    }
+    let etype = u16::from_be_bytes([frame[12], frame[13]]);
+    if etype != ETHER_TYPE_AKASHA {
+        return Err(ErrorAkasha::EtherTypeAjeno);
+    }
+    let mut src: Mac = [0; 6];
+    src.copy_from_slice(&frame[6..12]);
+    let payload = &frame[CABECERA_ETHERNET..];
+    let mensaje: MensajeAkasha =
+        postcard::from_bytes(payload).map_err(|_| ErrorAkasha::PayloadInvalido)?;
+    Ok((src, mensaje))
+}
+
+// =============================================================================
+//  Pruebas
+// =============================================================================
+
+#[cfg(test)]
+mod pruebas {
+    use super::*;
+    use alloc::vec;
+
+    const MAC_A: Mac = [0x52, 0x54, 0x00, 0x12, 0x34, 0x56];
+    const MAC_B: Mac = [0x52, 0x54, 0x00, 0xAB, 0xCD, 0xEF];
+    const HASH_X: ObjectId = [0x11; 32];
+
+    #[test]
+    fn componer_y_analizar_solicitud_es_simetrico() {
+        let msg = MensajeAkasha::SolicitarObjeto(HASH_X);
+        let frame = componer_frame(MAC_A, MAC_B, &msg).unwrap();
+        // Cabecera: dst + src + ethertype.
+        assert_eq!(&frame[0..6], &MAC_B);
+        assert_eq!(&frame[6..12], &MAC_A);
+        assert_eq!(&frame[12..14], &ETHER_TYPE_AKASHA.to_be_bytes());
+        let (src, rec) = analizar_frame(&frame).unwrap();
+        assert_eq!(src, MAC_A);
+        match rec {
+            MensajeAkasha::SolicitarObjeto(id) => assert_eq!(id, HASH_X),
+            otro => panic!("variante inesperada: {otro:?}"),
+        }
+    }
+
+    #[test]
+    fn anuncio_de_raiz_viaja_compacto() {
+        let frame =
+            componer_frame(MAC_A, MAC_BROADCAST, &MensajeAkasha::AnunciarRaiz(HASH_X))
+                .unwrap();
+        // 14 cabecera + 1 byte variante + 32 hash = 47 bytes en el cable.
+        assert_eq!(frame.len(), 47);
+        assert_eq!(&frame[0..6], &MAC_BROADCAST);
+    }
+
+    #[test]
+    fn proveedor_lleva_payload_arbitrario() {
+        let payload = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
+        let msg = MensajeAkasha::ProveedorObjeto(HASH_X, payload.clone());
+        let frame = componer_frame(MAC_A, MAC_B, &msg).unwrap();
+        let (_, rec) = analizar_frame(&frame).unwrap();
+        match rec {
+            MensajeAkasha::ProveedorObjeto(id, p) => {
+                assert_eq!(id, HASH_X);
+                assert_eq!(p, payload);
+            }
+            otro => panic!("variante inesperada: {otro:?}"),
+        }
+    }
+
+    #[test]
+    fn frame_demasiado_corto_se_distingue() {
+        assert!(matches!(
+            analizar_frame(&[0u8; 8]),
+            Err(ErrorAkasha::FrameDemasiadoCorto)
+        ));
+    }
+
+    #[test]
+    fn ethertype_ajeno_se_distingue() {
+        // Cabecera valida pero EtherType de IPv4 (0x0800).
+        let mut frame = [0u8; CABECERA_ETHERNET];
+        frame[12] = 0x08;
+        frame[13] = 0x00;
+        assert!(matches!(
+            analizar_frame(&frame),
+            Err(ErrorAkasha::EtherTypeAjeno)
+        ));
+    }
+
+    #[test]
+    fn payload_invalido_se_distingue() {
+        // EtherType correcto, pero el payload es basura para postcard.
+        let mut frame = [0u8; CABECERA_ETHERNET + 4];
+        frame[12..14].copy_from_slice(&ETHER_TYPE_AKASHA.to_be_bytes());
+        frame[14..].copy_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]);
+        assert!(matches!(
+            analizar_frame(&frame),
+            Err(ErrorAkasha::PayloadInvalido)
+        ));
+    }
+}
diff --git a/renaser/kernel/Cargo.toml b/renaser/kernel/Cargo.toml
index 0573f72..8f8ae6e 100644
--- a/renaser/kernel/Cargo.toml
+++ b/renaser/kernel/Cargo.toml
@@ -76,6 +76,14 @@ virtio-drivers = { version = "0.13", default-features = false, features = ["allo
 # sola verdad del formato de disco, imposible de divergir entre los dos lados.
 formato = { path = "../formato" }
 
+# --- Fase 20 :: Akasha Over Ether ---
+# El protocolo nativo de red de renaser: mensajes `MensajeAkasha` serializados
+# con `postcard`, sobre Ethernet crudo. Mismo patron que `formato`: nucleo
+# `no_std` compartido, no miembro del workspace anfitrion. El kernel y boot lo
+# enlazan, y cualquier app o daemon que quiera hablar AoE en el futuro tomara
+# la misma crate — una sola verdad del protocolo.
+akasha = { path = "../akasha" }
+
 # --- Fase 8 (preparación) :: el compositor ---
 # `mirada-layout` es el motor de teselado del compositor de brahman —
 # geometría pura (rectángulos, foco, Z-order), `no_std`, sin smithay ni
diff --git a/renaser/kernel/src/akasha.rs b/renaser/kernel/src/akasha.rs
new file mode 100644
index 0000000..c6f5360
--- /dev/null
+++ b/renaser/kernel/src/akasha.rs
@@ -0,0 +1,431 @@
+// =============================================================================
+//  renaser :: kernel/src/akasha — Fase 20 :: Akasha Over Ether
+// -----------------------------------------------------------------------------
+//  El servicio del kernel que habla AoE: el respondedor de Akasha Over Ether.
+//  Tres oficios:
+//
+//   1. Drena la cola RX del dispositivo de red y DEMULTIPLEXA cada frame:
+//      - Si su EtherType es `0x88B5` y el payload deserializa como un
+//        `MensajeAkasha`, lo procesa en el kernel.
+//      - Cualquier otro frame se encola hacia el userspace: las apps lo
+//        recibiran via `sys_net_recibir` como hasta ahora.
+//   2. Atiende los mensajes AoE:
+//      - `SolicitarObjeto(id)`     → si tenemos `id` en el grafo,
+//                                    respondemos con `ProveedorObjeto`.
+//      - `ProveedorObjeto(id, p)`  → si la integridad cuadra, lo absorbemos
+//                                    al grafo local.
+//      - `AnunciarRaiz(id)`        → se contabiliza y, si no tenemos el
+//                                    nodo, se le solicita al emisor.
+//   3. Difunde periodicamente nuestra raiz del grafo (`AnunciarRaiz` con el
+//      hash del manifiesto) — el faro que delata nuestra presencia.
+//
+//  Con esto, el grafo de objetos —direccionado por contenido, ya BLAKE3— deja
+//  de ser una propiedad local del disco y empieza a ser una propiedad
+//  distribuida del cable. Tres frames bastan, sin TCP, sin IP, sin DNS.
+// =============================================================================
+
+use alloc::collections::VecDeque;
+use alloc::vec::Vec;
+use core::fmt::Write;
+use core::sync::atomic::{AtomicU64, Ordering};
+
+use spin::{Mutex, Once};
+
+use crate::async_system::reloj;
+
+use akasha::{
+    analizar_frame, componer_frame, ErrorAkasha, Mac, MensajeAkasha, MAC_BROADCAST,
+};
+use formato::Hash;
+
+use crate::almacen;
+use crate::baliza;
+use crate::drivers::red;
+
+// =============================================================================
+//  Cola del userspace — frames NO-AoE en espera de `sys_net_recibir`
+// =============================================================================
+
+/// Cuantos frames como mucho retenemos en la cola del userspace. Cota dura:
+/// si el userspace se duerme, los frames mas antiguos se pierden antes que
+/// la cola se desborde.
+const PROFUNDIDAD_COLA_USUARIO: usize = 64;
+
+/// La cola FIFO de frames que NO son AoE y aguardan a `sys_net_recibir`. Cada
+/// frame se copia tal cual lo entrega el driver; el userspace ve la cabecera
+/// Ethernet completa, como antes de la Fase 20.
+static COLA_USUARIO: Mutex<VecDeque<Vec<u8>>> = Mutex::new(VecDeque::new());
+
+// =============================================================================
+//  Contadores — la voz que la barra y el diagnostico leen
+// =============================================================================
+
+/// Frames AoE procesados en RX, por variante.
+static RX_SOLICITUDES: AtomicU64 = AtomicU64::new(0);
+static RX_PROVEEDORES: AtomicU64 = AtomicU64::new(0);
+static RX_ANUNCIOS: AtomicU64 = AtomicU64::new(0);
+
+/// Frames AoE emitidos en TX, por variante.
+static TX_SOLICITUDES: AtomicU64 = AtomicU64::new(0);
+static TX_PROVEEDORES: AtomicU64 = AtomicU64::new(0);
+static TX_ANUNCIOS: AtomicU64 = AtomicU64::new(0);
+
+/// Frames descartados en RX por ser basura (payload AoE invalido).
+static RX_DESCARTADOS: AtomicU64 = AtomicU64::new(0);
+
+/// Frames no-AoE encolados hacia el userspace.
+static USUARIO_ENCOLADOS: AtomicU64 = AtomicU64::new(0);
+/// Frames no-AoE descartados por desbordamiento de la cola del userspace.
+static USUARIO_DESBORDADOS: AtomicU64 = AtomicU64::new(0);
+
+/// La MAC con la que firmamos los frames AoE. La cachea `montar`.
+static NUESTRA_MAC: Once<Mac> = Once::new();
+
+// =============================================================================
+//  Montaje
+// =============================================================================
+
+/// Anota la MAC del dispositivo de red. La llama el orquestador cuando el
+/// driver entrega su `Mac` tras `red::montar`.
+pub fn montar(mac: Mac) {
+    NUESTRA_MAC.call_once(|| mac);
+}
+
+// =============================================================================
+//  Demultiplexor — el oficio numero 1 de la Fase 20
+// =============================================================================
+
+/// Drena la cola RX del dispositivo y demultiplexa cada frame:
+/// - Frames AoE (EtherType `0x88B5` con payload valido) → `procesar`.
+/// - Cualquier otro frame → cola del userspace (lo recogera `sys_net_recibir`).
+///
+/// Llamada en CADA fotograma desde la tarea cooperativa de red. El cerrojo
+/// del driver virtio-net se libera entre frame y frame; el del userspace solo
+/// se toma para empujar, sin solapar con el del driver.
+pub fn drenar_y_demultiplexar() {
+    let mac = match NUESTRA_MAC.get().copied() {
+        Some(m) => m,
+        None => return,
+    };
+    red::drenar_rx(|frame| {
+        // Intentar analizar como AoE. Si el EtherType cuadra Y el payload
+        // deserializa, procesamos en el kernel; el frame NO viaja al
+        // userspace —el protocolo es asunto del nucleo—.
+        match analizar_frame(frame) {
+            Ok((origen, mensaje)) => procesar(mensaje, origen, mac),
+            // EtherType ajeno o frame truncado: a la cola del userspace.
+            Err(ErrorAkasha::EtherTypeAjeno) | Err(ErrorAkasha::FrameDemasiadoCorto) => {
+                encolar_para_usuario(frame)
+            }
+            // EtherType nuestro pero payload no-postcard: el ejemplo canonico
+            // es el saludo en texto plano de `pregon`. NO es Akasha legitimo,
+            // pero tampoco basura del cable — es un protocolo userspace que
+            // comparte EtherType. Lo contamos y lo dejamos pasar al userspace
+            // para que el app destinatario (si lo hay) lo recoja.
+            Err(ErrorAkasha::PayloadInvalido) | Err(ErrorAkasha::PayloadDemasiadoLargo) => {
+                RX_DESCARTADOS.fetch_add(1, Ordering::Relaxed);
+                encolar_para_usuario(frame);
+            }
+        }
+    });
+}
+
+/// Encola un frame entero hacia el userspace. Si la cola esta llena, descarta
+/// el mas antiguo —preferimos perder el pasado a quedar sordos del futuro—.
+fn encolar_para_usuario(frame: &[u8]) {
+    let mut cola = COLA_USUARIO.lock();
+    if cola.len() >= PROFUNDIDAD_COLA_USUARIO {
+        cola.pop_front();
+        USUARIO_DESBORDADOS.fetch_add(1, Ordering::Relaxed);
+    }
+    cola.push_back(frame.to_vec());
+    USUARIO_ENCOLADOS.fetch_add(1, Ordering::Relaxed);
+}
+
+// =============================================================================
+//  Interfaz que ve `sys_net_recibir`
+// =============================================================================
+
+/// Saca UN frame de la cola del userspace hacia `buf`. Devuelve los bytes
+/// copiados (acotados por `buf.len()`), o `0` si no hay frame pendiente. El
+/// reemplazo natural de `red::recibir_en` para el lado WASM —ahora el
+/// userspace ya no compite con el kernel por la cola RX del dispositivo—.
+pub fn pop_usuario(buf: &mut [u8]) -> usize {
+    let frame = match COLA_USUARIO.lock().pop_front() {
+        Some(f) => f,
+        None => return 0,
+    };
+    let n = frame.len().min(buf.len());
+    buf[..n].copy_from_slice(&frame[..n]);
+    n
+}
+
+// =============================================================================
+//  Atencion de mensajes — el oficio numero 2
+// =============================================================================
+
+/// Procesa UN mensaje AoE entrante. Contabiliza, traza y —si toca— responde.
+fn procesar(mensaje: MensajeAkasha, origen: Mac, nuestra: Mac) {
+    match mensaje {
+        MensajeAkasha::SolicitarObjeto(id) => {
+            RX_SOLICITUDES.fetch_add(1, Ordering::Relaxed);
+            atender_solicitud(id, origen, nuestra);
+        }
+        MensajeAkasha::ProveedorObjeto(id, payload) => {
+            RX_PROVEEDORES.fetch_add(1, Ordering::Relaxed);
+            absorber_proveedor(id, &payload, origen);
+        }
+        MensajeAkasha::AnunciarRaiz(id) => {
+            RX_ANUNCIOS.fetch_add(1, Ordering::Relaxed);
+            atender_anuncio(id, origen, nuestra);
+        }
+    }
+}
+
+/// Si tenemos el objeto que nos piden, le respondemos al solicitante (unicast)
+/// con un `ProveedorObjeto` que carga la forma serializada del nodo. La
+/// integridad se preserva por construccion: `payload` es exactamente la
+/// secuencia que rehashea al `id` pedido. Si no lo tenemos, no decimos nada
+/// —AoE no tiene «not found»; un par puede preguntarle a otro—.
+fn atender_solicitud(id: Hash, origen: Mac, nuestra: Mac) {
+    let objeto = match almacen::recuperar(&id) {
+        Ok(Some(o)) => o,
+        Ok(None) => {
+            let _ = writeln!(
+                baliza::Serie,
+                "akasha :: solicitud rechazada (objeto ausente) :: {}",
+                FormatoHash(&id)
+            );
+            return;
+        }
+        Err(motivo) => {
+            let _ = writeln!(baliza::Serie, "akasha :: solicitud fallida :: {motivo}");
+            return;
+        }
+    };
+    let payload = match objeto.serializar() {
+        Ok(p) => p,
+        Err(_) => return,
+    };
+    // Defensa en profundidad: el rehash DEBE coincidir. postcard es canonico
+    // pero verificamos antes de poner algo en el cable que dice ser `id`.
+    if formato::hash(&payload) != id {
+        let _ = writeln!(
+            baliza::Serie,
+            "akasha :: rehash no coincide al servir :: descartado"
+        );
+        return;
+    }
+    let mensaje = MensajeAkasha::ProveedorObjeto(id, payload);
+    if enviar(&mensaje, nuestra, origen).is_ok() {
+        TX_PROVEEDORES.fetch_add(1, Ordering::Relaxed);
+        let _ = writeln!(
+            baliza::Serie,
+            "akasha :: PROVEEDOR enviado :: {} -> {}",
+            FormatoHash(&id),
+            FormatoMac(&origen)
+        );
+    }
+}
+
+/// Acepta un objeto venido del cable. Verifica que su forma serializada
+/// rehashea al `id` que el remitente afirma; si cuadra, lo deposita en el
+/// grafo local; si no, lo descarta con una traza. La unica entrada de
+/// integridad esta aqui: el grafo local no admite mentiras.
+fn absorber_proveedor(id: Hash, payload: &[u8], origen: Mac) {
+    if formato::hash(payload) != id {
+        let _ = writeln!(
+            baliza::Serie,
+            "akasha :: proveedor rechazado (rehash no coincide) :: src={}",
+            FormatoMac(&origen)
+        );
+        return;
+    }
+    let objeto = match formato::Objeto::deserializar(payload) {
+        Ok(o) => o,
+        Err(motivo) => {
+            let _ = writeln!(
+                baliza::Serie,
+                "akasha :: proveedor no deserializable :: {motivo}"
+            );
+            return;
+        }
+    };
+    match almacen::almacenar(objeto.datos, objeto.hijos) {
+        Ok(hash) => {
+            let _ = writeln!(
+                baliza::Serie,
+                "akasha :: PROVEEDOR absorbido :: {} <- {}",
+                FormatoHash(&hash),
+                FormatoMac(&origen)
+            );
+        }
+        Err(motivo) => {
+            let _ = writeln!(
+                baliza::Serie,
+                "akasha :: no se pudo absorber proveedor :: {motivo}"
+            );
+        }
+    }
+}
+
+/// Un par nos anuncio su raiz. Si la conocemos, no hay nada que hacer; si no,
+/// le pedimos el nodo: el `AnunciarRaiz` es el faro que basta para iniciar la
+/// replicacion.
+fn atender_anuncio(id: Hash, origen: Mac, nuestra: Mac) {
+    // ¿Lo tenemos ya? Entonces nada que pedir.
+    if let Ok(Some(_)) = almacen::recuperar(&id) {
+        let _ = writeln!(
+            baliza::Serie,
+            "akasha :: anuncio reconocido (ya lo tenemos) :: {} de {}",
+            FormatoHash(&id),
+            FormatoMac(&origen)
+        );
+        return;
+    }
+    // No lo tenemos — pedirlo al emisor en unicast.
+    let mensaje = MensajeAkasha::SolicitarObjeto(id);
+    if enviar(&mensaje, nuestra, origen).is_ok() {
+        TX_SOLICITUDES.fetch_add(1, Ordering::Relaxed);
+        let _ = writeln!(
+            baliza::Serie,
+            "akasha :: SOLICITUD enviada :: {} -> {}",
+            FormatoHash(&id),
+            FormatoMac(&origen)
+        );
+    }
+}
+
+// =============================================================================
+//  Difusion periodica de nuestra raiz — el oficio numero 3
+// =============================================================================
+
+/// Intervalo entre faros AoE consecutivos, en milisegundos. 5 s es un
+/// compromiso conservador: lo bastante frecuente para descubrir vecinos
+/// nuevos sin saturar la red de capa-2 con anuncios.
+const INTERVALO_FARO_MS: u64 = 5_000;
+
+/// Marca del reloj monotono (`reloj::milisegundos`) en la que se difundira el
+/// proximo faro. La inicializamos a `0` — la primera difusion ocurre cuanto
+/// antes pase `tic_compositor` con el manifiesto montado—.
+static PROXIMO_FARO_MS: AtomicU64 = AtomicU64::new(0);
+
+/// Punto de entrada que el tic del compositor llama una vez por fotograma.
+/// Junta los dos oficios AoE que viven en linea con el latido del escritorio:
+///   - drenar la cola RX del dispositivo y demultiplexar Akasha vs userspace,
+///   - difundir nuestra raiz cada `INTERVALO_FARO_MS` segun reloj monotono.
+///
+/// El compositor late de forma fiable (avanza el reloj de la barra cada
+/// segundo, prueba indirecta de que su tarea se atiende sin atascos); por
+/// eso elegimos su tic como portador. La cadencia del faro se mide contra
+/// el reloj monotono y NO contra los awaits, asi el ritmo del faro es
+/// independiente de cuanto trabajo del reactor consume cada vuelta.
+pub fn tic_compositor() {
+    drenar_y_demultiplexar();
+    let ahora = reloj::milisegundos();
+    let proximo = PROXIMO_FARO_MS.load(Ordering::Relaxed);
+    if ahora >= proximo {
+        difundir_raiz();
+        PROXIMO_FARO_MS.store(ahora + INTERVALO_FARO_MS, Ordering::Relaxed);
+    }
+}
+
+/// Anuncia, por broadcast, el hash del manifiesto actual. Es el faro de
+/// renaser: quien escuche en la red de capa-2 sabra de nuestra existencia y
+/// del nodo raiz del grafo. Si aun no hay manifiesto anclado, no se difunde
+/// nada — el silencio es preferible a un faro vacio.
+pub fn difundir_raiz() {
+    let nuestra = match NUESTRA_MAC.get().copied() {
+        Some(m) => m,
+        None => return,
+    };
+    let id = match almacen::manifiesto() {
+        Some(h) => h,
+        None => return,
+    };
+    let mensaje = MensajeAkasha::AnunciarRaiz(id);
+    if enviar(&mensaje, nuestra, MAC_BROADCAST).is_ok() {
+        TX_ANUNCIOS.fetch_add(1, Ordering::Relaxed);
+        let _ = writeln!(
+            baliza::Serie,
+            "akasha :: ANUNCIO emitido :: raiz={}",
+            FormatoHash(&id)
+        );
+    }
+}
+
+// =============================================================================
+//  Helpers internos
+// =============================================================================
+
+/// Compone un frame AoE y lo entrega al driver. Punto de salida unico de TX.
+fn enviar(mensaje: &MensajeAkasha, src: Mac, dst: Mac) -> Result<(), ()> {
+    let frame = componer_frame(src, dst, mensaje).map_err(|_| ())?;
+    red::enviar(&frame).map_err(|motivo| {
+        let _ = writeln!(baliza::Serie, "akasha :: envio fallido :: {motivo}");
+    })
+}
+
+// =============================================================================
+//  Lectores de estado — los expone la barra de tareas (futuro indicador) y el
+//  diagnostico de COM1
+// =============================================================================
+
+/// Resumen de actividad AoE, en una sola lectura coherente. Los enteros se
+/// leen `Relaxed`: el resumen es informativo, no transaccional.
+#[allow(dead_code)]
+#[derive(Clone, Copy, Debug)]
+pub struct ResumenAkasha {
+    pub rx_solicitudes: u64,
+    pub rx_proveedores: u64,
+    pub rx_anuncios: u64,
+    pub tx_solicitudes: u64,
+    pub tx_proveedores: u64,
+    pub tx_anuncios: u64,
+    pub rx_descartados: u64,
+    pub usuario_encolados: u64,
+    pub usuario_desbordados: u64,
+}
+
+#[allow(dead_code)]
+pub fn resumen() -> ResumenAkasha {
+    ResumenAkasha {
+        rx_solicitudes: RX_SOLICITUDES.load(Ordering::Relaxed),
+        rx_proveedores: RX_PROVEEDORES.load(Ordering::Relaxed),
+        rx_anuncios: RX_ANUNCIOS.load(Ordering::Relaxed),
+        tx_solicitudes: TX_SOLICITUDES.load(Ordering::Relaxed),
+        tx_proveedores: TX_PROVEEDORES.load(Ordering::Relaxed),
+        tx_anuncios: TX_ANUNCIOS.load(Ordering::Relaxed),
+        rx_descartados: RX_DESCARTADOS.load(Ordering::Relaxed),
+        usuario_encolados: USUARIO_ENCOLADOS.load(Ordering::Relaxed),
+        usuario_desbordados: USUARIO_DESBORDADOS.load(Ordering::Relaxed),
+    }
+}
+
+// =============================================================================
+//  Formateadores cortos para la traza de COM1 — sin allocations innecesarias
+// =============================================================================
+
+struct FormatoHash<'h>(&'h Hash);
+impl core::fmt::Display for FormatoHash<'_> {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        // Primeros 8 bytes en hex — suficiente para distinguir en una traza.
+        for b in &self.0[..8] {
+            write!(f, "{b:02x}")?;
+        }
+        write!(f, "..")
+    }
+}
+
+struct FormatoMac<'m>(&'m Mac);
+impl core::fmt::Display for FormatoMac<'_> {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        for (i, b) in self.0.iter().enumerate() {
+            if i > 0 {
+                write!(f, ":")?;
+            }
+            write!(f, "{b:02x}")?;
+        }
+        Ok(())
+    }
+}
diff --git a/renaser/kernel/src/drivers/red.rs b/renaser/kernel/src/drivers/red.rs
index 83bb051..5a7f4e6 100644
--- a/renaser/kernel/src/drivers/red.rs
+++ b/renaser/kernel/src/drivers/red.rs
@@ -217,7 +217,12 @@ pub fn drenar_rx<F: FnMut(&[u8])>(mut callback: F) {
 
 /// Recibe UN paquete: copia su contenido en `buf` y devuelve los bytes
 /// copiados, o `0` si no hay paquete pendiente. La interfaz que el host
-/// expone a los apps via `sys_net_recibir` — un paquete por llamada (Fase 19).
+/// expuso a los apps via `sys_net_recibir` en la Fase 19. Desde la Fase 20
+/// el kernel toma la cola RX para FILTRAR Akasha (`akasha::drenar_y_demultiplexar`
+/// + `akasha::pop_usuario`); `recibir_en` queda como primitiva del driver,
+/// disponible para futuros consumidores directos (p. ej., una herramienta de
+/// diagnostico que quiera leer la cola RX sin demultiplexar).
+#[allow(dead_code)]
 pub fn recibir_en(buf: &mut [u8]) -> usize {
     let Some(tarjeta) = TARJETA.get() else {
         return 0;
diff --git a/renaser/kernel/src/main.rs b/renaser/kernel/src/main.rs
index c730cdb..be6339d 100644
--- a/renaser/kernel/src/main.rs
+++ b/renaser/kernel/src/main.rs
@@ -45,6 +45,7 @@ use bootloader_api::{entry_point, BootInfo};
 use spin::{Mutex, Once};
 
 // --- Subsistemas del kernel ---
+mod akasha;
 mod almacen;
 mod async_system;
 mod baliza;
@@ -165,6 +166,10 @@ async fn tarea_compositor() {
         // FASE 16 :: avanzar el reloj de la barra de tareas — recompone si el
         // segundo cambio respecto al ultimo mostrado. Si no, vuelve enseguida.
         compositor::tick_reloj();
+        // FASE 20 :: pulso del oficio AoE — el demultiplexor de RX en cada
+        // tic (cero coste sin trafico), y el faro `AnunciarRaiz` cada 5 s
+        // (medido contra el reloj monotono, no contra los awaits).
+        akasha::tic_compositor();
         // FASE 10 :: atender las altas en vivo. Por cada `Alt+N` pendiente,
         // dar a luz una aplicacion nueva — el compositor solo conto la
         // peticion; instanciar el WASM es trabajo del orquestador.
@@ -174,16 +179,14 @@ async fn tarea_compositor() {
     }
 }
 
-/// FASE 18/19 — la primera voz del kernel hacia la red. Envia un ARP request
-/// al gateway de QEMU para anunciarse, y termina: a partir de la Fase 19, los
-/// apps drenan la cola RX por su cuenta via `sys_net_recibir`, asi que el
-/// kernel no le quita paquetes a nadie.
+/// FASE 18 — saludo inicial a la red. La tarea es CORTA y de un solo tiro:
+/// deja estabilizarse la cola RX del dispositivo y envia un ARP request al
+/// gateway de QEMU para anunciarse en capa-3. El demuxer Akasha (Fase 20)
+/// vive en el tic del compositor; el faro periodico, en `tarea_akasha_faro`.
 async fn tarea_red(mac: drivers::red::Mac) {
-    // Dejar un par de fotogramas para que la cola RX se estabilice.
     for _ in 0..10 {
         async_system::reloj::EsperaFrame::nueva().await;
     }
-    // Componer y enviar el ARP request: «¿quien tiene 10.0.2.2?».
     let frame = drivers::red::componer_arp_request(
         mac,
         drivers::red::IP_RENASER,
@@ -200,9 +203,14 @@ async fn tarea_red(mac: drivers::red::Mac) {
             let _ = writeln!(baliza::Serie, "red :: envio fallido :: {motivo}");
         }
     }
-    // La tarea termina aqui. Los apps se encargan del trafico desde ahora.
 }
 
+/// FASE 20 — el faro Akasha. Cada `INTERVALO_FARO` fotogramas (= 5 s a
+/// 100 Hz) difunde por broadcast `AnunciarRaiz(manifiesto)`. La primera
+/// difusion se demora unos pocos fotogramas para que el grafo termine de
+/// montarse — si `manifiesto()` aun es `None`, el envio es no-op y el
+/// siguiente faro lo reintentara. Tarea sin fin, con la misma forma que
+/// `tarea_compositor` (probada y latiente).
 /// FASE 6.2 — la prueba viva de la E/S asincrona. Esta tarea del reactor lee el
 /// sector 0 del disco SIN bloquear: cede la CPU mientras el disco trabaja —las
 /// apps siguen pintando entre tanto— y la IRQ del disco la reanuda cuando el
@@ -552,6 +560,9 @@ fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
                 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5],
                 drivers::red::irq(),
             );
+            // FASE 20 :: registrar la MAC en el respondedor Akasha, para que
+            // pueda firmar sus frames AoE.
+            akasha::montar(mac);
         }
         Err(motivo) => {
             let _ = writeln!(baliza::Serie, "red :: virtio-net :: {motivo}");
@@ -578,8 +589,9 @@ fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
     // disco de forma ASINCRONA: la demostracion de que la IRQ del disco
     // conduce la E/S sin detener a las aplicaciones visuales.
     ejecutor.spawn(tarea_sonda_disco());
-    // FASE 18 :: si la tarjeta de red se monto, una tarea le envia un ARP
-    // request al gateway y registra por COM1 los paquetes entrantes.
+    // FASE 18 :: si la tarjeta de red se monto, una tarea corta envia un ARP
+    // al gateway para anunciarse en capa-3. El oficio AoE (Fase 20) — demuxer
+    // de entrada + faro periodico — va clavado al tic del compositor.
     if let Ok(mac) = mac_red {
         ejecutor.spawn(tarea_red(mac));
     }
diff --git a/renaser/kernel/src/wasm/env.rs b/renaser/kernel/src/wasm/env.rs
index 66763d2..d6cee68 100644
--- a/renaser/kernel/src/wasm/env.rs
+++ b/renaser/kernel/src/wasm/env.rs
@@ -18,7 +18,9 @@
 //    * sys_tono              — hacer sonar la bocina del PC (Fase 12);
 //    * sys_net_mac           — leer la MAC de la tarjeta de red (Fase 19);
 //    * sys_net_enviar        — enviar un frame Ethernet crudo (Fase 19);
-//    * sys_net_recibir       — leer el siguiente frame recibido (Fase 19).
+//    * sys_net_recibir       — leer el siguiente frame recibido (Fase 19;
+//                              desde la Fase 20, los frames Akasha se filtran
+//                              en el kernel y no llegan al userspace).
 //
 //  GUARDARRAIL: el kernel valida MATEMATICAMENTE todo puntero que el modulo le
 //  entrega contra los limites reales de su memoria lineal. No se confia en que
@@ -530,10 +532,14 @@ pub(crate) fn enlazar_capacidades(
     )?;
 
     // --- CAPACIDAD 14 :: sys_net_recibir(salida, capacidad) -> i32 ---
-    // Saca el siguiente frame de la cola RX del dispositivo y lo copia en
-    // `salida`. Devuelve los bytes copiados (>0), 0 si no hay frame pendiente,
-    // o -1 si no hay red montada. La cola RX es del dispositivo y se comparte
-    // entre los apps: el primero que pregunte se lleva el paquete.
+    // Saca el siguiente frame de la cola del USUARIO y lo copia en `salida`.
+    // Desde la Fase 20, esa cola la rellena el demultiplexor del kernel
+    // (`akasha::drenar_y_demultiplexar`): los frames Akasha (`0x88B5` con
+    // payload valido) se procesan en el nucleo y NO llegan aqui; el resto
+    // del trafico —ARP, IPv4 de QEMU, futuros protocolos— si. Devuelve los
+    // bytes copiados (>0), 0 si no hay frame pendiente, o -1 si no hay red
+    // montada. La cola se vacia FIFO; si un app no llama nunca, los frames
+    // mas antiguos se descartan al desbordar (ver `akasha::COLA_USUARIO`).
     enlazador.func_wrap(
         "renaser",
         "sys_net_recibir",
@@ -555,10 +561,10 @@ pub(crate) fn enlazar_capacidades(
                     "WASM :: sys_net_recibir desbordo la memoria lineal",
                 )?;
             }
-            // Bufer kernel-side donde el driver vuelca el frame; luego se copia
-            // a la memoria del app en una sola pasada.
+            // Bufer kernel-side donde la cola del usuario vuelca el frame; luego
+            // se copia a la memoria del app en una sola pasada.
             let mut buf: alloc::vec::Vec<u8> = alloc::vec![0u8; capacidad as usize];
-            let n = crate::drivers::red::recibir_en(&mut buf);
+            let n = crate::akasha::pop_usuario(&mut buf);
             if n == 0 {
                 return Ok(0);
             }