feat: mirada standalone — compositor Wayland + WM sobre Llimphi (build magro)
1use super::*;
2
3// =============================================================================
4// El hash y el trazado de un registro en el log
5// =============================================================================
6
7/// La identidad de un objeto: el hash BLAKE3 de su forma serializada. Kernel y
8/// `boot` la calculan por aqui — una sola definicion del hash, jamas dos.
9pub fn hash(bytes: &[u8]) -> Hash {
10 *blake3::hash(bytes).as_bytes()
11}
12
13/// Numero de sectores que ocupa un registro cuyo payload mide `longitud`
14/// bytes. Cada registro es `[longitud: u32 LE][payload postcard][relleno 0]`.
15pub fn sectores_registro(longitud: usize) -> u64 {
16 (4 + longitud).div_ceil(TAM_SECTOR) as u64
17}
18
19/// Compone el registro en disco de un payload: `[longitud u32 LE][payload]
20/// [relleno a cero]`, alineado a un numero entero de sectores. Es el trazado
21/// exacto que el kernel lee al reconstruir su indice — lo escriben tanto
22/// `kernel::almacen` (al anexar un objeto) como `boot` (al sembrar la imagen).
23pub fn componer_registro(payload: &[u8]) -> Vec<u8> {
24 let n = sectores_registro(payload.len()) as usize;
25 let mut registro = vec![0u8; n * TAM_SECTOR];
26 registro[0..4].copy_from_slice(&(payload.len() as u32).to_le_bytes());
27 registro[4..4 + payload.len()].copy_from_slice(payload);
28 registro
29}
30
31/// Lee la cabecera de longitud de un registro (sus 4 primeros bytes). Devuelve
32/// `None` si la longitud es cero —fin del log— o supera [`MAX_OBJETO`]
33/// —corrupcion—. Gemela de [`componer_registro`].
34pub fn longitud_registro(cabecera: &[u8]) -> Option<usize> {
35 if cabecera.len() < 4 {
36 return None;
37 }
38 let longitud =
39 u32::from_le_bytes([cabecera[0], cabecera[1], cabecera[2], cabecera[3]]) as usize;
40 if longitud == 0 || longitud > MAX_OBJETO {
41 None
42 } else {
43 Some(longitud)
44 }
45}
46
47// =============================================================================
48// Fase 60 — Asistente Akasha: tipos de mensaje del canal del asistente
49// -----------------------------------------------------------------------------
50// La app `asistente.wasm` (kernel-side) y el `asistente-puente` (host-side)
51// conversan por un canal Akasha bien conocido. Estos tipos definen el
52// protocolo. Diseñado para serializarse con `postcard` (el mismo encoder
53// que usa todo el resto del kernel) y vivir en `#![no_std] + alloc` para
54// cruzar la frontera kernel-wasm sin friction.
55//
56// ESTADO (Fase 60 v1): tipos definidos, sin código que los consuma todavía.
57// Ver `docs/ASISTENTE_WAWA.md` §2.2 para el contexto del diseño.
58// =============================================================================
59
60/// Canal Akasha bien conocido para el asistente. ASCII `"AS"` = 0x4153. El
61/// kernel filtra paquetes con este canal hacia los suscriptores del oficio
62/// asistente; el puente Linux abre un socket raw que suscribe al mismo
63/// número para recibir consultas y enviar propuestas.
64///
65/// NOTA: 0x4153 está dentro del rango histórico de "longitud" de Ethernet
66/// (< 0x0600), así que NO sirve como EtherType. Para los frames del
67/// asistente sobre el cable se usa [`ETHERTYPE_ASISTENTE`]; este valor
68/// queda como discriminante interno (postcard tag, identificador del
69/// oficio en logs y trazas).
70pub const CANAL_ASISTENTE: u16 = 0x4153;
71
72/// EtherType de los frames del asistente sobre el cable. Vecino del
73/// 0x88B5 que ya usa Akasha — los dos viven en el rango "experimental"
74/// que la IEEE deja libre. El demuxer Akasha del kernel (`akasha.rs`)
75/// trata frames con EtherType ajeno como "para el usuario": los encola
76/// tal cual sin procesar. La app `asistente.wasm` los recoge con
77/// `sys_net_recibir`, filtra por este EtherType y decodifica el payload
78/// como [`MensajeAsistente`] postcard.
79///
80/// Mantenerlo distinto de 0x88B5 evita que el demuxer intente
81/// deserializar el payload como `MensajeAkasha` y lo descarte como
82/// `PayloadInvalido` antes de pasarlo al usuario.
83pub const ETHERTYPE_ASISTENTE: u16 = 0x88B6;
84
85/// Acción que el LLM (vía el puente) propone al asistente. La app pinta
86/// la propuesta, el humano decide. Acciones potentes (re-anclar manifiesto,
87/// cambiar configuración) referencian objetos del grafo por `Hash` — el
88/// puente los preparó y los ingestó vía Akasha; el kernel los verifica al
89/// aplicar.
90#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
91pub enum AccionPropuesta {
92 /// Lanzar la app `plantilla`-ésima del manifiesto. Equivalente al
93 /// `Mando::LanzarFila` del launcher, pero dirigido por LLM.
94 LanzarApp { plantilla: u32 },
95 /// Re-anclar el manifiesto al hash propuesto. Requiere firma humana
96 /// vía `daemon-firma` antes de invocar `sys_manifiesto_proponer`.
97 InstalarApp { manifiesto_propuesto: Hash },
98 /// Cambiar la `Configuracion` activa al hash propuesto. Mismo flujo
99 /// de firma humana que `InstalarApp`.
100 CambiarConfiguracion { config_propuesta: Hash },
101 /// Sin efecto sobre el sistema — el LLM nada más anota algo para que
102 /// el humano lo lea. Útil para responder preguntas tipo "¿cuántas
103 /// apps tengo?" sin disparar acciones.
104 Notar { texto: String },
105}
106
107/// Contexto del estado actual del nodo wawa que la app envía al puente
108/// junto con la consulta. Permite que el LLM responda con info concreta
109/// (nombres de apps reales, configuración activa) en lugar de a ciegas.
110/// Lo que se incluye está acotado deliberadamente — más campos = más
111/// tokens en el system prompt = más coste.
112#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, Default)]
113pub struct Contexto {
114 /// Nombres de las apps del manifiesto vivo, en el orden del catálogo
115 /// del launcher. El LLM puede usar `LanzarApp { plantilla: i }` con
116 /// el índice de la fila correspondiente.
117 pub apps: Vec<String>,
118 /// Hash del manifiesto vigente. Permite que el puente detecte si su
119 /// caché local quedó stale (otro nodo re-ancló en paralelo) y
120 /// rerequiera contexto fresco.
121 pub manifiesto_actual: Option<Hash>,
122 /// Hash de la `Configuracion` activa, si la hay. `None` si el
123 /// manifiesto no enlaza ninguna.
124 pub configuracion_activa: Option<Hash>,
125}
126
127/// Un mensaje sobre el canal `CANAL_ASISTENTE`. La app y el puente
128/// hablan exclusivamente este enum — un atacante que envíe payload ajeno
129/// al canal se queda sin decodificar (postcard rechaza el frame).
130#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
131pub enum MensajeAsistente {
132 /// La app pregunta. El puente lo retransmite al LLM. `id` correlaciona
133 /// request/response — un puente sirviendo varios nodos los distingue
134 /// por id ANTES de cualquier RTT extra.
135 Consulta {
136 id: u64,
137 prompt: String,
138 contexto: Contexto,
139 },
140 /// El puente responde con una propuesta interpretada del LLM.
141 /// `confianza` es la decisión del puente — `1.0` si el LLM produjo
142 /// JSON limpio y la acción está en la lista blanca; valores menores
143 /// si tuvo que adivinar o si el parseo fue parcial.
144 Propuesta {
145 id: u64,
146 accion: AccionPropuesta,
147 explicacion: String,
148 confianza: f32,
149 },
150 /// El puente reporta un fallo de transporte o parseo. El `id`
151 /// correlaciona contra la consulta original; el `motivo` es un string
152 /// libre que la app pinta al humano.
153 Error { id: u64, motivo: String },
154}
155
156impl MensajeAsistente {
157 /// Serializa con postcard. El kernel lo manda por Akasha; el puente
158 /// lo recibe y deserializa.
159 pub fn serializar(&self) -> Result<Vec<u8>, postcard::Error> {
160 postcard::to_allocvec(self)
161 }
162
163 /// Deserializa desde bytes. Si el frame está truncado o el canal
164 /// trajo basura ajena, devuelve error sin tocar `self`.
165 pub fn deserializar(bytes: &[u8]) -> Result<Self, postcard::Error> {
166 postcard::from_bytes(bytes)
167 }
168}
169
170// =============================================================================
171// Protocolo "cable" del asistente — alfabeto minimo sin alloc
172// -----------------------------------------------------------------------------
173// `MensajeAsistente` (arriba) usa `String` y `Vec` para empaquetar prompts
174// y explicaciones de longitud arbitraria. La app `asistente.wasm` corre en
175// no_std SIN alloc — no puede construir esos tipos. Para el cable definimos
176// un alfabeto minimo que cabe en arrays fijos: cabecera de 12 bytes
177// (canal + tipo + id) + payload de longitud inferida del frame.
178//
179// El puente Linux traduce entre el rico `MensajeAsistente` (que usa para
180// hablar con pluma-llm) y este protocolo cable (que viaja por Akasha).
181// =============================================================================
182
183/// Tamaño en bytes de la cabecera del protocolo cable.
184/// `canal (2) + tipo (2) + id (8) = 12`.
185pub const TAM_CABECERA_CABLE: usize = 12;
186
187/// Tipos de mensaje sobre el cable del asistente. Discriminante u16 big
188/// endian estable — los lectores binarios pueden grep por estos valores.
189#[repr(u16)]
190#[derive(Clone, Copy, PartialEq, Eq, Debug)]
191pub enum TipoCable {
192 /// Consulta de la app al puente. Payload = bytes ASCII del prompt
193 /// (sin nul terminator — la longitud se infiere del frame).
194 Consulta = 1,
195 /// Propuesta del puente del tipo `Notar` (la IA contestó algo
196 /// informativo). Payload = bytes ASCII del texto.
197 PropuestaNotar = 2,
198 /// Propuesta del puente del tipo `LanzarApp`. Payload = u32 BE con
199 /// el índice de plantilla a lanzar (4 bytes).
200 PropuestaLanzarApp = 3,
201 /// Propuesta de re-anclar el manifiesto. Payload = 32 bytes del hash.
202 PropuestaInstalarApp = 4,
203 /// Propuesta de cambiar la configuración activa. Payload = 32 bytes
204 /// del hash de la nueva configuración.
205 PropuestaCambiarConfig = 5,
206 /// Error reportado por el puente (transporte, rechazo del LLM,
207 /// parseo). Payload = bytes ASCII del motivo.
208 Error = 6,
209 /// Fase 60 v4 :: la app `asistente.wasm` pide la firma humana de un
210 /// objeto (manifiesto/configuración). El puente lo relaya al
211 /// `wawactl daemon-firma` por su transporte normal (PTY/virtio-console)
212 /// y devuelve la firma en un [`TipoCable::Firma`]. Payload:
213 /// `[tipo_obj: u8, hash: [u8; 32]]` = 33 bytes.
214 /// - `tipo_obj` = [`TIPO_OBJETO_CUADERNO`] (1) si el hash es de
215 /// manifiesto/cuaderno (legacy `wawa::sign_request::`).
216 /// - `tipo_obj` = [`TIPO_OBJETO_CONFIGURACION`] (2) si es de
217 /// configuración (`wawa::sign_config::`).
218 /// Otros valores son rechazados por el puente con un `TipoCable::Error`.
219 RequestFirma = 7,
220 /// Fase 60 v4 :: respuesta del puente con la firma humana ya
221 /// autorizada por el operador (via `daemon-firma`). Payload:
222 /// `[slot: u8, firma: [u8; 64]]` = 65 bytes. `slot` es 0/1/2 — el
223 /// índice dentro de `AGORA_AUTH_RING` que el operador eligió al
224 /// arrancar el demonio. El asistente.wasm construye el sobre
225 /// firmado y, cuando tenga PERMISO_RAIZ (hito 6), invoca
226 /// `sys_manifiesto_proponer`.
227 Firma = 8,
228}
229
230/// FASE 60 v4 :: discriminantes del primer byte del payload de
231/// `TipoCable::RequestFirma`. El puente los mapea al prefijo correcto
232/// para `daemon-firma` (`wawa::sign_request::` vs `wawa::sign_config::`).
233/// El mismo discriminante puede aparecer en logs del operador.
234pub const TIPO_OBJETO_CUADERNO: u8 = 1;
235/// Como [`TIPO_OBJETO_CUADERNO`] pero para configuraciones. Ver Fase 60 v2
236/// del `wawactl daemon-firma` — el prefijo correspondiente es
237/// `wawa::sign_config::`.
238pub const TIPO_OBJETO_CONFIGURACION: u8 = 2;
239
240impl TipoCable {
241 /// Traduce un u16 al variant correspondiente o `None` si es
242 /// desconocido (el cable trajo un tipo no registrado).
243 pub fn de_u16(v: u16) -> Option<Self> {
244 match v {
245 1 => Some(Self::Consulta),
246 2 => Some(Self::PropuestaNotar),
247 3 => Some(Self::PropuestaLanzarApp),
248 4 => Some(Self::PropuestaInstalarApp),
249 5 => Some(Self::PropuestaCambiarConfig),
250 6 => Some(Self::Error),
251 7 => Some(Self::RequestFirma),
252 8 => Some(Self::Firma),
253 _ => None,
254 }
255 }
256}
257
258/// Escribe la cabecera del cable en `out`. Devuelve la longitud escrita
259/// (siempre `TAM_CABECERA_CABLE`) o `None` si `out` no cabe — el caller
260/// reserva el buffer apropiado.
261pub fn escribir_cabecera_cable(out: &mut [u8], tipo: TipoCable, id: u64) -> Option<usize> {
262 if out.len() < TAM_CABECERA_CABLE {
263 return None;
264 }
265 out[0..2].copy_from_slice(&CANAL_ASISTENTE.to_be_bytes());
266 out[2..4].copy_from_slice(&(tipo as u16).to_be_bytes());
267 out[4..12].copy_from_slice(&id.to_be_bytes());
268 Some(TAM_CABECERA_CABLE)
269}
270
271/// Lee la cabecera del cable y verifica que el canal sea el del
272/// asistente. Devuelve `(tipo, id)` o `None` si los bytes son
273/// insuficientes, el canal no coincide o el tipo es desconocido. El
274/// llamante interpreta `&bytes[TAM_CABECERA_CABLE..]` según `tipo`.
275pub fn leer_cabecera_cable(bytes: &[u8]) -> Option<(TipoCable, u64)> {
276 if bytes.len() < TAM_CABECERA_CABLE {
277 return None;
278 }
279 let canal = u16::from_be_bytes([bytes[0], bytes[1]]);
280 if canal != CANAL_ASISTENTE {
281 return None;
282 }
283 let tipo_raw = u16::from_be_bytes([bytes[2], bytes[3]]);
284 let tipo = TipoCable::de_u16(tipo_raw)?;
285 let id = u64::from_be_bytes([
286 bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], bytes[10], bytes[11],
287 ]);
288 Some((tipo, id))
289}
290