gioser

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 815Rust 56Shell 37Markdown 291229 Sutichaykuna

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>

d52824efebbb0b4eb0b9437640d7cc9e51b75514 · ruwaq Sergio · (1 killa ñawpaq) · tayta b3ec21375 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 6

crates/takana-cli/src/qorpa.rs

2 qillqakuna · +139 −5

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: