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…
claude-caja: el tope de sesiones era un número suelto que se leía como arquitectura
1#!/bin/sh
2# claude — entrar al agente en esta caja. El CLI es glibc y la caja es musl: corre enjaulado
3# (ADR 0015), con el repo, el store, el lab, la memoria y las llaves concedidos y nada más.
4#
5# ── UNA JAULA POR PERSONA, Y SIN PRIVILEGIO ─────────────────────────────────────────────────────
6# `root` usa la instancia `claude`; cada usuario usa `claude-<usuario>`. No es purismo: la instancia
7# concede el HOME de quien entra —su `.claude`, o sea sus credenciales y su memoria— y una sola
8# instancia obligaría a compartirlo. Medido: con la instancia de root, `sergio` muere en
9# `bwrap: Can't mkdir /root/.claude: Permission denied`.
10#
11# ⚠ Y NO se eleva con `doas`/`sudo`: en esta caja **el setuid no toma** («doas: Operation not
12# permitted», con `NoNewPrivs: 0` y `/` sin `nosuid` — queda como deuda entender por qué). No hace
13# falta: `qorpa run` levanta la jaula con user namespaces y anda **sin privilegio**, siempre que el
14# directorio de la instancia sea del usuario.
15#
16# ⚠ Y LOS BUILDS **SÍ CORREN DENTRO DE LA JAULA** — medido el 2026-09-18 por el agente de adentro,
17# y corrige lo que decía este comentario. Dos recetas construidas y SELLADAS desde la jaula, con el
18# hash forzado para que no hubiera caché:
19# · `anew` (Go, estática) — fetch → `go mod vendor` → compile → sealed, 6,8 s
20# · `lz4` (C, estática) — `make` + `make install` escribiendo `/out` → sealed, 4,4 s
21# Con `root = false` el bwrap anidado se levanta y `/proc` monta. Lo que NO se probó es una receta
22# cuyo `install` fije DUEÑOS: ahí sí muerde que el anidado no pueda mapear el uid 0, porque `sergio`
23# no tiene rango en `/etc/subuid` (sólo lo tiene `root`) y `newuidmap`/`newgidmap` no son setuid.
24#
25# ⚠ SE INTENTÓ CERRARLO Y **NO ALCANZÓ** (2026-09-18). Se hicieron las dos cosas obvias:
26# · rango para `sergio` en `/etc/subuid` y `/etc/subgid` (165536:65536);
27# · `newuidmap`/`newgidmap` en setuid root **y además** con capacidades (`cap_setuid=ep`).
28# El error se MOVIÓ —ya no dice «no hay rango para sergio»— pero sigue sin mapear:
29#
30# ⚠ sin mapeo por rango (newuidmap: open of uid_map failed: Permission denied)
31#
32# Lo que quedó descartado, medido: **el setuid sí eleva en esta caja** —sonda en C compilada con el
33# `cc` nuevo: `uid=1001 euid=0`—, `/` no está `nosuid`, y el `uid_map` del proceso es suyo
34# (`-rw-r--r-- sergio:sergio`). O sea que no es ni el rango que faltaba ni el bit que no tomaba.
35# ⚠ Y un detalle que cuesta un rato si no se sabe: **`chown` BORRA el bit setuid**, así que hay que
36# chownear primero y poner el bit después; la primera sonda salió sin bit por ese orden.
37# La causa de fondo queda SIN DETERMINAR, y se dice así en vez de inventarla.
38#
39# Mientras tanto el camino que sí anda es pedírselo al ANFITRIÓN por ssh:
40#
41# ssh -p 22022 root@127.0.0.1 'cd /opt/takana && flock -o work/.farm-build.lock takana build <r>'
42#
43# ⚠ Y UNA JAULA QUE TE MAPEA A ROOT ROMPE AL PROPIO AGENTE: con `root = true` el CLI se ve uid 0 y
44# **rechaza `--dangerously-skip-permissions`** («cannot be used with root/sudo privileges»). Por eso
45# las instancias de PERSONA van con `root = false` (queda uid 1001 adentro) y con `/store` en `rw`,
46# que es lo que la de `root` ya tenía y a la de `sergio` le faltaba: sin eso el agente lee el store
47# y no puede sellar nada.
48set -eu
49
50# ⚠ `/usr/bin/claude` ES UN ENLACE A ESTE FICHERO, y lo es por algo (2026-09-18): antes era una
51# COPIA, así que arreglar el guion en el repo no cambiaba nada para quien escribe `claude` — el
52# usuario reportó dos veces «sigue abriendo en /opt/takana» con el arreglo ya pusheado y tirado en la
53# caja. Un instalado que es copia DERIVA en silencio y sólo se nota por el síntoma viejo.
54#
55# ln -sf /opt/takana/scripts/servidor/claude-caja.sh /usr/bin/claude
56_yo="$(id -un)"
57_hogar="${HOME:-/root}"
58_base="claude"
59[ "$_yo" != "root" ] && _base="claude-$_yo"
60
61# ── VARIAS SESIONES A LA VEZ ────────────────────────────────────────────────────────────────────
62# El kernel niega DOS overlays sobre el mismo `upper` —la capa mutable se corrompería— y eso no se
63# discute. Pero «un solo Claude» era empaquetado nuestro, no del kernel: instancias distintas tienen
64# `upper` distintos y conviven. Así que si la de siempre está ocupada, se usa la siguiente.
65#
66# ⚠ La instancia nueva NO se aprovisiona: se CLONA la capa ya hecha. Con un mapa de un solo id
67# —lo que hay hoy, ver §6.55— `pacman` no puede chownear su descarga y el `provision` muere en
68# «failed to chown temporary download directory». Copiar el `upper` cuesta ~1 s y 169 M y no depende
69# de eso. Cuando el mapeo por rango funcione, `takana qorpa provision` vuelve a ser el camino.
70BASEDIR=${BASEDIR:-/var/lib/hammer/qorpa/instances}
71_libre() { ! pgrep -f "[q]orpa run $1 " >/dev/null 2>&1; }
72
73_inst="$_base"; _n=2
74while ! _libre "$_inst"; do
75 _inst="$_base-$_n"; _n=$((_n+1))
76 # ⚠ ESTE NÚMERO ES EL TOPE, y hasta el 2026-09-23 era lo ÚNICO que limitaba cuántos Claudes
77 # hay. Se leía como arquitectura y no lo es: el kernel sólo prohíbe DOS overlays sobre el mismo
78 # `upper` (una jaula = un Claude), y de eso ya se encarga el bucle clonando la capa siguiente.
79 # El techo real es la RAM, MEDIDO acá el 23-sep: 8 sesiones = 2,5 G de RSS (147–493 M cada una,
80 # ~310 M de media) sobre 7,7 G con 8 G de swap y 4,1 ya comidos. Los 4 núcleos NO limitan —las
81 # sesiones esperan la API, no CPU, y `taller` ya serializa los `cargo`—. Disco: 169 M por jaula.
82 # Subirlo es cambiar los dos números de esta línea; pasado ~14 empieza a jugarse con el OOM, que
83 # es el modo de falla documentado (INFORME-CAIDA-2026-08-17: la caída fue con ocho adentro).
84 [ "$_n" -gt 11 ] && { echo "claude: hay 10 sesiones abiertas de $_base — cerrá alguna" >&2; exit 1; }
85done
86if [ ! -d "$BASEDIR/$_inst" ]; then
87 echo "claude: abro una sesión más ($_inst) — clonando la capa, ~1 s" >&2
88 # ⚠ SIN RETROREFERENCIAS: acá había un `sed` con `\1` y el fichero terminó guardando el
89 # BYTE 0x01 en su lugar, así que el sha salía como el carácter de control y `qorpa create`
90 # moría con «ninguna imagen empieza por "\u{1}"». `awk` parte por comillas y no tiene
91 # nada que una herramienta pueda reinterpretar.
92 _sha=$(awk -F\" '/^base *=/ {sub(/^sha256:/,"",$2); print $2; exit}' "$BASEDIR/$_base/instance.toml")
93 [ -n "$_sha" ] || { echo "claude: no pude leer el sha de la imagen en $BASEDIR/$_base/instance.toml" >&2; exit 1; }
94 takana qorpa create "$_inst" --base "$_sha" >/dev/null || { echo "claude: no pude crear $_inst" >&2; exit 1; }
95 cp "$BASEDIR/$_base/instance.toml" "$BASEDIR/$_inst/instance.toml"
96 rm -rf "$BASEDIR/$_inst/upper"
97 cp -a "$BASEDIR/$_base/upper" "$BASEDIR/$_inst/upper"
98 mkdir -p "$BASEDIR/$_inst/work"
99fi
100
101# El `cd` no es comodidad: la memoria del agente se indexa POR RUTA DE TRABAJO. Desde `/` —que es
102# donde `qorpa run` deja el shell— la sesión usa el proyecto `-` y el agente empieza sin saber nada
103# aunque su memoria esté en disco.
104#
105# ⚠ **Y ABRE DONDE ESTÁS PARADO, no en un sitio fijo** (2026-09-18). Antes el destino era
106# `/opt/takana` cableado, así que `cd /work/sergio/takana && claude` abría… `/opt/takana`: el agente
107# contestaba sobre OTRO árbol —el del root— y encima con la memoria de ese proyecto. Se reportó como
108# «entro al repo y claude sigue en /opt/takana», y era literal. Ahora el defecto es `$PWD`; el
109# cableado queda sólo para cuando no hay `PWD` utilizable.
110#
111# `PROY` sigue existiendo para abrirlo sobre otro árbol a propósito:
112#
113# PROY=/work/sergio/tawasuyu claude
114# ⚠ El `shift 2` no es adorno: sin él, el HOME y el proyecto que se pasan como posicionales se
115# cuelan COMO ARGUMENTOS del agente. Medido: la primera versión abrió la sesión con el prompt
116# «/home/sergio», y el agente contestó —con razón— que eso es una ruta, no un comando.
117# ⚠ El `cd` de adentro FALLA RUIDOSAMENTE: la jaula sólo monta los directorios concedidos, así que
118# desde una ruta no concedida el `cd` no encuentra nada. Antes eso se tragaba con `&&` y el agente
119# arrancaba en `/` sin decir por qué; ahora lo dice y sale.
120_proy="${PROY:-$PWD}"
121[ -d "$_proy" ] || _proy=/opt/takana
122
123exec takana qorpa run "$_inst" -- sh -c \
124 'hogar="$1"; proy="$2"; yo="$3"; shift 3
125 cd "$proy" 2>/dev/null || { echo "claude: adentro de la jaula no existe \"$proy\" — ¿está concedido ese directorio en la instancia? (ver grants.dirs)" >&2; exit 1; }
126 # `/work/<usuario>/bin` trae el toolchain del CORPUS (make, python3, rustc, cargo, takana…):
127 # musl estáticos del store, que corren dentro de la imagen glibc. Lo arma jaula-herramientas.sh.
128 # ⚠ EL PATH DEL LAB, que ningún doc decía y muerde en el primer build (2026-09-18, reportado por
129 # el agente de adentro): `takana build` invoca `go` y `zig` por nombre, y en la imagen de la jaula
130 # no están. Muere con `spawn go mod vendor: No such file or directory`, que no menciona el PATH.
131 # Viven en el lab, que ya está concedido.
132 exec env HOME="$hogar" TERM="${TERM:-xterm-256color}" \
133 PATH="/work/$yo/bin:/work/dev-fs/tools/go/bin:/work/dev-fs/tools/zig:$PATH" \
134 /work/claude-cli/2.1.240 "$@"' \
135 _ "$_hogar" "$_proy" "$_yo" "$@"