feat: mirada standalone — compositor Wayland + WM sobre Llimphi (build magro)
1use super::*;
2
3// =============================================================================
4// Fase 33/43 :: el almacen semantico del cuaderno (modelo unificado)
5// -----------------------------------------------------------------------------
6// Un CUADERNO de Wawa es un nodo del grafo cuyo payload `postcard` es un
7// `Vec<CeldaWawa>`. Cada `CeldaWawa` empaqueta TODA la informacion de un
8// eslabon del calculo en una sola estructura inmutable:
9//
10// * `id_secuencial` :: indice lineal en el cuaderno.
11// * `fuente_hash` :: hash del texto Forth o token `@<hash>` literal.
12// * `binario_hash` :: hash del modulo WASM materializado (None si
13// la compilacion fallo).
14// * `ultimo_retorno` :: el i32 que la sub-jaula efimera devolvio
15// (None si nunca se ejecuto).
16// * `marca_error` :: bandera atomica: hubo TRAP, OUT_OF_FUEL,
17// PAYLOAD_INVALIDO, o cualquier otra falla.
18//
19// La fusion (Fase 43) elimina el enum heredado `TipoCeldaWawa` con sus
20// tres variantes flat — el modelo estructurado es mas honesto con la
21// semantica del cuaderno y converge bit-a-bit con la representacion
22// del motor Linux del ecosistema Pluma (`pluma-notebook-core`), que
23// re-exporta esta misma struct para hablar el mismo idioma en host y
24// en el silicio.
25//
26// Las aristas del nodo (los `hijos` que el almacen registra al insertar)
27// son: el CUADERNO PREVIO cuando existe (arista ancestral, Fase 47),
28// `fuente_hash` siempre, `binario_hash` cuando esta presente. El
29// direccionamiento por contenido hace EXPLICITAS las dependencias y
30// el cuaderno arrastra criptograficamente todo su tejido de causas y
31// efectos. Con la Fase 47, cada cuaderno apunta a su predecesor por
32// hash — el historial es una cadena recorrible por el Walker.
33//
34// Postcard-amigable: campos primitivos + `Option<T>` + arrays alineados.
35// La deserializacion del cuaderno no allocea fuera del `Vec` principal.
36// =============================================================================
37
38/// El eslabon canonico de un cuaderno (Fase 43). Reemplaza al enum
39/// `TipoCeldaWawa` de la Fase 33: en lugar de tres variantes flat que
40/// solo el orden del `Vec` ataba a una "celda", aqui CADA `CeldaWawa`
41/// es una celda completa con todos sus eslabones bundled. Bit-compatible
42/// con `pluma_notebook_core::CeldaWawa` (re-export en el motor Linux).
43#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
44pub struct CeldaWawa {
45 /// Indice lineal en el cuaderno — orden de presentacion. Empieza
46 /// en 0 y crece con cada celda exitosamente registrada.
47 pub id_secuencial: u32,
48 /// Hash del texto fuente: ASCII Forth tecleado por el humano, o
49 /// la cadena literal `@<64-hex>` para celdas macro-importadas
50 /// (Fase 36, Cross-App Bridge). Siempre presente — una celda sin
51 /// fuente es incoherente con el modelo.
52 pub fuente_hash: Hash,
53 /// Hash del modulo WASM materializado por `forth-emisor` (o
54 /// importado del grafo via `@<hash>`). `None` cuando la compilacion
55 /// fallo, la sintaxis Forth fue rechazada, o la vinculacion macro
56 /// no se logro — el binario no llego a inscribirse.
57 pub binario_hash: Option<Hash>,
58 /// El i32 que la sub-jaula efimera (Fase 32) devolvio en su ultima
59 /// ejecucion. `None` cuando la celda nunca corrio (sin binario, o
60 /// el despacho dinamico ni siquiera arranco). Un valor negativo
61 /// en `[-7, -1]` reservado en `CodigoError` codifica fallas
62 /// controladas; valores fuera de ese rango son resultados legitimos.
63 pub ultimo_retorno: Option<i32>,
64 /// Bandera atomica de error: `true` si CUALQUIER eslabon de la
65 /// cadena (compilacion, registro v2, ejecucion dinamica, anclaje
66 /// de cuaderno) devolvio fallo. El renderer la usa para teñir la
67 /// celda de amarillo palido sin enterrar el valor del retorno —
68 /// `marca_error && ultimo_retorno == Some(-7)` significa
69 /// "ejecutada, fallida con trap"; `marca_error && ultimo_retorno
70 /// == None` significa "ni siquiera corrio".
71 pub marca_error: bool,
72}
73
74/// Serializa una secuencia de celdas a `postcard` — la forma que el
75/// kernel inscribe como payload del nodo cuaderno. Centralizada aqui
76/// para que el kernel no tenga que declarar `postcard` directamente
77/// (ya lo hereda transitivamente via `format`).
78pub fn serializar_celdas(celdas: &[CeldaWawa]) -> Result<Vec<u8>, &'static str> {
79 postcard::to_allocvec(celdas).map_err(|_| "celdas :: serializacion fallida")
80}
81
82/// Reconstruye la secuencia de celdas desde el payload de un nodo cuaderno.
83/// Tolera bytes sobrantes — el relleno del registro vive despues del payload.
84pub fn deserializar_celdas(bytes: &[u8]) -> Result<Vec<CeldaWawa>, &'static str> {
85 postcard::take_from_bytes::<Vec<CeldaWawa>>(bytes)
86 .map(|(celdas, _)| celdas)
87 .map_err(|_| "celdas :: deserializacion fallida")
88}
89
90// =============================================================================
91// (De)serializacion — la forma binaria que viaja al disco
92// =============================================================================
93
94impl Objeto {
95 /// Serializa el objeto a su forma binaria `postcard`.
96 pub fn serializar(&self) -> Result<Vec<u8>, &'static str> {
97 postcard::to_allocvec(self).map_err(|_| "objeto :: serializacion fallida")
98 }
99
100 /// Reconstruye un objeto desde su forma binaria. Tolera bytes sobrantes
101 /// tras el objeto —el relleno del registro—: solo consume su prefijo.
102 pub fn deserializar(bytes: &[u8]) -> Result<Objeto, &'static str> {
103 postcard::take_from_bytes::<Objeto>(bytes)
104 .map(|(objeto, _)| objeto)
105 .map_err(|_| "objeto :: deserializacion fallida")
106 }
107}
108
109// =============================================================================
110// Fase 66 :: Árbol/Blob — el monorepo como grafo
111// -----------------------------------------------------------------------------
112// El grafo direccionado por contenido ES el modelo de objetos de git. Esta
113// capa lo hace explícito para que un árbol de directorios viva en el grafo:
114//
115// * BLOB :: el contenido de un archivo. Es un `Objeto { datos: bytes,
116// hijos: [] }` — sin estructura, solo bytes direccionados por
117// su hash. Archivos idénticos comparten un solo blob (dedup
118// por contenido, gratis).
119// * ÁRBOL :: el contenido de un directorio. Un `Objeto` cuyo `datos` es
120// un `Arbol` postcard (la lista de entradas: nombre + modo +
121// hash) y cuyos `hijos` son los hashes de esas entradas — así
122// el MARK del GC del kernel alcanza todo el subárbol siguiendo
123// `hijos`, SIN tener que entender el format `Arbol`.
124//
125// Las entradas de un árbol van ORDENADAS por nombre: mismo contenido de
126// directorio => mismo árbol serializado => mismo hash. Determinismo total, la
127// base de la dedup y de la verificación. Un repositorio entero colapsa a UN
128// hash raíz; dos commits que solo tocan un archivo comparten todo el resto del
129// árbol (estructura compartida, como git).
130// =============================================================================
131
132/// Version del format de un `Arbol`.
133pub const VERSION_ARBOL: u32 = 1;
134
135/// Qué clase de objeto referencia una entrada de árbol. Espeja los modos de
136/// git (archivo / archivo+x / symlink / directorio). Variantes AÑADIDAS AL
137/// FINAL: los tags `postcard` se asignan por orden y mover una romperia árboles
138/// ya serializados.
139#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
140pub enum ModoEntrada {
141 /// El hash apunta a un archivo regular (blob plano o índice de trozos).
142 Archivo,
143 /// El hash apunta a otro ÁRBOL (subdirectorio).
144 Directorio,
145 /// Como `Archivo` pero con bit de ejecución (un script, un binario).
146 Ejecutable,
147 /// El hash apunta a un blob cuyo contenido es el DESTINO del enlace
148 /// simbólico (la ruta a la que apunta), en UTF-8.
149 Symlink,
150}
151
152impl ModoEntrada {
153 /// `true` si el modo referencia CONTENIDO de archivo (blob/índice): un
154 /// archivo regular o un ejecutable. `Symlink` y `Directorio` no.
155 pub fn es_archivo(&self) -> bool {
156 matches!(self, ModoEntrada::Archivo | ModoEntrada::Ejecutable)
157 }
158}
159
160/// Una entrada de un árbol: un nombre dentro del directorio + el modo + el hash
161/// del objeto que la realiza (un blob si `Archivo`, un árbol si `Directorio`).
162#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
163pub struct EntradaArbol {
164 /// Nombre del archivo/subdirectorio (sin separadores de ruta).
165 pub nombre: String,
166 /// Si el hash apunta a un blob o a un subárbol.
167 pub modo: ModoEntrada,
168 /// Hash del objeto (blob o árbol) que esta entrada referencia.
169 pub hash: Hash,
170}
171
172/// Un árbol: el contenido de un directorio, como lista ordenada de entradas.
173#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
174pub struct Arbol {
175 /// Version del format — debe ser [`VERSION_ARBOL`].
176 pub version: u32,
177 /// Entradas ORDENADAS por nombre (invariante que `objeto_arbol` impone).
178 pub entradas: Vec<EntradaArbol>,
179}
180
181impl Arbol {
182 /// Serializa el árbol a su forma `postcard` —la carga útil del objeto que
183 /// lo aloja—.
184 pub fn serializar(&self) -> Result<Vec<u8>, &'static str> {
185 postcard::to_allocvec(self).map_err(|_| "arbol :: serializacion fallida")
186 }
187
188 /// Reconstruye un árbol desde la carga útil de su objeto. Rechaza una
189 /// version desconocida en lugar de malinterpretarla.
190 pub fn deserializar(bytes: &[u8]) -> Result<Arbol, &'static str> {
191 let (arbol, _) =
192 postcard::take_from_bytes::<Arbol>(bytes).map_err(|_| "arbol :: deserializacion fallida")?;
193 if arbol.version != VERSION_ARBOL {
194 return Err("arbol :: version de format desconocida");
195 }
196 Ok(arbol)
197 }
198}
199
200/// Construye el objeto BLOB de un archivo: bytes crudos, sin hijos. El hash de
201/// este objeto (sobre su forma serializada) es la identidad del archivo en el
202/// grafo. Dos archivos con idéntico contenido producen el MISMO blob.
203pub fn objeto_blob(datos: Vec<u8>) -> Objeto {
204 Objeto {
205 datos,
206 hijos: Vec::new(),
207 }
208}
209
210/// Construye el objeto ÍNDICE de un archivo GRANDE partido en trozos: `datos`
211/// VACÍO, `hijos` = los hashes de los blobs-trozo EN ORDEN. La convención de
212/// lectura: una entrada de archivo (`Archivo`/`Ejecutable`) cuyo objeto tiene
213/// `hijos` no vacío es un índice, y el contenido del archivo es la
214/// concatenación de los `datos` de sus trozos; si `hijos` está vacío, el
215/// objeto ES el contenido (blob plano). Así un archivo de cualquier tamaño se
216/// referencia igual desde el árbol — el lector decide plano vs índice por la
217/// forma del objeto, sin un modo aparte. Un archivo vacío es un blob plano
218/// (`datos` vacío, `hijos` vacío), nunca un índice.
219pub fn objeto_blob_indice(hijos: Vec<Hash>) -> Objeto {
220 Objeto {
221 datos: Vec::new(),
222 hijos,
223 }
224}
225
226/// Construye el objeto ÁRBOL de un directorio a partir de sus entradas. ORDENA
227/// las entradas por nombre (determinismo: mismo directorio → mismo hash) y fija
228/// `hijos` con los hashes de las entradas, en el MISMO orden, para que el GC
229/// alcance el subárbol siguiendo `hijos` sin parsear el `Arbol`.
230pub fn objeto_arbol(mut entradas: Vec<EntradaArbol>) -> Result<Objeto, &'static str> {
231 entradas.sort_by(|a, b| a.nombre.cmp(&b.nombre));
232 let hijos: Vec<Hash> = entradas.iter().map(|e| e.hash).collect();
233 let arbol = Arbol {
234 version: VERSION_ARBOL,
235 entradas,
236 };
237 let datos = arbol.serializar()?;
238 Ok(Objeto { datos, hijos })
239}
240
241impl SuperBloque {
242 /// Serializa el superbloque a su forma binaria `postcard`.
243 pub fn serializar(&self) -> Result<Vec<u8>, &'static str> {
244 postcard::to_allocvec(self).map_err(|_| "superbloque :: serializacion fallida")
245 }
246
247 /// Reconstruye el superbloque desde el sector 0. Tolera el relleno a cero
248 /// que completa el sector: solo consume el prefijo serializado.
249 pub fn deserializar(bytes: &[u8]) -> Result<SuperBloque, &'static str> {
250 postcard::take_from_bytes::<SuperBloque>(bytes)
251 .map(|(sb, _)| sb)
252 .map_err(|_| "superbloque :: deserializacion fallida")
253 }
254}
255
256impl Manifiesto {
257 /// Serializa el manifiesto a su forma binaria `postcard` — la carga util
258 /// del objeto del grafo que lo aloja.
259 pub fn serializar(&self) -> Result<Vec<u8>, &'static str> {
260 postcard::to_allocvec(self).map_err(|_| "manifiesto :: serializacion fallida")
261 }
262
263 /// Reconstruye un manifiesto desde la carga util de su objeto. Rechaza un
264 /// format de version desconocida en lugar de malinterpretarlo.
265 pub fn deserializar(bytes: &[u8]) -> Result<Manifiesto, &'static str> {
266 let (manifiesto, _) = postcard::take_from_bytes::<Manifiesto>(bytes)
267 .map_err(|_| "manifiesto :: deserializacion fallida")?;
268 if manifiesto.version != VERSION_MANIFIESTO {
269 return Err("manifiesto :: version de format desconocida");
270 }
271 Ok(manifiesto)
272 }
273}
274
275impl Canal {
276 /// Serializa el canal a su forma binaria `postcard` — la carga util del
277 /// objeto del grafo que lo aloja. Rechaza por adelantado un nombre que
278 /// supere [`NOMBRE_CANAL_LIMITE`]: mejor un error de serializacion que un
279 /// canal grafico que no quepa en disco.
280 pub fn serializar(&self) -> Result<Vec<u8>, &'static str> {
281 if self.nombre.len() > NOMBRE_CANAL_LIMITE {
282 return Err("canal :: nombre demasiado largo");
283 }
284 postcard::to_allocvec(self).map_err(|_| "canal :: serializacion fallida")
285 }
286
287 /// Reconstruye un canal desde la carga util de su objeto. Rechaza version
288 /// desconocida y nombres que excedan [`NOMBRE_CANAL_LIMITE`] —un canal con
289 /// nombre extravagante se detecta al recibirlo, no al servirlo—.
290 pub fn deserializar(bytes: &[u8]) -> Result<Canal, &'static str> {
291 let (canal, _) = postcard::take_from_bytes::<Canal>(bytes)
292 .map_err(|_| "canal :: deserializacion fallida")?;
293 if canal.version != VERSION_CANAL {
294 return Err("canal :: version de format desconocida");
295 }
296 if canal.nombre.len() > NOMBRE_CANAL_LIMITE {
297 return Err("canal :: nombre excede el techo");
298 }
299 Ok(canal)
300 }
301
302 /// La recomendacion vigente del canal: la ultima `RaizFirmada` por
303 /// `timestamp`, o `None` si el canal aun no propuso ninguna. Quien quiera
304 /// "actualizar" sigue este hash; quien quiera rollback elige otra entrada
305 /// del historial.
306 pub fn vigente(&self) -> Option<&RaizFirmada> {
307 self.raices.last()
308 }
309}
310
311/// Compone el mensaje canonico que un autor firma para respaldar una raiz en
312/// un canal: la concatenacion `nombre || timestamp_le || raiz_manifiesto`.
313/// Es la unica verdad del payload firmable —quien firma y quien verifica han
314/// de componerlo por aqui, jamas a mano—. La canonizacion incluye el nombre
315/// del canal para que una firma valida en `dev` no se replique en `estable`.
316pub fn mensaje_a_firmar(nombre_canal: &str, timestamp: u64, raiz_manifiesto: &Hash) -> Vec<u8> {
317 let mut mensaje = Vec::with_capacity(nombre_canal.len() + 8 + raiz_manifiesto.len());
318 mensaje.extend_from_slice(nombre_canal.as_bytes());
319 mensaje.extend_from_slice(×tamp.to_le_bytes());
320 mensaje.extend_from_slice(raiz_manifiesto);
321 mensaje
322}
323
324/// Compone el mensaje canonico que un autor firma para CONCEDER capacidad a un
325/// bytecode: `bytecode(32) || permisos_le(4)`. Es la unica verdad del payload
326/// firmable de una [`ConcesionCapacidad`] —firmante y verificador lo componen
327/// por aqui, jamas a mano—. Liga la firma al hash EXACTO del binario y al
328/// bitfield EXACTO: una concesion para el bytecode X no vale para Y, y subir un
329/// bit de permiso invalida la firma. Devuelve un arreglo de pila de 36 bytes:
330/// zero-alloc, apto para el camino Ring 0 del kernel.
331pub fn mensaje_capacidad(bytecode: &Hash, permisos: Permisos) -> [u8; 36] {
332 let mut m = [0u8; 36];
333 m[..32].copy_from_slice(bytecode);
334 m[32..].copy_from_slice(&permisos.to_le_bytes());
335 m
336}
337
338/// Dominio de separacion del mensaje de ROTACION de clave. Un byte canonico de
339/// rotacion jamas colisiona con uno de revocacion ni con un claim del grafo.
340pub const DOM_ROTACION_CLAVE: &[u8] = b"agora-key-rotation\x01";
341
342/// Dominio de separacion del mensaje de REVOCACION de clave.
343pub const DOM_REVOCACION_CLAVE: &[u8] = b"agora-revocation\x01";
344
345/// Compone el mensaje canonico de una ROTACION de clave (handoff voluntario
346/// vieja->nueva): `DOM || old(32) || new(32) || issued_at_le(8)`. Tamanos fijos,
347/// sin prefijos de largo; el dominio lo separa de otros records. Es la unica
348/// verdad del payload firmable de una rotacion — `agora-core::KeyRotation` lo
349/// compone por aqui y el kernel lo espeja sobre estos mismos bytes (ver
350/// `agora/SDD-rotacion-revocacion.md` §2.1).
351pub fn mensaje_rotacion_clave(old_key: &[u8; 32], new_key: &[u8; 32], issued_at: u64) -> Vec<u8> {
352 let mut out = Vec::with_capacity(DOM_ROTACION_CLAVE.len() + 72);
353 out.extend_from_slice(DOM_ROTACION_CLAVE);
354 out.extend_from_slice(old_key);
355 out.extend_from_slice(new_key);
356 out.extend_from_slice(&issued_at.to_le_bytes());
357 out
358}
359
360/// Compone el mensaje canonico de una REVOCACION de clave:
361/// `DOM || target(32) || [motivo] || issued_at_le(8) || tag || [expires_le(8)]`,
362/// donde `tag` es `0` si `expires_at` es `None` y `1` si es `Some` (para que
363/// `None` y `Some(0)` no colisionen). El `motivo` es el discriminante estable de
364/// `agora-core::RevReason` (0=Compromised, 1=Retired, 2=Superseded) — entra en la
365/// firma para que no se pueda "ascender" un retiro a compromiso sin re-firmar.
366/// Unica verdad del payload firmable de una revocacion: `agora-core::Revocation`
367/// lo compone por aqui y el kernel lo espeja en `claves::verificar_revocacion`
368/// (ver `agora/SDD-rotacion-revocacion.md` §2.2 y §4).
369pub fn mensaje_revocacion_clave(
370 target_key: &[u8; 32],
371 motivo: u8,
372 issued_at: u64,
373 expires_at: Option<u64>,
374) -> Vec<u8> {
375 let mut out = Vec::with_capacity(DOM_REVOCACION_CLAVE.len() + 50);
376 out.extend_from_slice(DOM_REVOCACION_CLAVE);
377 out.extend_from_slice(target_key);
378 out.push(motivo);
379 out.extend_from_slice(&issued_at.to_le_bytes());
380 match expires_at {
381 None => out.push(0),
382 Some(t) => {
383 out.push(1);
384 out.extend_from_slice(&t.to_le_bytes());
385 }
386 }
387 out
388}
389
390/// Permisos EFECTIVOS de una app: la INTERSECCION de lo que su `EntradaApp` del
391/// manifiesto DECLARA y lo que una [`ConcesionCapacidad`] valida CONCEDE para su
392/// bytecode. El manifiesto no puede escalar un binario mas alla de su concesion
393/// firmada, y una concesion generosa no enciende permisos que el manifiesto no
394/// pidio. Sin concesion valida, el llamante pasa `0` como `concedidos` y la app
395/// corre sin capacidades gateadas (la matriz pasiva siempre esta). Es la regla
396/// que el kernel aplica en el punto de carga —ver `SDD-capacidades.md`—.
397pub const fn permisos_efectivos(declarados: Permisos, concedidos: Permisos) -> Permisos {
398 declarados & concedidos
399}
400