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…
takana etapa 5c: comentarios de scripts, MOTD, y un BUG que introdujo la etapa 4
1#!/usr/bin/env bash
2# respaldo-promover.sh — mueve lo que el worker depositó en el BUZÓN al respaldo consolidado.
3#
4# ── POR QUÉ HAY UN BUZÓN Y NO ESCRITURA DIRECTA ────────────────────────────────────────────────
5# El worker es EFÍMERO y se borra solo; darle escritura sobre `takana/store` sería darle permiso de
6# borrado sobre el único respaldo que existe. Y los permisos de una subcuenta del Storage Box son
7# BINARIOS: `--readonly` sí o no. No hay append-only, no hay write-sin-delete (verificado en la API
8# el 2026-08-09, no de memoria).
9#
10# Así que la contención no se hace con un permiso sino con el ALCANCE: la subcuenta de escritura
11# tiene su home en `incoming/` y `takana/store` sencillamente NO EXISTE para ella. Lo máximo que
12# puede destruir es lo que ella misma depositó y todavía no se promovió — que sigue estando en el
13# volumen de la granja. Un permiso puede estar mal puesto; un directorio que no está en tu home no.
14#
15# La promoción es un `mv` DENTRO del mismo filesystem: un rename, instantáneo, cero bytes por la red.
16# Eso es lo que hace viable que el laptop dirija la operación con un uplink de 440 kB/s — manda
17# órdenes, no datos.
18#
19# ── LO QUE LA SHELL DEL BOX NO TIENE (medido) ──────────────────────────────────────────────────
20# Es una shell RESTRINGIDA, no un bash. Medido el 2026-08-09:
21# · `for … do … done` → «Command not found». No hay bucles: el batch se arma ACÁ.
22# · `a; b` → no encadena de fiar (la segunda orden no corrió). Una orden por conexión.
23# · `mv a b c destino/`→ SÍ acepta varios orígenes ⇒ así se mueven cientos en una sola conexión.
24# · `find` → NO existe, y devuelve 0 EN SILENCIO. Un recuento con `find` acá parece
25# «no hay nada respaldado» y es mentira. Comparar con `ls` + `comm`.
26#
27# ── LA TRAMPA DEL `mv` SOBRE UN DESTINO QUE YA EXISTE ──────────────────────────────────────────
28# `mv A destino/` con `destino/A` ya existente NO falla: mete A DENTRO, y queda `destino/A/A`. Como
29# el store es CAS, un artefacto que ya está en el respaldo es idéntico al del buzón ⇒ no se mueve,
30# se BORRA del buzón. Por eso se parte en dos conjuntos antes de tocar nada.
31#
32# Uso: scripts/farm/respaldo-promover.sh [--seco]
33set -uo pipefail
34ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT"
35KEY="${KEY:-$HOME/.ssh/github5}"
36MAIN="${MAIN:-u647150@u647150.your-storagebox.de}"
37BUZON="${BUZON:-incoming/store}"
38RESP="${RESP:-hammer/store}"
39LOTE="${LOTE:-80}" # orígenes por conexión; el límite real es la longitud de la orden
40SECO=""; [ "${1:-}" = "--seco" ] && SECO=1
41
42sb() { ssh -4 -p 23 -i "$KEY" -o StrictHostKeyChecking=no -o ServerAliveInterval=30 "$MAIN" "$@"; }
43ART='^[0-9a-f]{64}-' # sólo artefactos: `bootstrap.json` y demás metadatos NO se promueven
44
45TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
46sb "ls $BUZON" 2>/dev/null | grep -E "$ART" | sort -u > "$TMP/buzon.txt"
47sb "ls $RESP" 2>/dev/null | grep -E "$ART" | sort -u > "$TMP/resp.txt"
48N_BUZON=$(wc -l < "$TMP/buzon.txt"); N_RESP=$(wc -l < "$TMP/resp.txt")
49
50# Un listado vacío del RESPALDO es un fallo de red disfrazado de respaldo borrado. Si lo tomáramos
51# por bueno, todo el buzón parecería «nuevo» y lo moveríamos sobre un destino que quizá sí tiene
52# contenido. Ante la duda, no se toca nada.
53if [ "$N_RESP" -eq 0 ]; then
54 echo "⚠ el respaldo devolvió 0 artefactos — no promuevo (¿enlace caído?)" >&2; exit 1
55fi
56comm -23 "$TMP/buzon.txt" "$TMP/resp.txt" > "$TMP/nuevos.txt"
57comm -12 "$TMP/buzon.txt" "$TMP/resp.txt" > "$TMP/duplicados.txt"
58N_NUEVOS=$(wc -l < "$TMP/nuevos.txt"); N_DUP=$(wc -l < "$TMP/duplicados.txt")
59
60echo "==> buzón $N_BUZON · respaldo $N_RESP ⇒ nuevos $N_NUEVOS · ya estaban $N_DUP"
61[ "$N_BUZON" -eq 0 ] && { echo " buzón vacío, nada que hacer"; exit 0; }
62[ -n "$SECO" ] && { echo " (--seco: no toco nada)"; exit 0; }
63
64# Los lotes se arman ACÁ (el box no sabe iterar) y viajan como argumentos de UNA orden por conexión.
65# Cada nombre es `<64 hex>-<paquete>`: sin espacios ni metacaracteres por construcción, así que
66# concatenarlos es seguro — y aun así se filtra por `$ART` antes de llegar hasta acá.
67en_lotes() { # $1 = fichero de nombres, $2 = orden ("mv"|"rm")
68 lote=""; n=0; fallos=0
69 while read -r d; do
70 lote="$lote $BUZON/$d"; n=$((n + 1))
71 if [ "$n" -ge "$LOTE" ]; then
72 case "$2" in
73 mv) sb "mv$lote $RESP/" >/dev/null 2>&1 || fallos=$((fallos + 1)) ;;
74 rm) sb "rm -rf$lote" >/dev/null 2>&1 || fallos=$((fallos + 1)) ;;
75 esac
76 lote=""; n=0; printf '.'
77 fi
78 done < "$1"
79 if [ -n "$lote" ]; then
80 case "$2" in
81 mv) sb "mv$lote $RESP/" >/dev/null 2>&1 || fallos=$((fallos + 1)) ;;
82 rm) sb "rm -rf$lote" >/dev/null 2>&1 || fallos=$((fallos + 1)) ;;
83 esac
84 printf '.'
85 fi
86 echo " ($2: $fallos lote(s) con error)"
87}
88[ "$N_NUEVOS" -gt 0 ] && { printf ' promoviendo '; en_lotes "$TMP/nuevos.txt" mv; }
89[ "$N_DUP" -gt 0 ] && { printf ' limpiando duplicados '; en_lotes "$TMP/duplicados.txt" rm; }
90
91# ── EL GUARDIÁN: RECONTAR, NO CREER ────────────────────────────────────────────────────────────
92# «moví N» no es lo mismo que «hay N más». La misma regla que salvó la poda del store.
93sb "ls $RESP" 2>/dev/null | grep -cE "$ART" > "$TMP/n2" || true
94sb "ls $BUZON" 2>/dev/null | grep -cE "$ART" > "$TMP/b2" || true
95N_RESP2=$(cat "$TMP/n2"); N_BUZON2=$(cat "$TMP/b2")
96echo "==> respaldo $N_RESP → $N_RESP2 (esperado $((N_RESP + N_NUEVOS))) · buzón $N_BUZON → $N_BUZON2 (esperado 0)"
97RC=0
98[ "$N_RESP2" -eq "$((N_RESP + N_NUEVOS))" ] || { echo "⚠ el respaldo NO cuadra"; RC=1; }
99[ "$N_BUZON2" -eq 0 ] || { echo "⚠ el buzón no quedó vacío ($N_BUZON2)"; RC=1; }
100[ "$RC" -eq 0 ] && echo "GUARDIÁN: el recuento CASA ✓"
101
102# El manifiesto que hace honesto al grafo de estado (ver la cabecera de build-state.py).
103scripts/respaldo-storagebox.sh --listar || true
104exit $RC