tawasuyu

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 1572Shell 203Rust 109Markdown 993728 Sutichaykuna

takana-os.net/descargas: UN fichero que se baja y sabe hacer todo

Hasta hoy instalar takana pedía copiar cuatro cosas juntas, con su árbol, y saber cuál correr.
Eso sirve para quien ya sabe; no sirve para publicar. Ahora se baja un fichero:

    curl -LO https://takana-os.net/descargas/takana-instalador.run
    chmod +x takana-instalador.run && sudo ./takana-instalador.run

y él hace las tres cosas por orden: comprueba si la máquina puede instalarse sin USB, muestra
el terreno, y recién entonces pregunta.

Cómo está hecho: cabecera `sh` + tar.gz pegado detrás, separado por OFFSET DE BYTES y no por
número de línea — un payload binario trae cualquier cosa y `tail -n +N` sobre bytes arbitrarios
es una apuesta. NO baja nada: todo viaja adentro, porque un instalador de sistema que necesita
red falla justo donde más caro sale, en la máquina que todavía no tiene sistema. El precio son
137 MiB y se pagan una vez.

⚠ Y los ceros a la izquierda son OCTAL en `$(( ))`. El offset se escribe con ancho fijo para
que sustituirlo no mueva la cabecera, o sea rellenado con ceros; `$(( 0000001234 + 1 ))` en sh
es 668, y el payload se lee desde el byte equivocado. El síntoma fue «el payload no coincide
con su sha256»: un mensaje de integridad para un bug de aritmética. Los ceros se limpian antes
de cualquier cuenta.

La verificación no se salta en silencio: si la máquina no tiene sha256sum, shasum ni openssl,
lo DICE. Un chequeo ausente y uno que pasó se ven igual en pantalla si nadie los cuenta.

Y `/var/tmp` antes que `/tmp` para desempaquetar: en casi toda distro moderna /tmp es tmpfs, o
sea RAM, y esos 137 MiB se vuelven a cargar en memoria para el salto — hacerlo en RAM lo cuenta
dos veces y en una máquina justa eso es un OOM en el peor momento.

El fichero NO va al repo y no puede ir: 137 MiB serían 137 MiB para siempre en cada clon, y el
espejo de GitHub rechaza cualquier fichero de más de 100 MiB — el push se rompería y nadie
relacionaría la causa con el sitio. Vive en /work/sergio/descargas (sdc, 255 G) y no en la raíz
de 5,8 G, que al llenarse no se degrada: se cae entera. Caddy lo sirve con un handle_path
propio, fuera del `encode` (ya viene comprimido).

Probado de punta a punta, no sólo servido: `VIA_RUN=1 scripts/metal-salto-test.sh` mete el
fichero PUBLICADO en la VM con linux-metal-kexec y comprueba sobre el disco que se verificó
solo, se desempaquetó, saltó e instaló. 13 en verde, con el bootmgfw.efi del vecino byte a byte
igual y la tabla sin cambiar. Entre el paquete y el .run hay una cabecera sh, un offset de
bytes, un sha256 y un desempaquetado: cuatro sitios nuevos donde romperse que probar el paquete
no cubría.

cc04b37f8ec92fbbb64e45e4e4065d0db2e087a5 · ruwaq Sergio · (28 p'unchaw ñawpaq) · tayta 0c76fa228 · kay commitpi sach'ata qhaway

Semántico sayk'uy

Mana ima sutichaypas t'ikrakurqanchu: qillqalla, yachachiylla utaq willaylla t'ikrakurqan.

4 qillqakuna · +349 −14

diff --git a/scripts/instalador-autoextraible.sh b/scripts/instalador-autoextraible.sh
new file mode 100755
index 00000000..d47b3069
--- /dev/null
+++ b/scripts/instalador-autoextraible.sh
@@ -0,0 +1,234 @@
+#!/bin/sh
+# instalador-autoextraible.sh — UN fichero que se baja y sabe hacer todo.
+#
+# ══ POR QUÉ ═══════════════════════════════════════════════════════════════════════════════════
+# El paquete del instalador son cuatro cosas (el binario, el instalador en RAM, su kernel y el
+# censo) y hasta hoy había que copiarlas juntas, con su árbol, y saber cuál correr. Eso sirve para
+# quien ya sabe; no sirve para publicar. Lo que se publica tiene que ser **un fichero**: se baja, se
+# le da permiso de ejecución, se corre, y él se encarga.
+#
+#     curl -LO https://takana-os.net/descargas/takana-instalador.run
+#     chmod +x takana-instalador.run
+#     sudo ./takana-instalador.run
+#
+# ── CÓMO ESTÁ HECHO ───────────────────────────────────────────────────────────────────────────
+# Cabecera `sh` + payload `tar.gz` pegado detrás, separado por OFFSET DE BYTES y no por número de
+# línea: un payload binario puede traer cualquier cosa, y `tail -n +N` sobre bytes arbitrarios es
+# una apuesta. El offset se calcula midiendo la cabecera ya escrita, con un hueco de ancho fijo para
+# que sustituirlo no cambie el tamaño de lo que se midió.
+#
+# ── LO QUE NO HACE, A PROPÓSITO ───────────────────────────────────────────────────────────────
+# **No baja nada.** Todo viaja adentro: sin red, sin espejo, sin resolver un nombre. Un instalador
+# de sistema que necesita internet para instalar falla justo donde más caro sale — en la máquina que
+# todavía no tiene sistema. El precio son ~140 MiB, y se pagan una vez.
+#
+# Uso:  scripts/instalador-autoextraible.sh [--paquete <dir>] [--salida <fichero>]
+set -eu
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
+PAQUETE="${PAQUETE:-work/instalador-salida}"
+SALIDA="${SALIDA:-work/takana-instalador.run}"
+while [ $# -gt 0 ]; do
+    case "$1" in
+        --paquete) PAQUETE="${2:?}"; shift 2 ;;
+        --salida)  SALIDA="${2:?}";  shift 2 ;;
+        -h|--help) sed -n '2,30p' "$0"; exit 0 ;;
+        *) echo "opción desconocida: $1" >&2; exit 2 ;;
+    esac
+done
+
+for f in takana takana-instalador.cpio.gz kernel/boot/bzImage takana-install-partition.sh \
+         lib/herramientas-desde-store.sh; do
+    [ -r "$PAQUETE/$f" ] || { echo "el paquete $PAQUETE no trae $f — armalo con scripts/instalador-paquete.sh" >&2; exit 1; }
+done
+
+. "$(dirname "$0")/lib/herramientas-desde-store.sh"
+GZIP=$(herramienta gzip bin/gzip gzip) || exit 1
+
+TMP="work/.autoextraible"; rm -rf "$TMP"; mkdir -p "$TMP"
+
+# ── 1) el payload ─────────────────────────────────────────────────────────────────────────────
+# `tar` del host: es el único punto donde no se usa una herramienta sellada, y es a propósito —
+# lo que importa del tar es que lo pueda LEER cualquier Linux, no con qué se escribió. El formato
+# es el POSIX de siempre.
+echo "==> payload"
+tar -C "$PAQUETE" -cf "$TMP/payload.tar" . 2>/dev/null || {
+    echo "no pude armar el tar del paquete (¿hay 'tar' en el PATH?)" >&2; exit 1; }
+"$GZIP" -9 -c "$TMP/payload.tar" > "$TMP/payload.tar.gz"
+rm -f "$TMP/payload.tar"
+SHA=$(sha256sum "$TMP/payload.tar.gz" | cut -d' ' -f1)
+MIB=$(( $(wc -c < "$TMP/payload.tar.gz") / 1048576 ))
+echo "    $MIB MiB · sha256 $(echo "$SHA" | cut -c1-16)…"
+
+# ── 2) la cabecera ────────────────────────────────────────────────────────────────────────────
+# `@@@@@@@@@@` son diez caracteres y el offset se escribe con diez dígitos: la sustitución no mueve
+# ni un byte de lo que ya se midió. Si alguien cambia el ancho de uno, tiene que cambiar el del otro.
+cat > "$TMP/cabecera" <<HDREOF
+#!/bin/sh
+# takana — instalador. UN fichero: adentro viene todo y no baja nada de internet.
+#
+#   sudo ./takana-instalador.run              mirar, preguntar e instalar
+#   sudo ./takana-instalador.run --check      ¿se puede sin USB? No toca nada.
+#   sudo ./takana-instalador.run --survey     ¿qué hay en los discos? No toca nada.
+#        ./takana-instalador.run --extraer D  sólo desempaquetar en D, sin correr nada
+#        ./takana-instalador.run --sha256     el sha256 del payload que trae adentro
+#
+# Lo que hay adentro: el programa \`takana\` (ELF estático, corre en cualquier Linux x86-64), el
+# instalador que corre en RAM, su kernel, y el censo del terreno.
+set -eu
+OFFSET=@@@@@@@@@@
+SHA_ESPERADO=$SHA
+PAYLOAD_MIB=$MIB
+
+# ⚠ **Los ceros a la izquierda son OCTAL en \`\$(( ))\`.** El offset se escribe con ancho fijo para
+# que sustituirlo no mueva la cabecera, y eso obliga a rellenar con ceros; pero \`\$(( 0000001234 + 1 ))\`
+# en sh es octal, o sea 668, y el payload se lee desde el byte equivocado. El síntoma es «el payload
+# no coincide con su sha256» — un mensaje de integridad para un bug de aritmética. Medido acá el
+# 2026-09-23 en la primera corrida. Se limpian los ceros ANTES de cualquier cuenta.
+OFFSET=\$(printf '%s' "\$OFFSET" | sed 's/^0*//')
+
+AUTO="\$0"
+[ -r "\$AUTO" ] || { echo "no puedo leerme a mí mismo (\$AUTO)" >&2; exit 1; }
+
+decir() { printf '%s\n' "\$*"; }
+morir() { printf '%s\n' "\$*" >&2; exit 1; }
+
+MODO=instalar; DESTINO_EXTRAER=""
+case "\${1:-}" in
+    --check)   MODO=check;   shift ;;
+    --survey)  MODO=survey;  shift ;;
+    --extraer) MODO=extraer; DESTINO_EXTRAER="\${2:?--extraer necesita un directorio}"; shift 2 ;;
+    --sha256)  printf '%s  payload\n' "\$SHA_ESPERADO"; exit 0 ;;
+    -h|--help) sed -n '2,12p' "\$AUTO" | sed 's/^# \{0,1\}//'; exit 0 ;;
+esac
+
+# ── la máquina ────────────────────────────────────────────────────────────────────────────────
+# El binario y el kernel son x86-64. En otra arquitectura no hay nada que intentar, y decirlo acá
+# es la diferencia entre un mensaje claro y un "cannot execute binary file" tres pasos después.
+ARQ=\$(uname -m 2>/dev/null || echo desconocida)
+case "\$ARQ" in
+    x86_64|amd64) ;;
+    *) morir "takana-instalador: esto es para x86-64 y esta máquina es \$ARQ. No hay nada acá que le sirva." ;;
+esac
+
+# ── dónde desempaquetar ───────────────────────────────────────────────────────────────────────
+# \`/var/tmp\` ANTES que \`/tmp\`: en casi toda distro moderna \`/tmp\` es tmpfs, o sea RAM, y acá se
+# desempaquetan ~\$PAYLOAD_MIB MiB que después se vuelven a cargar en memoria para el salto. Hacerlo
+# en RAM lo cuenta dos veces, y en una máquina justa eso es un OOM en el peor momento.
+if [ -n "\$DESTINO_EXTRAER" ]; then
+    DIR="\$DESTINO_EXTRAER"; mkdir -p "\$DIR"
+else
+    BASE="\${TAKANA_TMP:-}"
+    [ -n "\$BASE" ] || for c in /var/tmp /tmp; do [ -d "\$c" ] && [ -w "\$c" ] && { BASE="\$c"; break; }; done
+    [ -n "\$BASE" ] || morir "takana-instalador: no encuentro un directorio donde escribir (probá TAKANA_TMP=/ruta)"
+    DIR="\$BASE/takana-instalador.\$\$"
+    mkdir -p "\$DIR"
+    # Sin \`trap\`, un fallo a mitad deja \$PAYLOAD_MIB MiB tirados con un nombre que nadie reconoce.
+    # Si el salto ocurre, esto no corre nunca — y es correcto: para entonces ya no hay userspace.
+    trap 'rm -rf "\$DIR"' EXIT INT TERM
+fi
+
+# El espacio se mide ANTES: un \`tar\` que se queda sin disco a mitad deja un árbol incompleto y el
+# error que sale es del último fichero, no del disco.
+LIBRE=\$(df -Pk "\$DIR" 2>/dev/null | awk 'NR==2 {print int(\$4/1024)}')
+if [ -n "\${LIBRE:-}" ] && [ "\$LIBRE" -lt \$(( PAYLOAD_MIB + 64 )) ]; then
+    morir "takana-instalador: hacen falta ~\$(( PAYLOAD_MIB + 64 )) MiB en \$DIR y hay \$LIBRE MiB.
+                    Probá con TAKANA_TMP=/ruta/con/espacio."
+fi
+
+# ── el payload ────────────────────────────────────────────────────────────────────────────────
+decir "==> desempaquetando (\$PAYLOAD_MIB MiB) en \$DIR"
+tail -c +\$(( OFFSET + 1 )) "\$AUTO" > "\$DIR/payload.tar.gz" || morir "no pude leer el payload"
+
+# La comprobación de integridad NO se salta en silencio. Si no hay con qué hacerla, se dice: un
+# chequeo ausente y uno que pasó se ven igual en pantalla si nadie lo cuenta.
+SUMA=""
+for h in sha256sum "shasum -a 256"; do
+    if command -v "\${h%% *}" >/dev/null 2>&1; then
+        SUMA=\$(\$h "\$DIR/payload.tar.gz" 2>/dev/null | cut -d' ' -f1); break
+    fi
+done
+if [ -z "\$SUMA" ] && command -v openssl >/dev/null 2>&1; then
+    SUMA=\$(openssl dgst -sha256 "\$DIR/payload.tar.gz" 2>/dev/null | sed 's/.*= *//')
+fi
+if [ -z "\$SUMA" ]; then
+    decir "    ⚠ esta máquina no tiene sha256sum, shasum ni openssl: el payload NO se verificó."
+elif [ "\$SUMA" != "\$SHA_ESPERADO" ]; then
+    morir "takana-instalador: el payload no coincide con su sha256.
+                    esperado \$SHA_ESPERADO
+                    leído    \$SUMA
+                    La descarga se cortó, o el fichero no es el que se publicó. No sigo."
+else
+    decir "    ✓ payload verificado (sha256)"
+fi
+
+gzip -d -c "\$DIR/payload.tar.gz" | tar -x -C "\$DIR" || morir "no pude desempaquetar (¿hay gzip y tar?)"
+rm -f "\$DIR/payload.tar.gz"
+chmod +x "\$DIR/takana" 2>/dev/null || true
+
+if [ "\$MODO" = extraer ]; then
+    trap - EXIT INT TERM
+    decir "✓ desempaquetado en \$DIR"
+    decir "  el instalador es:  sudo \$DIR/takana metal install"
+    exit 0
+fi
+
+TK="\$DIR/takana"
+[ -x "\$TK" ] || morir "el payload no trae un 'takana' ejecutable"
+
+case "\$MODO" in
+    check)  exec "\$TK" metal check ;;
+    survey) exec "\$TK" metal survey ;;
+esac
+
+# ── el camino completo ────────────────────────────────────────────────────────────────────────
+# check informa, survey enseña y install pregunta. Los tres por separado y en ese orden, porque es
+# el orden en el que una persona necesita saber las cosas: si se puede, qué hay, y recién entonces
+# qué se va a hacer.
+decir ""
+if ! "\$TK" metal check; then
+    decir ""
+    decir "Con la imagen USB se instala igual: el instalador que viaja acá adentro es el mismo."
+    exit 1
+fi
+decir ""
+"\$TK" metal survey || true
+decir ""
+# Sin \`exec\`: el \`trap\` de limpieza tiene que seguir vivo si el salto NO ocurre.
+"\$TK" metal install "\$@"
+HDREOF
+
+# `exit 0` cierra la cabecera: sin él, el shell seguiría leyendo el payload como si fuera guion.
+printf '%s\n' 'exit 0' >> "$TMP/cabecera"
+
+# ── 3) el offset, medido sobre la cabecera ya escrita ─────────────────────────────────────────
+LARGO=$(wc -c < "$TMP/cabecera")
+sed "s/@@@@@@@@@@/$(printf '%010d' "$LARGO")/" "$TMP/cabecera" > "$TMP/cabecera.final"
+LARGO2=$(wc -c < "$TMP/cabecera.final")
+[ "$LARGO" = "$LARGO2" ] || { echo "la sustitución del offset cambió el tamaño ($LARGO → $LARGO2)" >&2; exit 1; }
+
+mkdir -p "$(dirname "$SALIDA")"
+cat "$TMP/cabecera.final" "$TMP/payload.tar.gz" > "$SALIDA"
+chmod 0755 "$SALIDA"
+rm -rf "$TMP"
+
+# ── 4) el control: que se desempaquete de verdad ──────────────────────────────────────────────
+# Un autoextraíble que no se probó es un fichero de 140 MiB que nadie sabe si abre. Se extrae acá
+# mismo y se compara con el paquete de origen, fichero por fichero.
+echo "==> control: desempaquetar y comparar"
+V="work/.autoextraible-verif"; rm -rf "$V"
+"./$SALIDA" --extraer "$V" >/dev/null || { echo "el autoextraíble no se desempaqueta" >&2; exit 1; }
+FALLOS=0
+for f in takana takana-instalador.cpio.gz kernel/boot/bzImage takana-install-partition.sh; do
+    a=$(sha256sum "$PAQUETE/$f" | cut -d' ' -f1)
+    b=$(sha256sum "$V/$f" 2>/dev/null | cut -d' ' -f1)
+    if [ "$a" = "$b" ]; then echo "    ✓ $f"; else echo "    ✗ $f NO coincide" >&2; FALLOS=$((FALLOS+1)); fi
+done
+rm -rf "$V"
+[ "$FALLOS" = 0 ] || exit 1
+
+echo
+echo "✓ $SALIDA  ($(( $(wc -c < "$SALIDA") / 1048576 )) MiB)"
+echo "  sha256: $(sha256sum "$SALIDA" | cut -d' ' -f1)"
+echo
+echo "  curl -LO https://takana-os.net/descargas/takana-instalador.run"
+echo "  chmod +x takana-instalador.run && sudo ./takana-instalador.run"
diff --git a/scripts/metal-salto-test.sh b/scripts/metal-salto-test.sh
index f2b7e5da..74e6f323 100755
--- a/scripts/metal-salto-test.sh
+++ b/scripts/metal-salto-test.sh
@@ -29,6 +29,11 @@ P="${P:-work/.salto-test}"
 PAQUETE="${PAQUETE:-work/instalador-salida}"
 REMOTO="/tmp/salto-test-$$"
 CONSERVAR=""; SOLO_VERIFICAR=""
+# ⚠ `VIA_RUN=1` prueba **el fichero que se publica**, no el paquete. Son cosas distintas: el paquete
+# es lo que arma el repo y el `.run` es lo que baja una persona, y entre uno y otro hay una cabecera
+# `sh`, un offset de bytes, una verificación de sha256 y un desempaquetado — o sea cuatro sitios
+# nuevos donde romperse, ninguno cubierto por probar el paquete.
+VIA_RUN="${VIA_RUN:-}"
 for a in "$@"; do
     case "$a" in
         --conservar) CONSERVAR=1 ;;
@@ -119,11 +124,20 @@ cp "$SFDISK" "$T/bin/sfdisk"; chmod 0755 "$T/bin/sfdisk"
 TAKANA_BIN="${TAKANA_BIN:-$ROOT/target/x86_64-unknown-linux-musl/release/takana}"
 [ -x "$TAKANA_BIN" ] || TAKANA_BIN="$ROOT/target/release/takana"
 cp "$TAKANA_BIN" "$T/bin/takana"; chmod 0755 "$T/bin/takana"
-# El PAQUETE tal como lo recibe el usuario: el instalador, su kernel y el censo.
-cp "$PAQUETE/takana-instalador.cpio.gz" "$T/paquete/"
-cp "$PAQUETE/kernel/boot/bzImage"       "$T/paquete/kernel/boot/"
-cp scripts/takana-install-partition.sh  "$T/paquete/"
-cp scripts/lib/herramientas-desde-store.sh "$T/paquete/lib/"
+if [ -n "$VIA_RUN" ]; then
+    RUN="${RUN:-work/takana-instalador.run}"
+    [ -x "$RUN" ] || { echo "no encuentro $RUN (armalo con scripts/instalador-autoextraible.sh)" >&2; exit 1; }
+    mkdir -p "$T/var/tmp"
+    cp "$RUN" "$T/takana-instalador.run"; chmod 0755 "$T/takana-instalador.run"
+    for a in tar gzip gunzip df expr; do ln -sf busybox "$T/bin/$a"; done
+    rm -rf "$T/paquete"
+else
+    # El PAQUETE tal como lo recibe el usuario: el instalador, su kernel y el censo.
+    cp "$PAQUETE/takana-instalador.cpio.gz" "$T/paquete/"
+    cp "$PAQUETE/kernel/boot/bzImage"       "$T/paquete/kernel/boot/"
+    cp scripts/takana-install-partition.sh  "$T/paquete/"
+    cp scripts/lib/herramientas-desde-store.sh "$T/paquete/lib/"
+fi
 
 cat > "$T/init" <<'INIT'
 #!/bin/busybox sh
@@ -151,13 +165,20 @@ for w in $(cat /proc/cmdline 2>/dev/null); do
         test.esp=*)  ESPP="${w#test.esp=}" ;;
     esac
 done
-log "SALTO-TEST: ───── takana metal check ─────"
-/bin/takana metal check > /dev/console 2>&1; log "SALTO-TEST: check rc=$?"
-log "SALTO-TEST: ───── takana metal survey ─────"
-/bin/takana metal survey --payload /paquete > /dev/console 2>&1; log "SALTO-TEST: survey rc=$?"
-log "SALTO-TEST: ───── takana metal install ─────"
-/bin/takana metal install --payload /paquete --root "$DEST" --esp "$ESPP" \
-    --unattended --yes > /dev/console 2>&1
+if [ -x /takana-instalador.run ]; then
+    # El camino del que BAJA UN FICHERO: la cabecera se desempaqueta sola y corre lo de adentro.
+    log "SALTO-TEST: ───── takana-instalador.run (el fichero publicado) ─────"
+    TAKANA_TMP=/var/tmp /takana-instalador.run --root "$DEST" --esp "$ESPP" \
+        --unattended --yes > /dev/console 2>&1
+else
+    log "SALTO-TEST: ───── takana metal check ─────"
+    /bin/takana metal check > /dev/console 2>&1; log "SALTO-TEST: check rc=$?"
+    log "SALTO-TEST: ───── takana metal survey ─────"
+    /bin/takana metal survey --payload /paquete > /dev/console 2>&1; log "SALTO-TEST: survey rc=$?"
+    log "SALTO-TEST: ───── takana metal install ─────"
+    /bin/takana metal install --payload /paquete --root "$DEST" --esp "$ESPP" \
+        --unattended --yes > /dev/console 2>&1
+fi
 log "SALTO-TEST-FALLO: el salto NO ocurrió (install rc=$?) — el ORIGEN sigue vivo"
 sync; sleep 3; reboot -f
 INIT
@@ -196,6 +217,11 @@ grep -q 'puede instalarse SIN USB' "$LOG" && ok "metal check: dice que SÍ (el k
     || falla "metal check no reconoció el kexec en un kernel que lo trae"
 grep -q 'el terreno de esta máquina' "$LOG" && ok "metal survey: el censo salió por pantalla" \
     || falla "metal survey no imprimió el censo"
+if [ -n "$VIA_RUN" ]; then
+    grep -q 'payload verificado (sha256)' "$LOG" && ok "el .run verificó su propio payload" \
+        || falla "el .run no verificó el sha256 de su payload"
+    grep -q 'desempaquetando' "$LOG" && ok "el .run se desempaquetó solo" || falla "el .run no se desempaquetó"
+fi
 grep -q 'saltando al instalador' "$LOG" && ok "metal install: anunció el salto" || falla "no anunció el salto"
 grep -q 'SALTO-TEST-FALLO' "$LOG" && falla "EL SALTO NO OCURRIÓ: el origen siguió vivo" \
     || ok "EL SALTO OCURRIÓ: el origen no volvió del kexec"
diff --git a/web/takana-os.net/README.md b/web/takana-os.net/README.md
index 68821866..e9cdfe2e 100644
--- a/web/takana-os.net/README.md
+++ b/web/takana-os.net/README.md
@@ -29,6 +29,36 @@ desviación, deliberada: en tema claro el texto de acento usa un brasa oscurecid
 el `#DF6B2A` de la marca sobre alpaca da 2,9:1 y no se lee. Las combinaciones de la página están
 medidas: ninguna baja de 4,5:1.
 
+## El instalador: `/descargas/` vive FUERA del repo
+
+`https://takana-os.net/descargas/takana-instalador.run` es **un fichero que trae todo adentro**:
+cabecera `sh` + un `tar.gz` pegado detrás con el binario `takana`, el instalador que corre en RAM, su
+kernel y el censo del terreno. Se baja, se le da permiso y se corre; no baja nada más.
+
+⚠ **No está en el repo, y no puede estarlo.** Son 137 MiB: en git serían 137 MiB para siempre en
+cada clon, y además **el espejo de GitHub rechaza cualquier fichero de más de 100 MiB** — o sea que
+el `push` se rompería y nadie relacionaría la causa con el sitio. Vive en `/work/sergio/descargas`,
+que está en `sdc` (255 G) y no en la raíz de 5,8 G, que se llena y tira la caja entera.
+
+```sh
+scripts/instalador-paquete.sh          # arma el paquete (y comprueba su coherencia)
+scripts/instalador-autoextraible.sh    # lo convierte en UN fichero, y lo desempaqueta para probarlo
+cp work/takana-instalador.run /work/sergio/descargas/
+( cd /work/sergio/descargas && sha256sum takana-instalador.run > takana-instalador.run.sha256 )
+```
+
+El `.sha256` que se publica al lado es **el del fichero entero**, para comprobar la descarga. Es
+distinto del que el `.run` lleva adentro, que es el **del payload** y lo verifica él solo antes de
+desempaquetar (`./takana-instalador.run --sha256` lo imprime).
+
+⚠ `/descargas` queda **fuera del `encode`** del vhost: el `.run` ya viene comprimido, y volver a
+comprimirlo por HTTP gasta CPU en cada descarga sin ahorrar un byte.
+
+**Está probado de punta a punta, no sólo servido:** `VIA_RUN=1 scripts/metal-salto-test.sh` mete el
+fichero publicado en una VM con `linux-metal-kexec`, lo corre, y comprueba sobre el DISCO que se
+verificó solo, se desempaquetó, saltó e instaló — 13 comprobaciones, con el `bootmgfw.efi` del
+vecino byte a byte igual y la tabla de particiones sin cambiar.
+
 ## Las dos direcciones que publica
 
 - **código** → `https://git.gioser.net/sergio/takana` (público; `git clone` por HTTPS)
@@ -61,7 +91,14 @@ Los bloques, por si hay que rehacer la caja (`caddy validate` → `arjectl resta
 ```caddyfile
 takana-os.net, www.takana-os.net {
 	import acceso
-	encode gzip zstd
+	@comprimible not path /descargas/*
+	encode @comprimible gzip zstd
+
+	redir /descargas /descargas/ permanent
+	handle_path /descargas/* {
+		root * /work/sergio/descargas
+		file_server browse
+	}
 
 	redir /repo /repo/ permanent
 	handle_path /repo/* {
@@ -97,6 +134,7 @@ contra el disco, y los dos que redirigen por destino.
 |---|---|---|
 | `takana-os.net`, `www` | el sitio | ✅ con certificado de Let's Encrypt (2026-09-21) |
 | `takana-os.net/repo/` | el repo de paquetes, el mismo `/srv/repo` | ✅ |
+| `takana-os.net/descargas/` | el instalador en UN fichero (`/work/sergio/descargas`) | ✅ (2026-09-23) |
 | `takana.gioser.net` | **301 → `takana-os.net`** | ✅ |
 | `git.takana-os.net` | **301 → `git.gioser.net`** | ⏳ vhost puesto, **falta el registro A** |
 
diff --git a/web/takana-os.net/index.html b/web/takana-os.net/index.html
index 7d862564..f3ca8f3f 100644
--- a/web/takana-os.net/index.html
+++ b/web/takana-os.net/index.html
@@ -222,12 +222,49 @@ footer.pie{border-top:1px solid var(--linea);background:var(--papel);padding:2.6
   <span data-l="en">A Linux distribution with two floors: downstairs a hermetic laboratory that compiles everything from source and keeps every artefact by its hash; upstairs a classic, mutable Linux where you can break things and go back.</span>
  </p>
  <div class="botones">
-  <a class="boton lleno" href="https://git.gioser.net/sergio/takana"><span data-l="es">El repositorio</span><span data-l="en">The repository</span> ↗</a>
+  <a class="boton lleno" href="#instalar"><span data-l="es">Instalar</span><span data-l="en">Install</span></a>
+  <a class="boton" href="https://git.gioser.net/sergio/takana"><span data-l="es">El repositorio</span><span data-l="en">The repository</span> ↗</a>
   <a class="boton" href="#paquetes"><span data-l="es">Paquetes</span><span data-l="en">Packages</span></a>
   <a class="boton" href="https://tawasuyu.net">tawasuyu.net ↗</a>
  </div>
 </div></section>
 
+<section class="franja" id="instalar"><div class="envoltura">
+ <p class="sello"><svg class="simbolo" viewBox="0 0 748 748"><use href="#tocapu"/></svg><span data-l="es">descargar · un archivo</span><span data-l="en">download · one file</span></p>
+ <h2><span data-l="es">Un archivo, y sabe hacer todo</span><span data-l="en">One file, and it knows how to do everything</span></h2>
+ <p>
+  <span data-l="es">No hay un ISO que grabar ni un árbol que copiar: se baja <strong>un fichero</strong>, se le da permiso y se corre. Adentro viene todo —el programa, el instalador que corre en RAM y su propio kernel— así que <strong>no baja nada de internet mientras instala</strong>. Un instalador de sistema que necesita red falla justo donde más caro sale: en la máquina que todavía no tiene sistema.</span>
+  <span data-l="en">There is no ISO to burn and no tree to copy: you download <strong>one file</strong>, make it executable and run it. Everything travels inside — the program, the installer that runs in RAM and its own kernel — so it <strong>downloads nothing from the internet while installing</strong>. A system installer that needs the network fails exactly where it costs most: on the machine that has no system yet.</span>
+ </p>
+
+<pre><span class="p">$</span> curl -LO https://takana-os.net/descargas/takana-instalador.run
+<span class="p">$</span> chmod +x takana-instalador.run
+<span class="p">$</span> sudo ./takana-instalador.run</pre>
+
+ <p>
+  <span data-l="es">Eso hace las tres cosas por orden: comprueba si esta máquina puede instalarse <em>sin USB</em>, muestra el terreno —cada partición, su tamaño, qué hay dentro y si se puede usar— y recién entonces pregunta. Lo cifrado, lo de Windows, la swap, las particiones de arranque y lo que está montado <strong>no se ofrecen</strong>, y se muestran igual con el motivo escrito al lado.</span>
+  <span data-l="en">It does three things in order: it checks whether this machine can be installed <em>without a USB</em>, it shows the terrain — every partition, its size, what is inside it and whether it can be used — and only then it asks. Encrypted volumes, Windows volumes, swap, boot partitions and anything mounted are <strong>never offered</strong>, and are shown anyway with the reason written next to them.</span>
+ </p>
+
+ <p class="nota">
+  <span data-l="es"><strong>Sin USB</strong> quiere decir que el Linux que estás usando salta al instalador en memoria y suelta su propio disco. Si el kernel de esa máquina no lo permite, el instalador lo dice y explica por qué en vez de fallar a mitad. Antes de escribir nada pide un «si» escrito, y después del salto vuelve a mostrar el terreno y a preguntar: son dos consentimientos para dos cosas distintas.</span>
+  <span data-l="en"><strong>Without a USB</strong> means the Linux you are using jumps into the in-memory installer and releases its own disk. If that machine's kernel does not allow it, the installer says so and explains why instead of failing halfway. It asks for a typed «si» before writing anything, and after the jump it shows the terrain and asks again: two consents for two different things.</span>
+ </p>
+
+ <div class="direccion">
+  <p class="etiqueta"><span data-l="es">el instalador · 137 MiB · x86-64</span><span data-l="en">the installer · 137 MiB · x86-64</span></p>
+  <a class="url" href="/descargas/takana-instalador.run">takana-os.net/descargas/takana-instalador.run</a>
+ </div>
+
+<pre><span class="p">$</span> curl -s https://takana-os.net/descargas/takana-instalador.run.sha256
+d4c97c71cd471343ac8ad430457dbe707018a084ce99b72b8ed484f1b4674875  takana-instalador.run</pre>
+
+ <p class="nota">
+  <span data-l="es">El fichero verifica su propio contenido antes de desempaquetarlo, y si la máquina no tiene con qué calcular un sha256 lo <em>dice</em> en vez de saltárselo en silencio. Con <code>--check</code> y <code>--survey</code> mira y no toca nada; con <code>--extraer DIR</code> sólo se desempaqueta, para leer antes de correr. Hace falta un firmware UEFI y una partición de al menos 2 GiB que se pueda perder; nada más se modifica.</span>
+  <span data-l="en">The file verifies its own contents before unpacking them, and if the machine has no way to compute a sha256 it <em>says so</em> instead of skipping it silently. With <code>--check</code> and <code>--survey</code> it looks and touches nothing; with <code>--extraer DIR</code> it only unpacks, so you can read before running. It needs UEFI firmware and one partition of at least 2 GiB that you can afford to lose; nothing else is modified.</span>
+ </p>
+</div></section>
+
 <section class="franja" id="pisos"><div class="envoltura">
  <p class="sello"><svg class="simbolo" viewBox="0 0 748 748"><use href="#tocapu"/></svg><span data-l="es">01 · los dos pisos</span><span data-l="en">01 · the two floors</span></p>
  <h2><span data-l="es">Por qué dos pisos y no uno</span><span data-l="en">Why two floors and not one</span></h2>