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

⎇ 93c035f15f8f8aab331a99650e173b325ebf91b0 scripts/servidor/jaula-preparar.sh Shell · 109 siq'ikuna · 7.1 KB Ñawpa willay ch'uya

ADR 0020: el experimento CORRIDO — 10 de 12 sobreviven a `-ec`, y el que cae tiene el artefacto BIEN 27ec9caea · Sergio ·

1#!/bin/sh
2# Repone en la jaula de una persona lo que NO cabe en el manifiesto y por eso vive en el `upper`
3# —que por el D3 del ADR 0015 es **caché descartable**—. Se corre EN LA CAJA, como root, DESPUÉS de
4# cada `takana qorpa recreate`.
5#
6# ── POR QUÉ HAY COSAS FUERA DEL MANIFIESTO ──────────────────────────────────────────────────────
7# `instance.toml` declara imagen, paquetes y concesiones. No declara «editá este fichero de la
8# imagen», y tres defectos reales pedían exactamente eso. Los encontró el agente trabajando adentro
9# (2026-09-18) y los arregló a mano; esto es esa mano, escrita, para que sobreviva al recreate.
10#
11#   1. **No hay entrada en `/etc/passwd` para el uid de adentro.** `ssh` y `git` mueren con
12#      `No user exists for uid 1001` — ni lo intentan. Pasa porque la instancia de persona va con
13#      `root = false` (si no, el CLI se ve uid 0 y rechaza `--dangerously-skip-permissions`), y ese
14#      uid no existe en la imagen de Arch.
15#   2. **`/etc/ssh/ssh_config.d` es de la capa inferior y su dueño cae fuera del mapa** (`nobody`),
16#      así que `ssh` ABORTA con `Bad owner or permissions on …/20-systemd-ssh-proxy.conf`. No
17#      degrada: aborta. Y el directorio no se puede mover (EXDEV en overlay), así que se redirige el
18#      `Include` a una copia propia.
19#   3. `known_hosts` se siembra desde el ANFITRIÓN (ver abajo), que es lo que evita el
20#      `StrictHostKeyChecking` contra el worker la primera vez.
21set -eu
22
23U=${1:-sergio}
24INST=${INST:-claude-$U}
25UP=${UP:-/var/lib/hammer/qorpa/instances/$INST/upper}
26UID_DENTRO=$(id -u "$U")
27
28[ -d "$UP" ] || { echo "no existe el upper de $INST en $UP" >&2; exit 1; }
29# El upper es de la persona: escribir como root dejaría ficheros que ella no puede tocar.
30esc() { install -d -o "$U" -g "$U" "$(dirname "$1")"; }
31
32# ── 1. la cuenta, dentro de la imagen ───────────────────────────────────────────────────────────
33esc "$UP/etc/passwd"
34if [ ! -f "$UP/etc/passwd" ]; then cp /var/lib/hammer/qorpa/images/*/rootfs/etc/passwd "$UP/etc/passwd" 2>/dev/null || : ; fi
35grep -q "^$U:" "$UP/etc/passwd" 2>/dev/null || \
36  echo "$U:x:$UID_DENTRO:$UID_DENTRO::/home/$U:/usr/bin/bash" >> "$UP/etc/passwd"
37esc "$UP/etc/group"
38if [ ! -f "$UP/etc/group" ]; then cp /var/lib/hammer/qorpa/images/*/rootfs/etc/group "$UP/etc/group" 2>/dev/null || : ; fi
39grep -q "^$U:" "$UP/etc/group" 2>/dev/null || echo "$U:x:$UID_DENTRO:" >> "$UP/etc/group"
40chown "$U:$U" "$UP/etc/passwd" "$UP/etc/group" 2>/dev/null || true
41echo "  ✓ $U en el passwd/group de la instancia (uid $UID_DENTRO)"
42
43# ── 2. el Include de ssh, a un directorio cuyo dueño SÍ está en el mapa ──────────────────────────
44esc "$UP/etc/ssh/ssh_config"
45if [ ! -f "$UP/etc/ssh/ssh_config" ]; then
46  cp /var/lib/hammer/qorpa/images/*/rootfs/etc/ssh/ssh_config "$UP/etc/ssh/ssh_config" 2>/dev/null || :
47fi
48if [ -f "$UP/etc/ssh/ssh_config" ]; then
49  install -d -o "$U" -g "$U" "$UP/etc/ssh/ssh_config.d.local"
50  sed -i "s|^Include /etc/ssh/ssh_config.d/\*.conf|Include /etc/ssh/ssh_config.d.local/*.conf|" "$UP/etc/ssh/ssh_config"
51  chown "$U:$U" "$UP/etc/ssh/ssh_config"
52  echo "  ✓ Include redirigido a ssh_config.d.local (el original es de la capa inferior, dueño fuera del mapa)"
53fi
54
55# ── 3. known_hosts sembrado desde el anfitrión ──────────────────────────────────────────────────
56K=/home/$U/.ssh/known_hosts
57install -d -o "$U" -g "$U" -m 700 "/home/$U/.ssh"
58for h in dev.gioser.net git.gioser.net; do
59  grep -q "^\[\?$h" "$K" 2>/dev/null || ssh-keyscan -t ed25519 "$h" 2>/dev/null >> "$K" || true
60done
61chown "$U:$U" "$K" 2>/dev/null || true; chmod 600 "$K" 2>/dev/null || true
62echo "  ✓ known_hosts con $(grep -c . "$K" 2>/dev/null || echo 0) entradas"
63
64# ── 4. las herramientas del STORE, en el PATH de adentro ────────────────────────────────────────
65# MEDIDO el 2026-09-18 construyendo DESDE la jaula. Cuatro binarios que hacen falta adentro y que no
66# tienen por qué venir del gestor de paquetes de la imagen: ya están sellados en `/store`, que la
67# instancia tiene concedido, y son musl estáticos — corren igual dentro de una imagen glibc.
68#
69#   · `takana`  el que construye. `/opt/takana/target/release/takana` del anfitrión es un enlace a
70#               `/usr/bin/takana`, que ADENTRO es el /usr/bin de Arch y no lo tiene.
71#   · `patch`   takana aplica los parches FUERA del sandbox, así que lo busca en ESTE PATH. Sin él,
72#               toda receta con parches muere en `spawn patch: No such file or directory` — `wtype`
73#               (sin parches) pasaba y `zsh` (con seis) no. El del lab NO sirve: es musl DINÁMICO y
74#               no arranca en una jaula glibc.
75#   · `gh`      el espejo a GitHub empuja por HTTPS con su token como credential helper.
76#   · `hcloud`  para ver la flota de Hetzner sin salir de la jaula.
77#
78# Los wrappers viven en /work/<usuario>/bin, que es bind del anfitrión y SOBREVIVE al recreate; lo
79# que se pierde es el enlace en el PATH, y eso es lo que repone esto. Cada wrapper elige el artefacto
80# MÁS RECIENTE del store en vez de un hash cableado, para sobrevivir a un re-sellado.
81BIN=/work/$U/bin
82if [ -d "$BIN" ]; then
83  install -d -o "$U" -g "$U" "$UP/usr/bin"
84  for t in takana patch gh hcloud rsync python3 jq cargo; do
85    [ -x "$BIN/$t" ] || continue
86    ln -sfn "$BIN/$t" "$UP/usr/bin/$t" 2>/dev/null || true
87  done
88  ln -sfn "$BIN/python3" "$UP/usr/bin/python" 2>/dev/null || true
89  echo "  ✓ herramientas del store enlazadas al PATH desde $BIN"
90fi
91
92# ── 5. el cargador de musl, para que los artefactos DINÁMICOS arranquen ─────────────────────────
93# No todo lo del store es estático: `python3` y `cargo` son musl DINÁMICOS y piden
94# `/lib/ld-musl-x86_64.so.1`, que una imagen glibc no tiene. Sin esto fallan con un `No such file or
95# directory` que señala al binario y no al cargador, que es lo que despista.
96#
97# ⚠ Y no alcanza con el cargador: `cargo` pide además `libz.so.1` y `libgcc_s.so.1` POR NOMBRE, y en
98# una imagen glibc esos nombres existen — con otro contenido. Coge las de glibc y muere en
99# `Error relocating /lib/libz.so.1: __snprintf_chk: symbol not found` (símbolos de fortify que musl
100# no tiene). Por eso su wrapper exporta LD_LIBRARY_PATH al lab; acá sólo va el cargador.
101LAB=${LAB:-/work/$U/dev-fs/alpine}
102if [ -f "$LAB/lib/ld-musl-x86_64.so.1" ]; then
103  install -D -o "$U" -g "$U" -m 755 "$LAB/lib/ld-musl-x86_64.so.1" "$UP/usr/lib/ld-musl-x86_64.so.1"
104  echo "  ✓ cargador de musl en /usr/lib (lo alcanza /lib, que es symlink a usr/lib)"
105else
106  echo "  · sin lab en $LAB — el cargador de musl no se pone (python3/cargo del store no arrancarán)"
107fi
108
109echo "jaula $INST preparada — lo del manifiesto (paquetes, concesiones) lo pone `takana qorpa provision`"