> 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…
mudados tawasuyu.net y gioser.net — y los padres que inventa bwrap eran 0700
Los dos sitios que quedaban con trafico real sirven desde la caja `takana`, con TLS publico y
verificados desde fuera. Los servicios viejos SIGUEN corriendo en gioser a proposito (decision del
usuario: mover el DNS y dejar el origen vivo, para poder volver en un minuto).
· `tawasuyu.net` + `www`: la raiz en gioser era EL MONOREPO ENTERO (145 G) servido por HTTP. El log
de accesos dice que las unicas rutas con trafico son `/`, `/descargas` y `/web`, asi que se
replicaron solo los dos subarboles que el sitio usa (1,2 M + 3,8 G) con la MISMA estructura, para
que cada `rewrite` siga siendo el mismo.
· `gioser.net` + `www`: 428 M de estaticos + `/reencuentro`, que necesita PHP. `/hooks/*` NO se
muda: lo atiende `webhook-deploy.py`, que redespliega aura/sigma/summa/brahman — los cuatro
FOSILES del §6.19 — y tiene cero peticiones. Muere con la caja vieja.
· `sergio` dejo de ser `CNAME -> www` y tiene su A propio (apuntando a gioser, sin cambio visible).
Sin eso, mover `www` se lo llevaba puesto: en esa zona casi todo cuelga de `www`.
PHP no entra al corpus y no hace falta: `php-fpm 8.5.10` corre en la instancia `gioser-php` (ADR
0015), supervisado por arje. Control contra el original en los dos sentidos: POST con la trampa
anti-robots da `{"ok":true}` 200 y GET da 405, igual que en gioser.
EL MURO, que es general y no de PHP: para montar en `/work/www/gioser-web`, bwrap crea `/work` y
`/work/www` —que la imagen no trae— y los crea **0700 root**. Adentro somos root y a mano todo
funciona; pero un servicio que BAJA de privilegio (php-fpm a `http`, o cualquier instancia con
`run_as`) no puede ni atravesarlos, y el sintoma es un «File not found» sobre un fichero que ESTA.
Medido con el control que lo separa de un problema de permisos del anfitrion: como `http`, leer un
fichero de la IMAGEN funciona y leer el directorio CONCEDIDO da Permission denied.
`grants_to_args` crea ahora los ancestros que faltan con `--perms 0755 --dir`, y SOLO los que la
imagen no trae: hacerlo sobre `/etc` o `/home` le cambiaria los modos a la imagen. Con test, y
probado rompiendolo a proposito (sale `["/etc","/etc/php"]` en vez de `["/etc/php"]`).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
diff --git a/crates/takana-cli/src/qorpa.rs b/crates/takana-cli/src/qorpa.rs
index ddbdb9ac..fc587f5b 100644
--- a/crates/takana-cli/src/qorpa.rs
+++ b/crates/takana-cli/src/qorpa.rs
@@ -1211,9 +1211,30 @@ fn provision(root: &Path, id: &str, force: bool, dry_run: bool) -> Result<()> {
Ok(())
}
+/// Los directorios INTERMEDIOS de un punto de montaje que la imagen no trae, de arriba hacia abajo.
+///
+/// No incluye el punto de montaje en sí: ése lo tapa el `--bind` y hereda los modos del anfitrión.
+/// Sólo devuelve los que faltan en la imagen, porque `--dir` sobre uno existente le cambiaría los
+/// permisos a la imagen. Ver el comentario largo en `grants_to_args`.
+fn ancestros_a_crear(dentro: &str, tree: &Path) -> Vec<String> {
+ let mut out = Vec::new();
+ let mut acc = String::new();
+ let partes: Vec<&str> = dentro.trim_matches('/').split('/').collect();
+ // `partes.len() - 1`: el último es el punto de montaje, no un ancestro.
+ for p in partes.iter().take(partes.len().saturating_sub(1)) {
+ if p.is_empty() { continue; }
+ acc.push('/');
+ acc.push_str(p);
+ if !tree.join(acc.trim_start_matches('/')).is_dir() {
+ out.push(acc.clone());
+ }
+ }
+ out
+}
+
/// Traduce las concesiones a argumentos de bwrap. Devuelve también las advertencias que el usuario
/// tiene que LEER, no descubrir: el ADR pide que el socket de Wayland se diga en la cara.
-fn grants_to_args(g: &Grants, destino_xdg: &str, wl: Option<&Path>) -> (Vec<String>, Vec<String>) {
+fn grants_to_args(g: &Grants, destino_xdg: &str, wl: Option<&Path>, tree: &Path) -> (Vec<String>, Vec<String>) {
let mut a: Vec<String> = Vec::new();
let mut warn: Vec<String> = Vec::new();
// Sin dep de libc: el dueño de /proc/self ES el uid efectivo.
@@ -1255,6 +1276,22 @@ fn grants_to_args(g: &Grants, destino_xdg: &str, wl: Option<&Path>) -> (Vec<Stri
let host = if let Some(rest) = d.host.strip_prefix('~') {
format!("{}{}", std::env::var("HOME").unwrap_or_default(), rest)
} else { d.host.clone() };
+ // ── LOS PADRES QUE INVENTA bwrap SON 0700, Y ESO DEJA LA CONCESIÓN FUERA DE ALCANCE ─────
+ // MEDIDO el 2026-09-15 con `php-fpm` en la instancia `gioser-php`: la concesión estaba
+ // montada, visible y con sus modos correctos, y el servicio contestaba «File not found».
+ // La causa no está en el bind sino en el CAMINO: para montar en `/work/www/gioser-web`,
+ // bwrap crea `/work` y `/work/www` —que no existen en la imagen— y los crea **0700 root**.
+ // Adentro somos root, así que a mano todo se lee; pero un servicio que BAJA de privilegio
+ // (php-fpm a `http`, o cualquier instancia con `run_as`) no puede ni atravesarlos. El
+ // síntoma no se parece a la causa: dice que falta el fichero, y el fichero está.
+ //
+ // Se crean explícitamente a 0755 **sólo los que la imagen no trae**: hacerlo sobre un
+ // directorio que ya existe (`/etc`, `/usr`, `/home`) le cambiaría los modos a la imagen,
+ // que es justo lo que no se quiere. Misma familia que el `--perms 1777` del `--tmpfs /tmp`.
+ for ancestro in ancestros_a_crear(&d.inside, tree) {
+ a.push("--perms".into()); a.push("0755".into());
+ a.push("--dir".into()); a.push(ancestro);
+ }
a.push(if d.mode == "rw" { "--bind".into() } else { "--ro-bind".into() });
a.push(host); a.push(d.inside.clone());
}
@@ -1498,7 +1535,7 @@ fn run_instance(
} else {
None
};
- let (grant_args, warns) = grants_to_args(&inst.grants, &runtime_dir, proxy_sock.as_deref());
+ let (grant_args, warns) = grants_to_args(&inst.grants, &runtime_dir, proxy_sock.as_deref(), &tree);
// El orden importa: `--overlay-src` describe la capa BAJA del `--overlay` que viene justo
// después. Y `--clearenv` va primero porque el entorno del host también es una concesión: lo
@@ -2742,21 +2779,42 @@ mod tests {
#[test]
fn sin_concesiones_la_red_queda_fuera_y_con_red_entra_el_resolv_conf() {
- let (a, w) = grants_to_args(&Grants::default(), "/run/user/0", None);
+ let (a, w) = grants_to_args(&Grants::default(), "/run/user/0", None, Path::new("/nonexistent"));
assert!(a.contains(&"--unshare-net".to_string()));
assert!(w.is_empty());
let g = Grants { network: true, ..Default::default() };
- let (a, _) = grants_to_args(&g, "/run/user/0", None);
+ let (a, _) = grants_to_args(&g, "/run/user/0", None, Path::new("/nonexistent"));
assert!(!a.contains(&"--unshare-net".to_string()));
// El segundo fallo clásico de la primera corrida, según el ADR.
assert!(a.iter().any(|x| x == "/etc/resolv.conf"));
}
+ #[test]
+ fn los_padres_que_faltan_se_crean_y_los_de_la_imagen_no_se_tocan() {
+ // El control que importa es el NEGATIVO: si esto devolviera los directorios que la imagen
+ // ya trae, `--dir` les cambiaría los permisos a `/etc` y `/home` de la imagen.
+ let d = tempfile::tempdir().unwrap();
+ let tree = d.path();
+ std::fs::create_dir_all(tree.join("etc")).unwrap();
+ std::fs::create_dir_all(tree.join("home")).unwrap();
+
+ // `/work` y `/work/www` no existen en la imagen ⇒ hay que crearlos, de arriba hacia abajo.
+ assert_eq!(
+ ancestros_a_crear("/work/www/gioser-web", tree),
+ vec!["/work".to_string(), "/work/www".to_string()]
+ );
+ // `/etc` SÍ existe ⇒ sólo el que falta.
+ assert_eq!(ancestros_a_crear("/etc/php/php-fpm.d", tree), vec!["/etc/php".to_string()]);
+ // Montar directamente bajo un directorio de la imagen no necesita crear NADA.
+ assert!(ancestros_a_crear("/home/sergio", tree).is_empty());
+ assert!(ancestros_a_crear("/srv", tree).is_empty());
+ }
+
#[test]
fn el_socket_de_wayland_avisa_que_es_un_borde_de_privilegio() {
let g = Grants { sockets: vec!["wayland".into()], ..Default::default() };
- let (_, w) = grants_to_args(&g, "/run/user/0", None);
+ let (_, w) = grants_to_args(&g, "/run/user/0", None, Path::new("/nonexistent"));
assert_eq!(w.len(), 1);
assert!(w[0].contains("CAPTURAR LA PANTALLA"), "el aviso no puede ser tibio: {}", w[0]);
}
diff --git a/docs/28-servidor-de-produccion.md b/docs/28-servidor-de-produccion.md
index 59f7c421..89eab81f 100644
--- a/docs/28-servidor-de-produccion.md
+++ b/docs/28-servidor-de-produccion.md
@@ -1517,6 +1517,82 @@ cron de la cosecha, y pinear HEAD re-hashearía la receta cada media hora sin qu
| **lo que falta de `sergio.gioser.net`** | el frontend ya está copiado en `/work/www/sergioh`; falta `/shuma/*` → `shuma-gateway` (:7378), que es glibc dinámico y **cabe en esta misma jaula** (el binario está rescatado en `/work/rescate-binarios/`) pero cuelga de `shuma-daemon`, que es un sistema entero, no un binario |
| **y los otros dos vivos** | `gioser.net`/`www` (estáticos + `/hooks/*` en :8770 + `/reencuentro` con php-fpm) y `tawasuyu.net` (estático, raíz en `/mnt/vvv/tawasuyu`) |
+### 6.26 ✅ `tawasuyu.net` y `gioser.net` MUDADOS — y el PHP que no existe en el corpus *(2026-09-15)*
+
+**Los dos sitios que quedaban con tráfico real sirven desde la caja `takana`**, con TLS público y
+verificados desde fuera. Los servicios viejos **siguen corriendo en gioser a propósito** (decisión del
+usuario: mudar el DNS y dejar el origen vivo, para poder volver en un minuto).
+
+| dominio | qué es | estado |
+|---|---|---|
+| `tawasuyu.net`, `www.` | landing + `/pkg` wasm + `/descargas` (3,8 G) | **200 desde 2.29.29.217** |
+| `gioser.net`, `www.` | 5 raíces estáticas (428 M) + `/reencuentro` con PHP | **200 desde 2.29.29.217** |
+| `sergio.gioser.net` | frontend + `/shuma/*` | **se queda en gioser**: su consola no tiene camino todavía |
+
+**`sergio` dejó de ser un `CNAME → www` y tiene su A propio** (apuntando a gioser, sin cambio
+visible). Sin eso, mover `www` se lo habría llevado puesto — es la trampa del §6.14 vista a tiempo:
+en esta zona **casi todo cuelga de `www`**. Lo que sí quedó colgando son tres fósiles (`api`,
+`mail.sigma`, `monitor`): antes daban 502 desde gioser y ahora fallan el TLS contra la caja. Están
+muertos hace meses; se anotan, no se mudan.
+
+**Dos cosas que el log de accesos decidió mejor que cualquier intuición:**
+- La raíz de `tawasuyu.net` en gioser era **el monorepo entero (145 G) servido por HTTP**. El log
+ dice que las únicas rutas con tráfico son `/`, `/descargas` y `/web` ⇒ se replicaron **sólo** los
+ dos subárboles que el sitio usa (1,2 M + 3,8 G), con la MISMA estructura para que cada `rewrite`
+ siga siendo el mismo.
+- **`/hooks/*` NO se muda**: lo atiende `webhook-deploy.py`, que redespliega `aura_*`, `sigma_*`,
+ `summa_*` y `brahman` — **los cuatro son fósiles del §6.19**, y tiene cero peticiones. Muere con la
+ caja vieja. Un servicio vivo que sólo sirve a muertos es basura, no deuda.
+
+#### PHP: la segunda jaula, y por qué no entra al corpus
+
+`/reencuentro/` es una página con **un** `guardar.php`. PHP no está en el corpus y no tiene por qué
+estar: lo sirve **`php-fpm 8.5.10` dentro de la instancia `gioser-php`** (ADR 0015), por
+`127.0.0.1:9000`, supervisado por arje. Control contra el original, en los dos sentidos: `POST` con la
+trampa anti-robots → `{"ok":true}` 200 igual que en gioser; `GET` → **405** igual que en gioser.
+
+⚠ **El webroot entra a la jaula EN LA MISMA RUTA** (`/work/www/gioser-web`): el `SCRIPT_FILENAME` de
+FastCGI es una ruta absoluta del anfitrión, y si adentro viviera en otro sitio php-fpm contesta
+«File not found» sin decir cuál. Y la config de php-fpm vive en el ANFITRIÓN
+(`/work/etc/gioser-php/php-fpm.d/`) y entra por concesión: el `upper` de la instancia es caché y
+`recreate` se la llevaría.
+
+#### 🧨 Los padres que inventa bwrap son **0700** — y eso deja la concesión fuera de alcance
+
+El muro real del día, y es general, no de PHP:
+
+ $ curl -X POST https://gioser.net/reencuentro/guardar.php
+ File not found. ← y el fichero ESTÁ, montado, con sus modos correctos
+
+Para montar en `/work/www/gioser-web`, bwrap tiene que crear `/work` y `/work/www` —que la imagen de
+Arch no trae— y **los crea `drwx------ root`**. Adentro somos root, así que todo a mano funciona; pero
+**un servicio que baja de privilegio** (php-fpm a `http`, o cualquier instancia con `run_as`) no puede
+ni atravesarlos. Medido con el control que lo separa de una confusión de permisos del anfitrión: como
+`http`, leer un fichero **de la imagen** funciona y leer el **directorio concedido** da
+`Permission denied`.
+
+⇒ `grants_to_args` crea ahora los ancestros que faltan con `--perms 0755 --dir`, y **sólo los que la
+imagen no trae**: hacerlo sobre `/etc` o `/home` le cambiaría los modos a la imagen. Con test que
+falla a propósito si la condición se rompe (`left: ["/etc","/etc/php"]` contra `right: ["/etc/php"]`).
+Misma familia que el `--perms 1777` que ya hacía falta antes del `--tmpfs /tmp`.
+
+⚠ La caja corre el `takana` del `commit` pineado en `recipes/takana.toml`, así que **allá el arreglo
+todavía no está**: el camino se abrió a mano en el `upper` de la instancia (`chmod 755 /work
+/work/www`). Es exactamente lo que el arreglo vuelve innecesario en el próximo `takana upgrade`.
+
+#### Y el estado de `rust` y de `claude`, que no es el que parece
+
+- **`rust` está en la caja pero no instalado**: `rust-toolchain-bin` está **sellado en el store** y
+ declarado en `perfil.servidor`, y `rustc 1.97.0` corre invocándolo por su ruta del store — pero
+ **no está en el `PATH`** porque la caja se instaló antes de esa declaración. Proyectarlo son 798 M
+ sobre una raíz de **5,8 G con 2,9 G libres**: entra, pero el layout de la imagen (§6.24) merece la
+ revisión primero.
+- **`claude` no está mudado**, y es otro caso de jaula: el binario de gioser es **glibc dinámico**
+ (`/lib64/ld-linux-x86-64.so.2`), o sea el mismo montón que el backend de `sergio`. Lo que pesa no
+ es el binario sino su estado: **6,8 G de `~/.claude`**, de los cuales 5,3 G son `jobs` y 976 M
+ `projects` — y ahí vive la memoria, **indexada POR RUTA del repo**. Mudarlo es una decisión sobre
+ qué se lleva, no una copia.
+
## 7. Reusar los scripts que ya existen, y no escribir de nuevo
Pedido explícito del usuario. El inventario de lo que ya hace el trabajo: