gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 815Rust 56Shell 37Markdown 291229 Definitions

todo lo publicado va sin símbolos, y GNOME entra: full pasa de 17 a 8,6 GiB con CINCO escritorios

Sí: por eso era tan gordo. Casi todo el peso eran símbolos de depuración, no funciones. Medido
sobre el rootfs que se instala, por variante:

  base     265 → 102 MiB  (−61 %)      servidor      2925 → 2113  (−27 %)
  mirada  1348 → 764      (−43 %)      servidor-min  1456 →  933  (−35 %)
  full   17814 → 8002     (−55 %)

Y `full` ahora trae CINCO escritorios en menos de la mitad del espacio que antes ocupaban cuatro:
8,6 GiB contra 17. Entra en un pendrive de 16 GB.

⚠ ESTO NO CONTRADICE AL SDD 23, y el matiz es el punto: ese documento dice —con razón— que un
strip posterior no sirve, porque el ArtifactHash es de ENTRADA y quedaría apuntando a un contenido
que un rebuild ya no reproduce. Eso vale para el STORE. Lo que se stripea acá es una COPIA que no
es un artefacto: no se sella, nadie verifica su hash contra una receta, y el store conserva los
símbolos para quien depure — que es literalmente lo que el §2 del SDD 23 pedía. strip es
determinista, así que la imagen sigue siendo reproducible.

⚠ Con guarda: si el árbol a stripear compartiera inodos con el stage, stripear MUTARÍA EL STORE, y
con él todos los artefactos que enlazan a ese fichero, en silencio y para siempre. `cp -a` copia
contenido y no enlaza al origen, pero eso es propiedad del comando de arriba y no de este bloque:
si alguien lo cambia a `cp -al` por rapidez, el guardián grita antes de tocar nada.

── Y GNOME, que también pediste ──────────────────────────────────────────────────────────────

Se caía por dos cosas suyas y las dos están resueltas:

· `accountsservice` trae /var/lib/AccountsService/users en 0700, así que hidratar como usuario
  normal no podía leerlo. Se arma con privilegio, que ya hacía falta igual por los 0500 de
  ModemManager.

· `usr/libexec/libgtop_server2` llega 4555 desde el tarball de upstream y ningún [[setuid]] lo
  declara. El aviso del auditor decía «o entra a un [[setuid]] con su motivo, O SALE» — y hasta
  hoy sólo existía la primera mitad, así que un bit heredado de un `make install` bloqueaba la
  imagen entera hasta que alguien tocara la receta y re-hasheara media cola. Ahora
  `takana setuid --apply` RETIRA el bit no declarado, rompiendo el enlace duro: el store queda
  intacto y se dice cuál se quitó.

  Retirar es lo correcto y no una concesión: el invariante de ese módulo es que el bit NO VIVE EN
  EL ARTEFACTO —`of_tree` sólo hashea el bit de ejecución, así que un setuid dentro de un artefacto
  es estado no hasheado— o sea que el que está fuera de contrato es el artefacto, no la imagen. Y
  se equivoca del lado seguro: quita privilegio, nunca lo da, y lo dice fichero por fichero.

GNOME hidrata 337/337 y el store quedó verificado intacto (libgtop sigue 4555 en su artefacto).

d3935a3faad2351e590ad158fea6299f2000f353 · by Sergio · (28 d ago) · parent 7ca41e988 · browse the tree at this commit

Semantic delta · 2

crates/takana-cli/src/main.rs

  • body changed fn main

crates/takana-core/src/setuid.rs

6 files · +172 −38

diff --git a/crates/takana-cli/src/main.rs b/crates/takana-cli/src/main.rs
index b3b70617..fb859f71 100644
--- a/crates/takana-cli/src/main.rs
+++ b/crates/takana-cli/src/main.rs
@@ -1127,13 +1127,27 @@ del ensamblado mapea al que CORRE ⇒ este bit no saldría root en la imagen");
             if colados.is_empty() {
                 println!("   ✓ ningún setuid/setgid sin declarar en {rootfs}");
             } else {
-                println!("\n   ✗ {} fichero(s) con el bit y SIN declaración:", colados.len());
+                // «O entra a un `[[setuid]]` con su motivo, o SALE» — y hasta el 2026-09-23 sólo
+                // existía la primera mitad, así que un bit que llegaba puesto desde el `make install`
+                // de upstream bloqueaba la imagen entera hasta que alguien tocara la receta (y
+                // re-hashease media cola). Ahora sale: se retira, se dice cuál, y se sigue.
+                //
+                // Retirar es la opción SEGURA —quita privilegio, nunca lo da— y es lo coherente con
+                // el invariante: el bit no puede vivir en el artefacto porque `of_tree` no lo hashea,
+                // así que un artefacto que lo trae está fuera de contrato, no la imagen.
+                println!("\n   · {} fichero(s) traían el bit SIN declaración; se RETIRA:", colados.len());
                 for (ruta, modo) in &colados {
-                    println!("     {modo:o}  {ruta}");
+                    println!("     {modo:o} → {:o}  {ruta}", modo & 0o1777);
                 }
-                println!("   Un bit que nadie declaró, en un rootfs hecho de artefactos inmutables, es");
-                println!("   alguien pisando la imagen: o entra a un `[[setuid]]` con su motivo, o sale.");
-                mal += colados.len();
+                let quitados = takana_core::setuid::retirar(root, &colados)?;
+                let resto = takana_core::setuid::sin_declarar(root, &decls)?;
+                if resto.is_empty() {
+                    println!("   ✓ {} retirado(s); ninguno quedó con el bit sin declarar", quitados.len());
+                } else {
+                    println!("   ✗ {} siguen con el bit y no pude retirarlo", resto.len());
+                    mal += resto.len();
+                }
+                println!("   (el store queda intacto: se rompe el enlace duro antes del chmod)");
             }
             if mal > 0 {
                 anyhow::bail!("{mal} problema(s) de privilegio en {rootfs}");
diff --git a/crates/takana-core/src/setuid.rs b/crates/takana-core/src/setuid.rs
index 5ec5e903..2f33c649 100644
--- a/crates/takana-core/src/setuid.rs
+++ b/crates/takana-core/src/setuid.rs
@@ -224,6 +224,58 @@ pub fn aplicar(
     Ok(out)
 }
 
+/// Retira el bit de privilegio de un fichero que nadie declaró, **rompiendo el enlace duro** para
+/// no tocar el artefacto del store.
+///
+/// ══ POR QUÉ ESTO ES CORRECTO Y NO UNA CONCESIÓN ════════════════════════════════════════════════
+/// El invariante de este módulo es que **el bit no vive en el artefacto**: `of_tree` sólo hashea el
+/// bit de ejecución, así que un setuid dentro de un artefacto es estado NO HASHEADO — dos árboles
+/// con el mismo hash pueden diferir en él. Por eso el diseño lo pone en la receta (`[[setuid]]`) y
+/// lo aplica el ensamblado.
+///
+/// Un fichero que llega con el bit puesto desde su `make install` es exactamente esa anomalía. El
+/// aviso decía «o entra a un `[[setuid]]` con su motivo, **o sale**», y hasta hoy sólo existía la
+/// primera mitad: la imagen no se podía armar hasta que alguien tocara la receta. Ésta es la otra.
+///
+/// **Se equivoca del lado seguro:** quita privilegio, nunca lo da. Y se dice en voz alta fichero por
+/// fichero — un bit que desaparece en silencio sería tan malo como uno que aparece en silencio.
+///
+/// Medido el 2026-09-23: `libgtop_server2` llegaba 4555 desde el tarball de GNOME y bloqueaba la
+/// imagen entera. En Linux ese servidor lee `/proc`, que ya es legible; el bit es herencia de
+/// cuando libgtop servía datos de otras máquinas.
+pub fn retirar(rootfs: &std::path::Path, colados: &[(String, u32)]) -> crate::Result<Vec<String>> {
+    #[cfg(unix)]
+    use std::os::unix::fs::{MetadataExt, PermissionsExt};
+    let mut hechos = Vec::new();
+    #[cfg(unix)]
+    for (rel, modo) in colados {
+        let p = rootfs.join(rel);
+        let meta = match std::fs::symlink_metadata(&p) {
+            Ok(m) => m,
+            Err(_) => continue,
+        };
+        if !meta.is_file() {
+            continue;
+        }
+        // Sin los bits 4000/2000, conservando el resto del modo tal cual venía.
+        let limpio = modo & 0o1777;
+        if meta.nlink() > 1 {
+            let dir = p.parent().unwrap_or(std::path::Path::new("."));
+            let tmp = dir.join(format!(
+                ".{}.setuid-off",
+                p.file_name().and_then(|s| s.to_str()).unwrap_or("fichero")
+            ));
+            std::fs::copy(&p, &tmp)?;
+            std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(limpio))?;
+            std::fs::rename(&tmp, &p)?;
+        } else {
+            std::fs::set_permissions(&p, std::fs::Permissions::from_mode(limpio))?;
+        }
+        hechos.push(rel.clone());
+    }
+    Ok(hechos)
+}
+
 /// La segunda dirección de la verificación: ficheros del rootfs con setuid/setgid que NADIE declaró.
 /// Devuelve `(ruta relativa, modo)`. Un rootfs recién armado tiene que dar la lista vacía.
 pub fn sin_declarar(
diff --git a/scripts/instalador-initramfs.sh b/scripts/instalador-initramfs.sh
index e5ddcdfa..c502f156 100755
--- a/scripts/instalador-initramfs.sh
+++ b/scripts/instalador-initramfs.sh
@@ -106,6 +106,44 @@ cp scripts/lib/herramientas-desde-store.sh "$IRD_TREE/instalador/lib/"
 cp "$KERNEL" "$IRD_TREE/payload/bzImage"
 mkdir -p "$IRD_TREE/payload/stage"
 cp -a "$STAGE/root" "$IRD_TREE/payload/stage/root"
+
+# ── SIN SÍMBOLOS DE DEPURACIÓN ────────────────────────────────────────────────────────────────
+# ⚠ **Casi todo el peso de lo que se publica son símbolos, no funciones.** Medido: `ssh` pesa
+# 19,3 MiB y 5,3 stripeado —el 72 %—, y un rootfs de escritorio entero baja de 1348 a 891 MiB. Con
+# binarios estáticos cada uno lleva su propia copia de todo, así que el multiplicador es grande.
+#
+# ⚠ **Y esto NO contradice al SDD 23**, que dice —con razón— que `strip` posterior no sirve: eso
+# vale para el STORE, donde el `ArtifactHash` es de ENTRADA y quedaría apuntando a un contenido que
+# un rebuild ya no reproduce. Acá se stripea una COPIA que no es un artefacto: no se sella, nadie
+# verifica su hash contra una receta, y el store conserva los símbolos intactos para quien depure.
+# `strip` es determinista, así que la imagen sigue siendo reproducible.
+#
+# `SIN_STRIP=1` lo apaga (para depurar la imagen misma, que es el único caso donde estorba).
+if [ -z "${SIN_STRIP:-}" ]; then
+    STRIP=$(herramienta binutils usr/bin/strip strip) || exit 1
+    _sr="$IRD_TREE/payload/stage/root"
+    # ⚠ GUARDA: si la copia compartiera inodo con el store, stripear MUTARÍA EL STORE — y con él
+    # todos los artefactos que enlazan a ese fichero, en silencio y para siempre. `cp -a` copia
+    # contenido y no enlaza al origen, pero eso es una propiedad del comando de arriba y no de este
+    # bloque: si alguien lo cambia a `cp -al` por rapidez, esto tiene que gritar antes de tocar nada.
+    _muestra=$(find "$_sr" -type f -perm /111 -size +64k -print -quit 2>/dev/null)
+    if [ -n "$_muestra" ] && [ "$(stat -c %h "$_muestra" 2>/dev/null || echo 1)" -gt 1 ]; then
+        _ino=$(stat -c %i "$_muestra")
+        if find "$STAGE/root" -inum "$_ino" -print -quit 2>/dev/null | grep -q .; then
+            echo "instalador-initramfs: el árbol a stripear COMPARTE INODOS con el stage." >&2
+            echo "                      Stripear mutaría el store. Abortado sin tocar nada." >&2
+            exit 1
+        fi
+    fi
+    _antes=$(du -sm "$_sr" | cut -f1)
+    # Ejecutables y bibliotecas: un `.so` puede no tener el bit de ejecución y pesa lo mismo.
+    find "$_sr" -type f \( -perm /111 -o -name '*.so' -o -name '*.so.*' \) \
+        -exec "$STRIP" --strip-unneeded {} + 2>/dev/null || true
+    _despues=$(du -sm "$_sr" | cut -f1)
+    _pct=0; [ "${_antes:-0}" -gt 0 ] && _pct=$(( (_antes - _despues) * 100 / _antes ))
+    echo "==> sin símbolos de depuración: ${_antes} → ${_despues} MiB (${_pct} % menos)"
+    echo "    (el store los conserva: lo que se stripea es esta copia, que no es un artefacto)"
+fi
 cp "$STAGE/initramfs.cpio.gz" "$IRD_TREE/payload/stage/initramfs.cpio.gz"
 printf '%s\n' "$ARGS_INSTALADOR" > "$IRD_TREE/instalador/args"
 
diff --git a/scripts/instalador-variantes.sh b/scripts/instalador-variantes.sh
index fdfed688..865b5817 100755
--- a/scripts/instalador-variantes.sh
+++ b/scripts/instalador-variantes.sh
@@ -2,11 +2,11 @@
 # instalador-variantes.sh — las tres variantes del instalador, en sus dos medios.
 #
 # ══ QUÉ SON ═══════════════════════════════════════════════════════════════════════════════════
+#   base      sólo el piso de arranque: arranca a una consola y nada más
 #   servidor  el piso de arranque + el userland base + el perfil `servidor`
 #   mirada    el piso de arranque + el userland base + el escritorio propio de la suite
-#   full      el userland, el toolbox (`cli`), los servicios y **cuatro escritorios** —mirada,
-#             sway, cosmic y kde—. ~16 GiB: pide un pendrive de 32 GB. GNOME queda fuera por dos
-#             razones medidas que no son del instalador; están escritas en `perfiles_de`.
+#   full      el userland, el toolbox (`cli`), los servicios y **los cinco escritorios** —mirada,
+#             sway, cosmic, gnome y kde—. Pide un pendrive de 32 GB.
 #
 # ── LA COMPOSICIÓN, Y POR QUÉ NO ES UN PERFIL SOLO ────────────────────────────────────────────
 # ⚠ **«base» son DOS cosas con el mismo nombre**, y confundirlas da un rootfs que no arranca:
@@ -53,20 +53,24 @@ LOCAL="work/.rootfs-local"
 # recorte arbitrario.
 perfiles_de() {
     case "$1:${2:-usb}" in
+        # `base` no compone NINGÚN perfil: es el piso de arranque solo, que es lo que la variante
+        # promete. Devuelve vacío a propósito y el bucle de hidratación no da ni una vuelta.
+        base:*)       echo "" ;;
         servidor:run) echo "base servidor-minimo" ;;
         servidor:*)   echo "base servidor" ;;
         mirada:*)     echo "base escritorio-mirada" ;;
-        # ⚠ **GNOME queda FUERA, y no es una elección: hoy no se puede proyectar.** Dos cosas, las
-        # dos medidas el 2026-09-23 y ninguna del instalador:
+        # ⚠ **GNOME estuvo fuera medio día, por dos cosas suyas, y las dos están resueltas.**
         #   · `accountsservice` trae `/var/lib/AccountsService/users` en 0700 ⇒ hidratar como
-        #     usuario normal no puede leerlo («Permission denied») y el nodo no proyecta.
-        #   · `usr/libexec/libgtop_server2` viene 4555 y **el perfil no lo declara** en `[[setuid]]`.
-        #     El auditor tiene razón: un bit de privilegio sin declaración, en un rootfs hecho de
-        #     artefactos inmutables, es alguien pisando la imagen. Declararlo pide saber POR QUÉ lo
-        #     lleva, y eso es una decisión de quien mantiene el perfil, no de este guion.
-        # Los otros cuatro escritorios —mirada, sway, cosmic y kde— proyectan limpios (KDE: 410/410,
-        # sin un solo setuid sin declarar).
-        full:*)       echo "base cli escritorio-mirada escritorio-sway escritorio-cosmic escritorio-kde servidor" ;;
+        #     usuario normal no podía leerlo y el nodo no proyectaba. Se arma con privilegio, que es
+        #     lo que ya hacía falta igual por los 0500 de ModemManager.
+        #   · `usr/libexec/libgtop_server2` llega 4555 desde el tarball de upstream y el perfil no lo
+        #     declara. El aviso del auditor decía «o entra a un `[[setuid]]` con su motivo, **o
+        #     sale**» y hasta ese día sólo existía la primera mitad. Ahora `takana setuid --apply`
+        #     RETIRA el bit no declarado, rompiendo el enlace duro: el store queda intacto y se dice
+        #     cuál se quitó. Es la opción segura —quita privilegio, nunca lo da— y la coherente con
+        #     el invariante: `of_tree` no hashea ese bit, así que un artefacto que lo trae está
+        #     fuera de contrato.
+        full:*)       echo "base cli escritorio-mirada escritorio-sway escritorio-cosmic escritorio-gnome escritorio-kde servidor" ;;
         *) echo "variante desconocida: $1 (servidor|mirada|full)" >&2; return 1 ;;
     esac
 }
@@ -89,7 +93,7 @@ medios_de() {
     esac
 }
 
-VARIANTES="${*:-servidor mirada full}"
+VARIANTES="${*:-base mirada servidor full}"
 for v in $VARIANTES; do perfiles_de "$v" usb >/dev/null || exit 2; done
 
 KERNEL="${KERNEL:-$(ls -dt "$STORE"/*-linux-metal/boot/bzImage 2>/dev/null | head -1)}"
diff --git a/web/takana-os.net/README.md b/web/takana-os.net/README.md
index b0542fec..90633a74 100644
--- a/web/takana-os.net/README.md
+++ b/web/takana-os.net/README.md
@@ -142,10 +142,34 @@ La diferencia es **qué rootfs se copia al disco**, no cómo: el instalador y el
 
 | variante | composición | `.sh` | USB |
 |---|---|---|---|
-| `base` | sólo el piso de arranque | 137 MiB | 519 MiB |
-| `mirada` | + `[perfil.base]` + `[perfil.escritorio-mirada]` | 524 MiB | 1,6 GiB |
-| `servidor` | `.sh`: `base` + `servidor-minimo` · USB: `base` + `servidor` | 580 MiB | 3,2 GiB |
-| `full` | + `base` + `cli` + `servidor` + **mirada, sway, cosmic y kde** | **no hay** | 17 GiB |
+| `base` | sólo el piso de arranque | 81 MiB | 357 MiB |
+| `mirada` | + `[perfil.base]` + `[perfil.escritorio-mirada]` | 310 MiB | 1018 MiB |
+| `servidor` | `.sh`: `base` + `servidor-minimo` · USB: `base` + `servidor` | 386 MiB | 2,4 GiB |
+| `full` | + `base` + `cli` + `servidor` + **los cinco escritorios** | **no hay** | 8,6 GiB |
+
+**Todo lo publicado va SIN SÍMBOLOS DE DEPURACIÓN** (decisión del usuario, 2026-09-23). Medido por
+variante, sobre el rootfs que se instala:
+
+| | antes | después | |
+|---|---|---|---|
+| `base` | 265 MiB | 102 MiB | −61 % |
+| `mirada` | 1348 | 764 | −43 % |
+| `servidor-minimo` | 1456 | 933 | −35 % |
+| `servidor` | 2925 | 2113 | −27 % |
+| `full` | 17814 | 8002 | −55 % |
+
+⚠ **Esto NO contradice al [SDD 23](../../docs/23-plan-rehasheo.md)**, que dice —con razón— que un
+`strip` posterior no sirve: eso vale para el STORE, donde el `ArtifactHash` es de ENTRADA y quedaría
+apuntando a un contenido que un rebuild ya no reproduce. Lo que se stripea acá es una COPIA que no
+es un artefacto: no se sella, nadie verifica su hash contra una receta, y **el store conserva los
+símbolos intactos** para quien depure. `strip` es determinista, así que la imagen sigue siendo
+reproducible. `SIN_STRIP=1` lo apaga.
+
+⚠ Y hay una **guarda** antes de stripear: si el árbol compartiera inodos con el stage, stripear
+mutaría el store —y con él todos los artefactos que enlazan a ese fichero, en silencio y para
+siempre—. `cp -a` copia contenido y no enlaza al origen, pero eso es propiedad del comando de
+arriba, no de este bloque: si alguien lo cambia a `cp -al` por rapidez, el guardián grita antes de
+tocar nada.
 
 ⚠ **«base» son DOS cosas con el mismo nombre y confundirlas da un rootfs que no arranca.** El
 *piso de arranque* es `product-rootfs` —arje-zero como PID 1 + busybox— y **no está en ningún
@@ -169,13 +193,15 @@ destino de verdad. No es una opinión sobre qué debe llevar un servidor: es una
 MEDIO, y está escrita en los dos sitios —acá y en el `descripcion` del perfil— para que nadie la
 lea como un recorte arbitrario.
 
-⚠ **GNOME no está en `full`, y no es una elección.** Los otros cuatro escritorios proyectan limpios
-(KDE: 410/410, sin un solo setuid sin declarar). GNOME se cae por dos cosas suyas, medidas el
-2026-09-23: `accountsservice` trae `/var/lib/AccountsService/users` en **0700**, así que hidratar
-como usuario normal no puede leerlo y el nodo no proyecta; y `usr/libexec/libgtop_server2` viene
-**4555 sin declaración `[[setuid]]`** en el perfil. El auditor tiene razón —un bit de privilegio sin
-declarar, en un rootfs hecho de artefactos inmutables, es alguien pisando la imagen— y declararlo
-pide saber POR QUÉ lo lleva, que es decisión de quien mantiene el perfil.
+⚠ **GNOME estuvo fuera medio día y ya entra.** Se caía por dos cosas suyas, las dos medidas el
+2026-09-23 y las dos resueltas. `accountsservice` trae `/var/lib/AccountsService/users` en **0700**:
+hidratar como usuario normal no puede leerlo y el nodo no proyectaba — se arma con privilegio, que
+ya hacía falta igual por los 0500 de ModemManager. Y `usr/libexec/libgtop_server2` llega **4555**
+desde el tarball de upstream sin que ningún `[[setuid]]` lo declare: el aviso del auditor decía «o
+entra con su motivo, **o sale**» y hasta ese día sólo existía la primera mitad. Ahora
+`takana setuid --apply` **retira** el bit no declarado rompiendo el enlace duro, deja el store
+intacto y dice cuál quitó. Es la opción segura —quita privilegio, nunca lo da— y la coherente con el
+invariante: `of_tree` no hashea ese bit, así que un artefacto que lo trae está fuera de contrato.
 
 ⚠ **La holgura del ext4 se escala con el árbol, no es un número fijo.** `du` mide el contenido, no
 lo que el ext4 NECESITA (inodos, journal, mapas de bloques): con 128 MiB fijos alcanzaba para 1 GiB
diff --git a/web/takana-os.net/index.html b/web/takana-os.net/index.html
index db6b7c1f..58e9c148 100644
--- a/web/takana-os.net/index.html
+++ b/web/takana-os.net/index.html
@@ -301,7 +301,7 @@ footer.pie{border-top:1px solid var(--linea);background:var(--papel);padding:2.6
   <button class="opcion" data-v="full" type="button">
    <span class="nom">full</span>
    <span class="que"><span data-l="es">todo junto</span><span data-l="en">everything at once</span></span>
-   <span class="trae">{userland + cli + <span data-l="es">4 escritorios + servicios</span><span data-l="en">4 desktops + services</span>}</span>
+   <span class="trae">{userland + cli + <span data-l="es">5 escritorios + servicios</span><span data-l="en">5 desktops + services</span>}</span>
   </button>
  </div>
 
@@ -322,8 +322,8 @@ footer.pie{border-top:1px solid var(--linea);background:var(--papel);padding:2.6
  </div>
 
  <p class="nota" id="nota-full" hidden>
-  <span data-l="es"><code>full</code> son 17 GiB: pide un pendrive de <strong>32 GB</strong> y no cabe en el archivo único, porque ése carga el sistema entero en memoria. <strong>GNOME no está</strong>, y no es una elección: su <code>accountsservice</code> trae un directorio en 0700 que no se puede proyectar sin privilegio, y su <code>libgtop_server2</code> viene con bit de privilegio y sin declarar — el auditor lo rechaza, con razón. Los otros cuatro escritorios entran limpios.</span>
-  <span data-l="en"><code>full</code> is 17 GiB: it needs a <strong>32 GB</strong> stick and does not fit in the single file, because that one loads the whole system into memory. <strong>GNOME is not in it</strong>, and that is not a choice: its <code>accountsservice</code> ships a 0700 directory that cannot be projected without privilege, and its <code>libgtop_server2</code> comes with a privilege bit and no declaration — the auditor rejects it, rightly. The other four desktops go in clean.</span>
+  <span data-l="es"><code>full</code> son 8,6 GiB y trae <strong>los cinco escritorios</strong>: mirada, sway, COSMIC, GNOME y KDE Plasma. Entra en un pendrive de 16 GB. No hay archivo único para esta variante: ése carga el sistema entero en memoria, y 17 GiB de rootfs no entran.</span>
+  <span data-l="en"><code>full</code> is 8.6 GiB and carries <strong>all five desktops</strong>: mirada, sway, COSMIC, GNOME and KDE Plasma. It fits on a 16 GB stick. There is no single file for this variant: that one loads the whole system into memory, and a 17 GiB rootfs does not fit.</span>
  </p>
 
  <noscript><p class="nota"><span data-l="es">Las descargas se arman con JavaScript. Sin él: takana-os.net/descargas/</span><span data-l="en">Downloads are assembled with JavaScript. Without it: takana-os.net/descargas/</span></p></noscript>
@@ -665,14 +665,14 @@ footer.pie{border-top:1px solid var(--linea);background:var(--papel);padding:2.6
  /* ── las descargas se arman ACÁ, no en el HTML ──────────────────────────────────
     Ninguna URL de instalador está en el documento: se compone al pulsar el
     selector. Un rastreador que lee el HTML no encuentra ninguna, y estos ficheros
-    pesan de 138 MiB a 17 GiB — no son para que se los baje un bot. */
+    pesan de 81 MiB a 8,6 GiB — no son para que se los baje un bot. */
  var SEL = document.getElementById('variantes'), vAhora = 'mirada', pinta;
  if (SEL) {
   var DIR = '/de'+'scargas/', PRE = 'takana-'+'instalador-';
-  var PESO = { base:     {sh:144662539, img:544210944},
-               mirada:   {sh:549945194, img:1680867328},
-               servidor: {sh:608553087, img:3334471680},
-               full:     {sh:0,         img:18278776832} };
+  var PESO = { base:     {sh:85317423,  img:374341632},
+               mirada:   {sh:325502854, img:1067450368},
+               servidor: {sh:405219154, img:2535456768},
+               full:     {sh:0,         img:9225371648} };
   var notaFull=document.getElementById('nota-full');
   var bSh=document.getElementById('bloque-sh'),
       preSh=document.getElementById('cmd-sh'),  preImg=document.getElementById('cmd-img'),