3379a1f1709f6dab9d012cbbb46d7e6ace20db45
39
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fdce080d96 |
qorpa D8: sniper sellado al store, con la marca que evita que la cifra mienta
D8 decía que sniper «entra al store por `file_drop`». Dos correcciones, y la primera es de vocabulario: **`file_drop` en hammer es otra cosa** — una operación de `hammer apply` que coloca un fichero en el sistema instalado verificando su hash. No tenía nada que ver con sellar. Lo que sella es lo de siempre, una receta. Queda escrito en el ADR: un término inventado que suena a mecanismo existente manda a buscar el código donde no está. `recipes/steam-runtime-sniper.toml` sella el árbol del runtime (11196 ficheros) pineado por el sha256 que ya estaba verificado. Entra donde Arch y Ubuntu no pueden por una propiedad, no por simpatía: **no muta** —nadie le instala nada adentro— así que el mismo tarball da siempre el mismo árbol y sellarlo es una afirmación verdadera. **La marca: `foreign = true`.** No cambia el build en un byte y **no entra en `hash_inputs`** (describe procedencia, no identidad — hay test). Lo que cambia es contable: `build-state.py` la clasifica `ajeno`, la resta del denominador de las imágenes y la deja fuera del recuento de recetas. Sin eso, sellar un prebuilt habría subido la cifra que todo el mundo lee como «cuánto construimos» — el riesgo que el ADR escribió antes de que existiera la primera instancia. Verificado: sigue diciendo 821 recetas, y aparte `de las ajenas, 1 selladas al store (prebuilt pineado, sin procedencia de fuente)`. Y la diferencia con el otro ajeno: `xwayland` no se hashea (no hay receta, y un hash afirmaría que lo reproducimos); el sellado **sí conserva su hash**, porque está en el store y que un artefacto exista mientras el grafo lo niega sería otra forma de mentir. Comparten el estado, que es lo que protege la cifra. **`hammer qorpa import --from-store <hash>`** lo consume, y ahí está el detalle que hace que valga: la imagen se registra bajo el **sha256 del archivo de upstream**, no bajo el ArtifactHash. Al revés, la imagen del store y la traída con `pull` serían dos imágenes distintas con los mismos bytes y las instancias de dos máquinas dejarían de coincidir — justo lo que el pin existe para evitar. El árbol se **enlaza**: una imagen nunca se escribe (lo que escribe la instancia va a su `upper`), así que compartir inodos con un artefacto sellado y de sólo lectura es correcto por construcción y la imagen cuesta ~0 bytes. La contracara conocida de `.dmerge`: mientras el artefacto siga en el store, borrar la imagen no libera disco; `--copy` lo evita. Licencia `LicenseRef-qorpa-ajena-no-enumerable` a propósito: adentro hay cientos de paquetes Debian y no podemos enumerarlos; vacío se leería como «todavía no la poblamos». SDD 20 lo recoge y afila la distinción: replicarla a nuestras máquinas es lo que ya hace ADR 0013 con las fuentes; publicarla a terceros sigue pidiendo licencia y marca. 29 tests verdes. El sellado en sí corre aparte, esperando el lock de la granja. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QZjGRqvjWij9dew7NVFu4Q |
||
|
|
4a68dfe761 |
qorpa D9: el canal de evidencia, cableado — la única forma de auditar el montón B
De un binario ajeno no hay fuente que leer. Lo único observable es lo que el
kernel le NIEGA y anota, y hasta acá ese canal existía en el build pero ninguna
instancia lo abría. `hammer qorpa run <id> --evidence` levanta el lector
(`harkaq-audit`) en el HOST —el audit no está namespaceado— antes de que arranque
la instancia, y al terminar imprime uno de tres estados. Los tres, medidos:
HERMÉTICO 0 denegaciones Y el canario las respalda
IMPURO `touch /usr/INTRUSO; mkdir /opt/INTRUSO` →
fs.make_reg · /usr fs.make_dir · /opt
SIN EVIDENCIA quitándole las capabilities al lector. NO es «limpio»
**El canario es lo que hace que «cero denegaciones» valga algo:** un fichero
donde la política no alcanza; al leerlo, el kernel emite una denegación que
revela el `domain=` de ESTE dominio Landlock, un número que desde fuera no se
adivina. Sin él, `denials=[]` sería el instrumento callado.
**Dos condiciones estructurales, y se FALLA en vez de dar un veredicto vacío:**
con `nesting` no hay Landlock (D9 conflicto 1) ⇒ o anidás o auditás; y sin
`seal_image` la política es `rw /` ⇒ no hay NADA denegable y el veredicto sería
limpio por construcción, no por mérito. No es un defecto de la implementación:
**la evidencia sólo existe donde algo puede ser negado.**
**Un bug del propio instrumento, que sólo salió usándolo:** sin CAP_AUDIT_READ
el kernel RESPONDE que no (`NLMSG_ERROR`/EPERM) y el lector ignoraba esa
respuesta esperando una que no iba a llegar — 8 s por consulta, 16 s en su
compuerta. Como `qorpa run` lo despierta al terminar, moría por señal dentro de
la compuerta **sin emitir nada**: un «no» tardío se parecía demasiado a un
cuelgue. Ahora atiende el NLMSG_ERROR y dice su motivo en 2 s. Y si aun así el
veredicto sale vacío, se reporta con el código de salida del lector, que es el
único dato que queda.
Guardián: `scripts/qorpa/evidence-probe.sh`, con las tres aserciones. La 2 es la
que sostiene a la 1 — sin algo que TIENE que salir sucio, «HERMÉTICO» lo cumple
igual un canal muerto. Comprueba también las capabilities del lector, que **se
pierden en cada recompilación** y son la forma más probable de que el canal
muera en silencio.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QZjGRqvjWij9dew7NVFu4Q
|
||
|
|
50d1dbcc31 |
qorpa D3: packages se instala solo — el manifiesto deja de ser un adorno
Hasta acá el ADR afirmaba «el manifiesto es la verdad, el upper es caché» mientras `recreate` confesaba en su propia salida que «instalarlos todavía es a mano». Con eso el `upper` SÍ era el activo: un blob irreemplazable, que es justo lo que hammer existe para no tener. `hammer qorpa provision <id>` instala lo declarado, y **`recreate` lo llama solo** (`--no-provision` para saltarlo). Cuatro decisiones, cada una con su porqué: - **El gestor se DETECTA** en la vista merged (apt, pacman, dnf, apk), no se configura: cada imagen trae el suyo. Y no se multiplexa detrás de un comando único —el `pmm` de Bedrock que el ADR rechaza—: se elige cuál correr. - **`provision` ensancha la política y lo dice en la cara.** Instalar pide las tres cosas que una instancia bien declarada no tiene: red, root y la imagen sin sellar. Se ensancha SÓLO durante esa operación, el manifiesto no se toca y el siguiente `run` vuelve a lo escrito. En silencio sería lo que D7 prohíbe. - **El registro vive FUERA del `upper`** (`provisioned.toml`): dentro se iría con la capa. Por eso `recreate` lo borra — un registro que afirma paquetes sobre una capa recién vaciada es la forma más pura del error de la regla 3. - **Los nombres se validan y se comillan**: salen de un fichero que escribe una persona, así que `strace; rm -rf /` no llega al guión. **Las dos manías que sólo salen provisionando de verdad:** el bootstrap de Arch trae la mirrorlist ENTERA comentada (pacman muere con «no servers configured») y el llavero sin inicializar (toda firma inválida). El guión pone el mirror geo oficial avisando cuál, y hace `pacman-key --init && --populate` sólo si falta. `apt` no necesita ni un workaround: es el dividendo del rango de subuid. Probado de punta a punta en los dos gestores —apt sobre Ubuntu base, pacman sobre el bootstrap de Arch—: instalan, el binario corre después con la red apagada, y un `recreate` tira la capa y la deja igual. 27 tests verdes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QZjGRqvjWij9dew7NVFu4Q |
||
|
|
74fec60281 |
qorpa §Orden 6: pressure-vessel ANIDA bajo la jaula — sin cuenta, sin juego y sin pantalla
El paso 6 quedaba parcial por esta frase: «pressure-vessel con un juego real sigue sin ejercitarse», porque el runtime sniper sólo se baja al instalar un juego y eso pide credenciales. Era un techo falso: el depot está publicado y ahora pineado, así que se coloca a mano y la pregunta que ordenaba D6 se responde entera. **La evidencia**, dentro de una instancia qorpa sobre Ubuntu base, con `nesting` y la jaula puesta: os-release Ubuntu 24.04.3 LTS → Steam Runtime 3 (sniper) ns de montaje mnt:[4026532468] → mnt:[4026532526] /usr/lib/x86_64-linux-gnu → 675 libs de sniper, libSDL2 incluida Contenedor de Valve anidado dentro del nuestro, con el runtime real adentro. **Y la concesión resultó de verdad:** con `nesting = false` el mismo comando muere en `bwrap: Creating new namespace failed: Operation not permitted`. Queda como guardián (`scripts/qorpa/pressure-vessel-probe.sh`), que exige las DOS mitades — sin la negativa, «no salió sniper» lo cumpliría también un cuelgue. Tres cicatrices del camino, todas en los comentarios: 1. **`ldd --version` es la comprobación equivocada** y es la primera que uno escribe: pressure-vessel importa la libc del host cuando es más nueva que la del runtime, así que ver la glibc de afuera adentro es lo correcto y no prueba nada. El veredicto es `os-release`. 2. **`SALIDA=$(timeout … qorpa run …)` se cuelga para siempre** aunque timeout mate al hijo: la sustitución no espera al PROCESO, espera a que se cierre el PIPE, y pressure-vessel deja descendientes con el fd abierto. A fichero termina y devuelve su código. 3. **Dos `run` seguidos sobre la misma instancia fallaban** con `Can't make overlay mount … Device or resource busy`. El kernel niega dos overlays vivos con el mismo `upper` porque eso corrompe la capa — o sea que el EBUSY es un guardián correcto a destiempo: la corrida anterior ya devolvió el prompt y su namespace no terminó de reaparse. `run` reintenta acotado y, si sigue tomado, dice la causa en vez de soltar el mensaje crudo de bwrap. El punto 3 salió porque el guardián exige evidencia POSITIVA de la denegación: con «no apareció sniper» habría dado OK tapando un fallo distinto. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QZjGRqvjWij9dew7NVFu4Q |
||
|
|
3f560de8af |
qorpa D8: el pin de sniper verificado, y el pin deja de ser una cita
Cierra el ⚠ que quedaba en la tabla de D8. Tres cosas, y la del medio es la que más duele. 1. **Sniper pineado, y por la versión correcta.** No se pinea «la última» sino la que `latest-container-runtime-depot.txt` dice que despliega el cliente de Steam — 3.0.20260805.254768. Pinear otra sería pinear algo que nadie corre. Se pinean DOS artefactos: la imagen (rootfs, 302 MB, 11196 ficheros) y el depot `SteamLinuxRuntime_sniper.tar.xz` con pressure-vessel adentro. El segundo es el que destraba el paso 6: ese runtime sólo se baja al instalar un juego (credenciales), y pineado se coloca a mano ⇒ pressure-vessel se puede ejercitar sin cuenta, sin juego y sin pantalla. 2. **Los digests estaban en prosa y ABREVIADOS, y eso no es un pin.** Cuando la poda se llevó las imágenes, `895661bd…` no alcanzó para volver a traerlas: hubo que ir a buscar los sha256 otra vez a upstream. Ahora enteros y verificados en `docs/state/qorpa-imagenes.toml`, con de qué lista salieron y **si esa lista está firmada** — que no todas: Ubuntu firma su SHA256SUMS, Arch firma el tarball pero no la lista, y Valve no firma nada. 3. **`--retry` de curl no cubría el fallo que de verdad pasa.** Los 302 MB de sniper murieron al 73% con `HTTP/2 INTERNAL_ERROR` y curl NO reintentó: sin `--retry-all-errors` sólo considera transitorios los timeouts y los 5xx. Y aun reintentando, sin `-C -` cada intento vuelve a empezar de cero. Con las dos banderas el mismo pull sobrevivió dos cortes más y llegó. El parcial ya no se borra al fallar (es lo que permite reanudar); es seguro porque quien decide es el sha256 de después, y `prune` ya lo barre. De paso, dos comprobaciones en vez de suposiciones: el rootfs de sniper viene en `files/` con un hermano `metadata` y el anclaje por estructura lo elevó solo (tercera forma real de empaquetado), y traer el depot como rootfs FALLA en vez de adivinar («no encuentro un rootfs en el archivo»). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QZjGRqvjWij9dew7NVFu4Q |
||
|
|
ce76796b9b |
qorpa D11: el proxy de Wayland — el socket deja de ser un cheque en blanco
Paso 8 del ADR 0015, el que el propio ADR llamaba el más valioso, y cierra su §NO-resuelve 1. Pasar el socket de Wayland crudo NO es abrir un caño: es un borde de privilegio. El compositor anuncia TODOS sus globals, y entre ellos hay protocolos que hacen cosas que nadie concedió — screencopy captura la pantalla, virtual_keyboard sintetiza teclas, data_control lee el portapapeles sin que nadie copie, y layer_shell dibuja encima de todo. Una instancia ajena con el socket crudo podía las cuatro, y lo único que teníamos era un aviso. El proxy relaya todo salvo dos mensajes: - `wl_registry.global` del compositor: si el interface no está en la lista, no se reenvía y el cliente nunca se entera de que existe. - `wl_registry.bind` del cliente: si el `name` es uno de los ocultos, se manda un wl_display.error y se corta. Sin esto el filtro sería decorativo — el `name` es un número y se puede adivinar. LISTA BLANCA, al revés que la denylist de syscalls de harkaq, y la asimetría tiene razón: el conjunto PELIGROSO de Wayland crece —cada compositor inventa protocolos privilegiados— mientras que el NECESARIO para dibujar una ventana es corto y estable. Una denylist estaría desactualizada el día que alguien actualice el compositor, y sin un solo error visible. La lista incluye a propósito lo que los juegos piden y suele olvidarse: pointer_constraints, relative_pointer, tearing_control, idle_inhibit. Lo que hace esto viable en un fichero: ningún mensaje que se descarta lleva descriptores, así que los fds —que Wayland pasa por SCM_RIGHTS para wl_shm y dmabuf, y sin los cuales no se dibuja nada— se relayan en orden de llegada sin tener que asociarlos a su mensaje. PROBADO SIN COMPOSITOR, con uno falso que anuncia siete globals (tres permitidos y cuatro peligrosos) y un cliente que cuenta lo que ve: pasan exactamente los tres, y el intento de bindear por número el name que nunca se anunció corta la conexión. Es la clase de prueba que no necesita GPU y responde la pregunta entera. Un test más cubre el `size` menor que la cabecera, que haría avanzar 0 bytes y colgar el proxy en un bucle. El socket crudo sigue disponible con `wayland_raw`, apagado por defecto y avisando a gritos. Y si no hay compositor, `run` FALLA en vez de degradar. Las features `socket`/`uio` de nix son aditivas: no cambian lo que compilan los demás crates. 54/54. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2 |
||
|
|
d77ad15d01 |
qorpa prune: la poda nace con el subsistema, no después
Paso 7 del ADR 0015, sus dos mitades. PODA. Un rootfs son cientos de MB o varios GB y no lo alcanzan ni store-gc ni la caché .dmerge: es un tercer montón sin dueño, como ya lo fue work/sources. `hammer qorpa prune` barre restos de pulls a medias, imágenes que ninguna instancia usa —se re-traen por digest, que es justo la propiedad que da el pin— y, con --upper, las capas mutables, que por D3 siempre se pueden tirar. Con las dos cicatrices del repo cableadas: - SIN --yes es un simulacro. Y tras borrar COMPRUEBA que el directorio se fue, porque store-gc reportaba borrados que no ocurrían y eso se descubrió tarde. Si dice que sí y sigue ahí, sale ≠0 diciendo que el número de arriba no es lo que se liberó. - El tamaño de un árbol con directorios ilegibles sale MENOR de lo que es: un upper con ficheros de los subuid no se puede recorrer entero. Ahora se cuentan los directorios ciegos y la cifra se marca con `≥`. Un número silenciosamente bajo es peor que ninguno cuando con él se decide borrar. Y --upper deja la instancia USABLE: sin upper/ y work/ no vuelve a arrancar. LICENCIAS (SDD 20). Las imágenes ajenas quedan fuera del catálogo publicable y del reporte de licencias por escrito, y la razón no es pereza: no podemos enumerarlas — un `pacman -S` dentro de una instancia trae paquetes que nadie declaró acá, y afirmar una licencia sobre eso sería inventarla. Lo que sí se hace: contarlas aparte en clase `ajeno` (el riesgo real del ADR es que en seis meses alguien las cuente como corpus), y dejar dicho que si algún día se espejan hay que mirar licencia Y MARCA antes, igual que con Firefox. Más el corolario que faltaba escribir donde se lea: el claim «hammer reproduce bit a bit» hay que acotarlo desde el día que exista una instancia. 2 tests nuevos (la poda no toca una imagen en uso y sí los restos; con --upper la capa se va pero la instancia queda usable). 50/50. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2 |
||
|
|
1d9ddcee37 |
qorpa D10: Steam en la mano, y los tres muros que sólo se ven así
Paso 6 del ADR 0015, PARCIAL y dicho como parcial. Steam 1.0.0.87 instalado de verdad desde el multilib de Arch —las libs de 32 bits que la F2 del plan de juegos daba por «una campaña entera»—, su cliente i386 bajado y desempacado por el bootstrap de Valve junto con el Steam Runtime, y el BWRAP ANIDADO VERIFICADO EXPLÍCITAMENTE (24 montajes propios dentro de la instancia), que es lo que este paso pedía. Lo que NO se pudo: la máquina no tiene sesión gráfica, y el runtime sniper sólo se baja al instalar un juego, que exige credenciales ⇒ pressure-vessel con un juego real sigue sin ejercitarse. Se dice, no se insinúa. Tres muros, ninguno en el ADR: 1. LA JAULA MATABA TODOS LOS BINARIOS DE 32 BITS. El filtro seccomp comprueba arch == x86_64 y MATA lo que no lo sea; para un build es la defensa clásica y correcta, para el montón B es fatal porque el cliente de Steam es un ELF i386. El síntoma fue `ldd: exited with unknown exit code (159)` = 128+31 = SIGSYS, que no se parece en nada a la causa. La salida no es aflojar el check sino darle a i386 su propia tabla con la MISMA política. Los 25 números se verificaron uno por uno contra /usr/include/asm/unistd_32.h: 24 bien y UNO MAL — kexec_file_load no existe en i386 y su número de x86_64 (320) es ahí `utimensat`, o sea que habríamos denegado algo que usa cualquier cosa que toque una marca de tiempo. Es la diferencia entre una tabla de memoria y una verificada. 2. STEAM SE NIEGA A CORRER COMO ROOT, y cambiar el mapa para evitarlo CORROMPE la instancia: un fichero creado bajo un mapa aparece con otro uid bajo el otro, así que el useradd de la preparación deja un /home que su propio dueño no puede escribir. ⇒ el mapa es parte de la IDENTIDAD de la instancia. La vía correcta es la de cualquier runtime de contenedores: un solo mapa y se BAJA de privilegio adentro — campo `run_as`, setpriv, con el CAP_SETUID que ya tenemos en el namespace. 3. EL XDG_RUNTIME_DIR ES DEL USUARIO QUE CORRE, no del uid del mapa: con `run_as`, apuntarlo al de root deja al Steam Runtime sin poder crear su temporal. Se lee como un aviso menor hasta que algo deja de andar sin decir por qué. Lo que sí quedó probado, y es el corazón del ADR: un userland glibc ajeno con su cadena de 32 bits completa corre enjaulado sobre nuestro kernel, con seccomp y no_new_privs puestos, y un contenedor anidado funciona adentro — la forma exacta en que Valve prueba Proton. 1 test nuevo (run_as resuelve uid/gid/home del passwd de la imagen, y un run_as inexistente NO cae a root). 48/48. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2 |
||
|
|
f64859bade |
qorpa export: shims generados, y la clase ajeno para que nadie los cuente mal
Paso 5 del ADR 0015, sus dos mitades. SHIMS. `hammer qorpa export <id>` genera lanzadores finos y `.desktop` en el espacio del host, desde lo DECLARADO en `[export]` — nunca todo: exportar todo haría que el `ls` de la imagen compita con el nuestro, que es la falla de Bedrock (arbitra en tiempo de exec, por heurística). Gana en tres cosas contra un FUSE: cero costo en runtime, `cat` al shim y ves qué hace, y se revoca borrándolos. Se GENERAN, no se copian. El `.desktop` se arma con lista BLANCA de claves, así que `Exec`, `TryExec`, `Path` y `DBusActivatable` quedan fuera por definición y no por enumeración — una lista negra dejaría entrar la próxima clave ejecutable que invente el estándar. El Exec original se cita en un comentario del fichero generado, para que se vea qué decía y qué no se copió. El icono se busca en la vista merged (upper primero, imagen después: si no, se perdería lo que instaló el gestor de paquetes) y se copia al host, porque un icono que el host no resuelve se ve como un cuadrito gris. Y `exported.json` registra cada fichero escrito, para que `--remove` borre EXACTAMENTE eso y no por patrón sobre el ~/.local/bin de alguien. Probado de punta a punta con un .desktop ajeno real de la imagen de Arch: el shim corre `pacman -Q` del huésped desde el host, el X-KDE-Wayland-Interfaces quedó fuera, el icono viajó, y --remove dejó 0 ficheros con la instancia intacta. CLASE `ajeno`. build-state.py inyecta los nodos declarados en el nuevo docs/state/qorpa-ajenos.toml ANTES que los `wanted`, y ese orden es la mitad del punto: un nodo que provee una imagen ajena no es una receta por escribir. Con eso el `xwayland` de escritorio-kde deja de ser deuda y pasa a contarse aparte: escritorio-kde 187/188 listo falta 1 (raíces 14, + 1 ajenas) Dos decisiones que sostienen esa cifra: los ajenos se RESTAN del denominador (si entraran, el número que se lee como "cuánto construimos" crecería solo cada vez que alguien enjaula una app), y la declaración vive en el REPO y no se lee de /var/lib/hammer — build-state.json se commitea y lo regenera el cron en dos máquinas; si la clase saliera de las instancias instaladas, cada una diría algo distinto y se pisarían en cada cosecha. Es el error que ya se cometió con sealed_remoto. Qué provee una imagen ajena es diseño; qué tenés instalado, no. Un ajeno tampoco se hashea, y no por comodidad: no tiene procedencia de fuente, así que un hash afirmaría que lo reproducimos. 2 tests nuevos (que del .desktop ajeno no sobreviva nada ejecutable; que el shim no se rompa con rutas raras). 47/47. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2 |
||
|
|
89da9c822f |
qorpa: el mapeo por rango — subuid + --userns FD, y el impuesto se paga
Resuelve §NO-resuelve 2 del ADR 0015, que era el ticket que más desbloqueaba. Tres síntomas que parecían distintos —apt sin poder bajar a `_apt`, pacman sin poder chownear a `alpm`, pressure-vessel sin poder escribir su uid_map— eran la misma causa: bwrap crea el userns con UN SOLO id. La cura resultó tener TRES partes, y ninguna sobra: 1. `setcap cap_setuid+ep newuidmap` (+ cap_setgid en newgidmap). shadow.toml los instala pero no los provisiona; sin la capability no escriben el mapa. 2. Crear el userns nosotros, mapear el rango de /etc/subuid con newuidmap y pasárselo a bwrap con `--userns FD`. bwrap crea el suyo con un solo id A PROPÓSITO y nunca llama a newuidmap: el trabajo es de quien lo invoca. El fd lo abre la shell (`exec 3<…`), porque un fd sólo cruza el exec si no es CLOEXEC y no valía la pena una dep de C para un fcntl. 3. Devolver las capabilities DENTRO del namespace. Ésta no estaba en el plan y es la que costó: bwrap las tira todas, y en Linux ser root es tener CAP_SETUID, no tener uid 0. Sin ella apt seguía sin poder seteuid(42) — un síntoma que parecía de subuid y no lo era. Son seguras por construcción: dentro de un userns sólo alcanzan lo que ese namespace posee, o sea nuestros propios subuid. CAP_SYS_ADMIN queda fuera y sigue colgando de `nesting`. MEDIDO después: uid_map de 65537 ids, setgroups: allow, apt instala SIN el APT::Sandbox::User=root, pacman sincroniza con DownloadUser=alpm INTACTO, y el userns anidado monta con root=true ⇒ el conflicto 2 de D9 se disuelve solo. Dos cosas más que salieron por medir, no por pensar: - El guardián MENTÍA. qorpa-preflight envolvía al hijo en `timeout`, que forkea, así que newuidmap apuntaba al PID equivocado y el kernel respondía "Operation not permitted" — un falso negativo idéntico a un fallo real. Decía que subuid no andaba cuando a mano andaba. Ahora sale exit 0. - Quitar el impuesto MUEVE el problema: el upper pasa a contener ficheros de los subuid (apt deja los suyos con uid 165577) que nuestro uid no puede borrar ⇒ recreate entra a un userns mapeado para limpiar. Y cuando no hay rango, se degrada diciendo la causa exacta en vez de quedar en misterio. Y un detalle que no es cosmético: `--perms 1777` antes del `--tmpfs /tmp`, o el _apt al que apt baja no puede escribir su fichero temporal. Un /tmp que no es 1777 no es /tmp. 2 tests nuevos (el rango se lee por usuario; CAP_SYS_ADMIN NO está en las caps de root, o `nesting` dejaría de ser una decisión). 45/45. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2 |
||
|
|
5b82474079 |
qorpa D9: harkaq enjaula la instancia, y midiendo salieron dos conflictos
Paso 4 del ADR 0015. harkaq-exec entra como último eslabón dentro de bwrap, igual que en el sandbox del build. Cruza el borde un binario ESTÁTICO, no una librería, así que D2 sigue en pie: lo único compartido es la ABI del kernel. Honestidad primero, y está escrita en el código: en el eje del sistema de ficheros harkaq casi no agrega nada, porque el namespace de montaje de bwrap ya es una lista blanca. Escribir reglas `ro` que repiten eso sería un sello de goma, así que la política de una instancia no sellada es UNA línea (`rw /`) y no finge. Lo que sí aporta: seccomp (bwrap no instala filtro alguno — hoy una instancia podía io_uring, bpf, ptrace, userfaultfd, keyctl, perf_event_open), no_new_privs, el canal de evidencia, y `seal_image`, que congela /usr /bin /lib /opt aunque adentro seas root. Verificado contra el kernel, no contra el log: Landlock ABI 9, logging post-exec ON, NoNewPrivs 1, Seccomp 2. Y sellando, `/usr/bin` y `/bin` denegados mientras /etc y /var siguen escribibles. DOS CONFLICTOS que sólo se ven midiendo, y ninguno estaba en el ADR: 1. Landlock y los contenedores anidados son INCOMPATIBLES hoy: con un dominio activo, `mount` falla con EACCES aunque seccomp lo permita — el kernel no admite montajes nuevos bajo un dominio porque escaparían de sus reglas por-ruta. ⇒ pressure-vessel no arranca bajo Landlock. Por eso `nesting` pasa `--allow-nesting --no-landlock` y lo dice a gritos; seccomp y no_new_privs siguen puestos, que es lo que más pesa con un binario ajeno. 2. `root` adentro y anidar se pelean: con --uid 0, un userns anidado no puede escribir su uid_map. Sin remapear anida, pero el gestor de paquetes se queja. La instancia de juegos y la de paquetes quieren mapeos OPUESTOS, y ahora el manifiesto lo declara (`root`, encendido por defecto). Las dos mitades se curan con lo mismo que el impuesto de apt: un rango real de subuid con newuidmap + --userns FD. Ése es el ticket que más desbloquea. En harkaq-exec, dos flags ADITIVOS y apagados por defecto (--allow-nesting, --no-landlock): el camino del build no cambia ni un byte, que es requisito duro con 700+ artefactos sellados. La lista de syscalls del anidamiento se separó de la base y el _Static_assert del techo de salto BPF ahora suma las dos. 3 tests nuevos: que la política sin sellar no finja, que sellando el `rw /` no sobreviva (uniría derechos por ancestro y anularía el sellado), y que `root` sea lo único que nace encendido. 43/43. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2 |
||
|
|
ba09fdb403 |
qorpa create/recreate/run: la instancia nace sin ver nada
Paso 3 del ADR 0015. El overlay lo monta bwrap dentro de su propio namespace (--overlay-src + --overlay), así que no hace falta root ni se monta nada en el host. `run` entra con --clearenv y con --unshare-net salvo que se declare `network`: el entorno del host TAMBIÉN es una concesión, y lo que no se declara no entra (D2/D7). `--dry-run` imprime el bwrap entero, una línea por concesión, porque una jaula que no se puede leer no se puede auditar. `list` ahora enumera también las instancias con lo que abre cada una — una instancia sin política y una con la pantalla abierta se ven IGUAL desde fuera y no son lo mismo. Los campos del manifiesto van en inglés (regla 4); el ADR los tenía en castellano y quedan corregidos, igual que las rutas images/ e instances/. D3 VALIDADO en la mano, no en el papel: la escritura va al upper, la imagen base no se toca, y `recreate` tira la capa y la instancia sigue siendo la misma. Y se midió la otra mitad del paso 1, que el ADR daba por «lo primero que va a fallar». Falla, sí, pero el veredicto es MEJOR de lo que decía: uid_map: 0 1001 1 · setgroups: deny - apt: el método http hace setgroups para bajar a _apt ⇒ para. Salteándolo con -o APT::Sandbox::User=root baja 34 MB, instala y corre los triggers de dpkg enteros; el único residuo es un AVISO de chown a root:adm. - pacman: chownea el directorio de descarga a `alpm` ⇒ para en duro. Con DownloadUser comentado sincroniza, y tras pacman-key --init/--populate instala y el binario corre. ⇒ subuid no es un muro, es un IMPUESTO: un solo id alcanza para instalar paquetes reales en los dos gestores, y lo que rompe es el chown/setgroups a OTRO id, que cada gestor hace en un sitio distinto. Y quitarlo pide algo que el ADR no decía: bwrap crea el userns con un solo id A PROPÓSITO y no llama a newuidmap, así que además del setcap hay que crear el namespace aparte, mapear el rango y pasárselo con --userns FD. 5 tests nuevos (nace sin concesiones, rechaza imagen vacía, el upper es caché, sin grants la red queda fuera, y que el aviso de wayland no sea tibio). 40/40. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2 |
||
|
|
fe155cf20d |
qorpa pull/list: la imagen ajena entra verificada, o no entra
Paso 2 del §Orden de trabajo del ADR 0015. Verbos en inglés (regla 4); el ADR decía traer/crear/correr y queda corregido, con una línea que dice por qué para que no se vuelva a proponer. `hammer qorpa pull <url> --sha256 <sha>` baja, VERIFICA y recién entonces desempaca — nunca al revés: un tar ajeno sin verificar es código ajeno que ya escribió en tu disco. Veredicto de ADR 0014: contenido distinto ⇒ ABORTAR, y no queda nada a medias. La identidad es el sha256 del ARCHIVO, no del árbol, así que la URL es informativa y espejar sale gratis (ADR 0013). `list` marca a gritos las imágenes vacías y sale ≠0 (regla 3). Los pasos 3-7 están declarados en la superficie y fallan diciendo a qué paso del ADR pertenecen. Nada de esto toca el store: es el espacio paralelo /var/lib/hammer/qorpa (D1). PROBADO de punta a punta contra las dos imágenes curadas — Ubuntu base 24.04.3 (2760 ficheros, 78 M) y Arch bootstrap 2026.09.01 (31748, 534 M), las dos con su glibc adentro, que es el montón B entero. Y probarlo de verdad destapó tres cosas que en verde no se ven: 1. `-p` sin `--delay-directory-restore` NO extrae un rootfs real sin ser root: /etc/ca-certificates/extracted/cadir es 0555 y tar lo crea con su modo final ANTES de llenarlo. 2. Mi limpieza mentía: `remove_dir_all().ok()` no puede con un árbol que trae directorios de sólo-lectura, así que el staging de un pull roto SOBREVIVÍA y el siguiente pull extraía encima. El síntoma («Permission denied» en un directorio recién creado) no se parece en nada a la causa. 3. Renombrar un DIRECTORIO exige escritura sobre el directorio mismo, y el root.x86_64 de Arch viene dr-xr-xr-x. Se abre, se mueve y se le devuelve su modo exacto. Y una regla que sonaba razonable y era falsa: «si hay un solo directorio arriba, ése es el rootfs». El bootstrap de Arch trae TRES entradas arriba (root.x86_64, version, pkglist) ⇒ no disparaba y el rootfs quedaba un nivel abajo, con todo verde y sin un error. Ahora se ancla por ESTRUCTURA (tiene etc/ y usr|bin), con --subdir como escape, y si no acierta FALLA en vez de adivinar: un rootfs mal anclado no rompe acá, rompe cuando la instancia no encuentra su loader. Los hermanos descartados quedan escritos en el manifiesto, no tirados en silencio. 5 tests nuevos, incluida la cicatriz de Arch. 35/35 en hammer-cli. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2 |
||
|
|
d8778e4dd9 |
qorpa D8: el pin fija el suelo, no la instancia; y la terna curada
Faltaba decir en el ADR lo que se confunde solo: "pinear" no significa que la imagen no se actualice. El digest es la IDENTIDAD, igual que en el rootfs del lab. Lo que se pinea es el suelo; lo que instalás adentro con dnf/pacman ni está pineado ni puede estarlo, y se actualiza normal. Subir la base de versión es barato PRECISAMENTE por D3: manifiesto = verdad, upper = caché ⇒ cambiar el digest y recrear. Y el riesgo del pin (que upstream borre el tarball) ya lo resolvió ADR 0013: la URL no entra en la identidad, sólo el sha256, así que espejar es gratis. Curaduría decidida con el usuario — TRES, cada una por un trabajo distinto: - Arch bootstrap (juegos: multilib 32-bit y SteamOS es Arch ⇒ extiende D6), - Ubuntu base LTS (binarios comerciales: es contra lo que se compilan), - Steam Runtime sniper (la única SELLABLE: inmutable ⇒ file_drop al store). Fedora queda BYO: hace el mismo trabajo que Arch en el slot "fresco" y una tercera cadena mutable es la normalización que §NO-resuelve 3 quiere evitar. Los pines de las dos primeras están VERIFICADOS contra upstream hoy (Arch 2026.09.01 sha 895661bd…, Ubuntu 24.04.3 sha 6bc2cde3…); el de sniper NO, y se dice que no en vez de suponerlo. Y queda anotado que hacerlas compartibles más adelante no pide diseño nuevo: una imagen pineada es cuerpo inmutable direccionado por contenido ⇒ ADR 0014 se le aplica tal cual. Lo único que hay que mirar antes de publicarlas a terceros es licencia y marca, que no es una pregunta técnica. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2 |
||
|
|
aa5bb67fd7 |
qorpa: nombre adoptado y el preflight que mide si una máquina puede alojar
ADR 0015 pasa de "propuesta de nombre" a `qorpa` adoptado: la frontera es
`hammer qorpa {…}` y el espacio de nombres se unifica en
/var/lib/hammer/qorpa/{imagenes,instancias}/ — un solo árbol, para que la poda
de §NO-resuelve 5 tenga un único sitio que barrer. La clase de nodo del grafo
sigue siendo `ajeno`: describe la procedencia, no el subsistema.
Y arranca el §Orden de trabajo 1 (subuid) como GUARDIÁN en vez de a mano:
scripts/qorpa/qorpa-preflight.sh mide las cinco capacidades de entorno que un
huésped necesita y que no están en ningún grafo — userns sin privilegios (+
anidado), mapeo multi-id, overlayfs sin root, los nodos del borde y disco.
Tres niveles (BLOQUEA/LIMITA/NOTA) y salida 0/1/2, porque "arranca pero sin
dnf" es una respuesta legítima, no un error.
Medido en `momento` (exit 2, 2 limitaciones):
- userns ANIDADO funciona ⇒ el "verificar, no asumir" de D6 (pressure-vessel
creando su userns dentro del nuestro) queda verificado a nivel de primitiva.
- subuid es papel mojado acá: el rango está declarado en /etc/subuid y las
herramientas están, pero newuidmap/newgidmap vienen sin setuid y sin
capability ⇒ no pueden escribir el uid_map. Es exactamente la "primera cosa
que va a fallar" del ADR, y resulta ser de PROVISIÓN, no de kernel.
El guardián ya se corrigió a sí mismo una vez: marcaba LIMITA por
CONFIG_OVERLAY_FS=m mientras tres secciones más abajo el overlay montaba de
verdad. Manda la prueba funcional, no la declarada.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2
|
||
|
|
776692d01a |
ADR 0015 (propuesto): imágenes ajenas — el mundo glibc entra enjaulado y no entra al store
Decide la frontera antes de escribir código. Sale de una medición incómoda: el
corpus tiene cuatro escritorios que cierran y CERO navegador, ofimática,
reproductor o editor de imagen. Al partir el «qué falta» por causa, la
intersección de «sólo X11» con «compilable desde fuente en musl» es casi vacía:
casi todo lo que se pierde por Wayland ya estaba perdido por la libc. El montón
que duele son binarios ajenos que nadie va a recompilar.
Las siete decisiones:
D1 — Una imagen ajena NO es un artefacto y no vive en el store. El store promete
reconstrucción bit a bit desde fuente; un rootfs de Fedora no. Meterlo ahí
sería la misma clase de error que el artefacto vacío: algo que se lee como
garantía y no lo es. Namespace paralelo, por digest, fuera de hash_inputs.
D2 — Se cruza el borde con protocolos y nodos de dispositivo, NUNCA con
librerías. Wayland/PipeWire son protocolos; /dev/dri y /dev/ntsync son ABI
de kernel. Mesa va adentro de la imagen. Corolario: la jaula no sabe qué
libc hay adentro, y por eso resuelve el montón entero de una vez.
D3 — El manifiesto es la verdad; el `upper` del overlay es CACHÉ. Misma relación
que receta↔artefacto. De ahí se caen solas la actualización de base (se
recrea, no se rebasea), el respaldo (KB, no GB) y la poda.
D4 — Cuatro granularidades, no una. El runtime curado inmutable (tipo 2) sigue
siendo el preferido cuando alcanza: se sella. El rootfs con dnf existe
porque es justo lo que el tipo 2 no permite.
D5 — Transparencia por shims GENERADOS, no por un FUSE global. Es el poder de
Bedrock sin sus formas: cero costo en runtime, inspeccionable, revocable, y
se exporta lo declarado (Bedrock arbitra en tiempo de exec, con heurísticas).
Los nodos exportados entran al grafo con clase `ajeno` ⇒ no se pueden contar
como corpus. Bedrock no puede decirte qué tenés.
D6 — Steam ya ES un contenedor: se anida pressure-vessel adentro, que es la
configuración que Valve prueba. El bwrap anidado hay que VERIFICARLO.
D7 — Es el único lugar del sistema donde la política se escribe en vez de
derivarse. harkaq deriva `política = clausura(deps)`; una imagen ajena no
tiene clausura declarada. Excepción nombrada y acotada, por defecto vacía.
Y lo que el ADR admite que NO resuelve, escrito para no descubrirlo en producción:
el socket de Wayland es un borde de privilegio y lo pasamos crudo (screencopy y
virtual-keyboard incluidos — Flatpak pasa un proxy filtrante, nosotros no lo
tenemos); el UID mapping va a fallar primero y las piezas ya están en el corpus
(shadow instala newuidmap/newgidmap y crea /etc/subuid vacío, falta
provisionarlo); es una segunda cadena de suministro sin garantías; y hay que
acotar por escrito el claim de bit-repro o la cultura de números honestos se
erosiona sola.
Se cae gratis: `xwayland` deja de ser deuda del corpus (va DENTRO de la imagen,
que ya lo trae, y se cuelga de kwin por el socket) ⇒ el wanted de KDE se
disolvería sin escribir la receta y sin tocar Wayland-only. GIMP e Inkscape dejan
de reabrir la deuda GTK3. Y del plan de juegos: F2 (glibc+multilib desde fuente,
«una campaña entera») queda CANCELADA y F0 (flatpak+ostree al catálogo)
innecesaria.
Lo nativo no se afloja: el montón A se sigue construyendo, en orden mpv → OBS →
Firefox.
Toca sólo documentación: el ADR nuevo, la nota de generalización en
plan-jaula-juegos.md §Capa 3, y el comentario en targets.toml que evita que
alguien escriba la receta de xwayland sin ver la decisión pendiente. Cero recetas
tocadas, cero re-hasheo: --kde sigue en 978 sealed / 1 wanted / 171-171, CIERRA.
|
||
|
|
8b2ff3cfd4 |
digest: blake3 pelado, no length-prefijado — el mismo archivo tenía dos hex
Lo destapó escribir la contraparte del ADR 0014 para churay: churay direcciona sus blobs con blake3(bytes) pelado y hammer iba a sellar el digest con of_inputs(&[bytes]), los dos bajo el prefijo `b3:`. El mismo archivo con dos hex distintos, y un CAS compartido que no falla ruidosamente: cada lado busca un nombre distinto para los mismos bytes. Con una sola entrada el length-prefijado no desambigua ninguna concatenación — sólo hace que el nombre deje de ser verificable por un tercero con b3sum en la mano. `ArtifactHash::of_bytes` YA EXISTÍA y ya era blake3 pelado: es la convención de of_file y la del expected_hash de un .swm, así que of_inputs era además la pieza fuera de sitio dentro del propio repo. of_inputs se queda para lo que fue escrito: hashear una LISTA. Coste: una línea, porque ningún índice publicado lleva todavía el campo. Es el argumento del ADR aplicado a sí mismo — decidir antes de que haya usuarios. La corrección queda en el ADR, no reescrita en silencio. Guardián: digest_es_blake3_pelado_y_no_length_prefijado, con vector fijo de b3sum y un assert_ne contra of_inputs que nombra la consecuencia. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CK6HpSoHcN9M4GBpqRSusR |
||
|
|
48b3c5be3f |
ADR 0014: distribución multi-origen — el origen no necesita confianza
Se decide ahora, antes de tener usuarios, porque toca el formato del índice y el comportamiento de clientes instalados en máquinas ajenas: después de la primera descarga pública el cambio rompe a alguien o se arrastran dos formatos para siempre. La decisión entera cuelga de una asimetría medida: ~51 G de cuerpo inmutable y verificable por contenido, contra KB de raíz mutable que es lo único que necesita ser fresco y auténtico. El cuerpo puede servirse desde cualquier host, incluso hostil ⇒ conseguir espejos deja de ser un problema de infraestructura. Cloudflare R2 como primer origen público por el egress a 0, pero es INSTANCIA, no arquitectura: lo que se decide es que R2 sea una entrada más de una lista. No depender de un servidorcito no se logra contratando un proveedor grande, sino pudiendo perder cualquiera sin enterarse. Regla derivada: nunca menos de dos orígenes en dos proveedores. Verificado de punta a punta con dos orígenes HTTP y un .swm con UN byte cambiado bajo el mismo nombre: origen muerto ⇒ failover; origen manipulado ⇒ aborta y NO cae al bueno de detrás. Sin ese segundo caso la lista sería un mecanismo para tapar espejos mentirosos. Queda pendiente y anotado: el vigía sólo mira upstream, no nuestros orígenes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CK6HpSoHcN9M4GBpqRSusR |
||
|
|
ce69ea23d4 |
ADR 0013: el pin de una receta no siempre es un commit
Documenta los tags anotados de diffutils/findutils-xargs/kustomize y por qué rompían las dos puntas del mirror, y corrige la pregunta abierta: ningún servidor ha negado aún el fetch por sha suelto. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016v9ozVm44p6DB7EMXeZK4o |
||
|
|
276efdeced |
mirror git: el poblador reporta el error real de git, no una conjetura
Imprimía siempre «upstream no da el commit» pasara lo que pasara. En la primera tanda marcó así a diffutils, findutils-xargs y kustomize, cuyos commits se traen a mano sin problema: era transitorio. Ahora bundle() devuelve (ruta, motivo) con el stderr del paso que falló. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016v9ozVm44p6DB7EMXeZK4o |
||
|
|
8ab5040a8d |
ADR 0013: mirror de fuentes git — bundles shallow por commit
Los 606 repos por commit son el 52% de las fuentes y el mirror de tarballs no los cubría. Se espejan
como `hammer/fuentes-git/{commit}.bundle` — 571 commits distintos, porque hay commits compartidos
entre colas y se espeja uno solo.
La identidad es el commit y la verificación la hace git: al desempaquetar comprueba cada objeto
contra su SHA, así que un bundle alterado no pasa. No hace falta índice ni sha256 aparte.
SHALLOW, NO CLONES COMPLETOS. El bundle sale de un `fetch --depth 1` del commit exacto: para `act`
son 9,3 MB en vez del repo entero, y con 571 fuentes eso decide si el mirror cabe. Es legítimo
porque hammer NUNCA usa la historia — lo único que hace con un repo es `git archive <commit> |
tar -x`, materializar un árbol.
EL DETALLE QUE COSTÓ ENCONTRAR. Un bundle hecho desde un repo shallow no lleva la frontera de
historia, y al desempaquetarlo git aborta con «Failed to traverse parents … did not send all
necessary objects». El mensaje dice que faltan objetos y es ENGAÑOSO: llegan enteros —`git archive`
ya funciona pese al error—; lo que falta es decirle a git dónde termina la historia. Se escribe el
propio commit en `<destino>/shallow` antes del fetch. No hay nada que transportar: la frontera de un
`--depth 1` es exactamente ese commit.
Y UN BUG PROPIO QUE VALE DOCUMENTAR: se pasaba al `git fetch` la ruta RELATIVA del bundle, y como
`run_git` invoca `git -C <destino>`, git la resolvía dentro de `<destino>`. Como el fallo del mirror
se traga a propósito para caer a upstream, el síntoma salía lejísimos: el build moría con «commit …
no existe en <repo> tras fetch», culpando a upstream de un error de ruta local. Ése es el precio de
que el mirror falle en silencio, y por eso el silencio se paga con comentarios explícitos.
Verificado igual que el de tarballs, con receta EFÍMERA para que no haya cache-hit: commit de `act`
ya espejado + un repo cuyo host no resuelve por DNS. Sin HAMMER_MIRROR_GIT falla en el clone; con
él, sella — y el artefacto trae el README.md real de act.
`cargo test -p hammer-build`: 5/5. Población de los 571 bundles corriendo aparte.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016v9ozVm44p6DB7EMXeZK4o
|
||
|
|
45b95f78b9 |
ADR 0013: mirror de fuentes — la URL es transporte, el sha256 es la identidad
`rsync` (404 de samba.org) y `musl` (musl.libc.org no responde) no se pueden construir hoy, y no por
culpa nuestra. Es el estado estacionario: una distro que construye TODO desde fuente tiene tantos
puntos de fallo como fuentes, y son servidores de terceros que nadie nos prometió mantener.
LA MEDIDA, peor de lo que parecía. Sobre 1167 fuentes (561 tarball + 606 git, 79 hosts):
github.com sostiene 742 — el 64% del corpus depende de UN host. Doce hosts sostienen el 89%. Y 43
hosts sostienen exactamente UNA receta cada uno: ahí es donde muerde el bit-rot lento.
El vigía, en su primera corrida: 8 URLs muertas de 1167. Tres de ellas —busybox, freetype,
freetype-shared— están SELLADAS Y EN USO: son el shell y las fuentes del escritorio que se capturó
hoy. Se salvan sólo porque el tarball sigue en la caché local de esta máquina.
LA URL NUNCA FUE LA IDENTIDAD, y el código ya lo sabía: `hash_inputs` usa `tarball:{sha256}` /
`git:{commit}` y el `..` descarta la URL; la caché se nombra `{sha256}.tar` con un comentario que
dice literalmente que cambiar de mirror no la invalida. Añadir mirrors NO re-hashea NADA. Faltaba el
mecanismo, no el diseño.
Orden: caché local → mirror propio → upstream. El mirror va ANTES, no como rescate: el sha256 se
verifica igual, así que no hay diferencia de contenido posible, y un mirror que sólo se usa cuando
upstream falla es un mirror que nadie prueba — se descubre roto el día que hace falta.
LO QUE HAY QUE HACER BIEN. Un mirror que sirve calladamente lo que upstream perdió convierte un
fallo ruidoso en silencio. Por eso construir y vigilar van SEPARADOS: `hammer build` nunca avisa
(sería ruido en 561 recetas), y `fuentes-vigia.sh` pide cabeceras, escribe
docs/state/fuentes-vigia.json y lo corre el latido. Sin ese contrapeso las URLs se mueren una a una
y el corpus queda irreconstruible con todo en verde — el mismo modo de fallo que dejó el grafo de
wlr 17 días anunciando un 121/121 falso.
⛔ PROHIBIDO cambiar el sha256 para "arreglar" una URL muerta. Es la tentación natural ante un 404 y
no arregla una descarga: cambia lo que la distro construye. Otro sha256 es otro contenido, y la
receta seguiría diciendo `rsync 3.4.4` mientras construye otra cosa.
VERIFICADO DE PUNTA A PUNTA. El primer intento —construir busybox con el mirror puesto— dijo BUILD
OK y NO PROBÓ NADA: cache-hit del artefacto, cero bytes descargados. La prueba válida usa una receta
efímera con un sha256 que sí está en el mirror y una URL que ni resuelve por DNS. Sin HAMMER_MIRROR:
`curl (6) Could not resolve host`. Con él: sella, con el contenido real.
Mirror poblado: 126 objetos en el Storage Box que ya se paga. `cargo test -p hammer-build`: 5/5.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016v9ozVm44p6DB7EMXeZK4o
|
||
|
|
c963b24b5e |
ADR 0012: registrar como PENDIENTE el dilema del árbol de fuentes (caché vs workspace)
Queda una carrera abierta que el lock de script NO cierra: `build-farm.sh` corre `xargs -P2` y dos recetas de la misma cola que compartan dep se pisan igual, porque `fetch` nombra el árbol de forma determinista y sin nada del constructor. El fondo no es "falta un lock", es que el árbol cumple DOS papeles que se contradicen bajo concurrencia: caché direccionada por contenido (clave = nombre+sha, re-extraer es caro) y workspace mutable de build (lib.rs lo parchea, aísla el workspace Cargo y vendorea EN EL SITIO, y después el sandbox lo bind-montea). Por eso un lock alrededor de la extracción parece correcto y NO lo es: un segundo proceso puede borrar un árbol que un bwrap ya está compilando. El ADR deja las tres salidas (lock por árbol durante todo el build / árbol privado por build / separar caché inmutable + copia privada) con su contra concreta cada una —incluida que el worker corre ext4, sin reflink, así que la copia de la opción C es real— y los tres números que habría que medir para elegir en vez de opinar. Se ancla en los dos sitios donde alguien va a chocar: el ADR indexado en docs/README.md (de paso se agregan 0009-0011, que faltaban) y un comentario en el propio `fetch_tarball` que dice explícitamente que no se arregle a medias. Va como PENDIENTE y no decidido a propósito: hay otro agente sobre este repo y esto es lo que evita que la carrera se "arregle" de una forma que parece bien y deja el bug. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d6944b2cd9 |
kde/Plasma: hidratación del ESCRITORIO COMPLETO store-directa (rebuild-free), cierre runtime verificado
- hydrate-from-store.sh: proyecta artefactos SELLADOS del store al FHS vía hammer hydrate, sin reproduce (install-reproduce rebuildеa qtbase por consumidor — hashing frágil bajo régimen dinámico). Selección por-pkg = artefacto más reciente con mtime<hoy (cosecha final coherente de la granja). 137/137 proyectados, 0 faltantes, 1257 .so, 5.0G en kde-store-rootfs. - Hashes elegidos = los sellados de la campaña (kwin 3a674cb6, plasma-workspace 284c862f, libplasma 963403fe, kscreenlocker 55120487, breeze df1d1e68, kio 44376420). - Cierre runtime VERIFICADO completo: todos los NEEDED de plasmashell(30)/kwin_wayland(24)/ startplasma-wayland(11)/krunner(19) + libs core resuelven en el rootfs — cero colgantes. - hydrate-fhs.sh: soporte multi-target (TARGETS) + canon prefiere nombre real qt6-* (el alias fuerza cache-miss+rebuild) + más build-only excluidos. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
08510c0424 |
kde/Plasma: hito H-QML PASADO — hidratación FHS del cierre runtime + reproducer QML
- repo-selfcontain.py: work/repo-kde auto-contenido (alias qt-corto→qt6-* + cierre base desde dist/repo), re-firmado 884 pkgs, repo verify trusted. - hydrate-fhs.sh: hidrata el cierre runtime instalando cada pkg del cierre en el mismo prefix (install --prefix sólo baja el pkg pedido); 32 pkgs → work/kde-rootfs, 455 .so (libQt6Core/Gui/Qml/Quick + QPA offscreen + mesa/xcb/wayland/openssl). - h-qml-run.sh + h-qml-test.cpp: QQmlApplicationEngine carga QtQuick Window+Text, compila zig-cc/musl contra el FHS, corre offscreen rc=0 (engine instancia el árbol QML). - ADR 0011: sección resultado H-QML + deudas (target_bin espurio en libs, cache-miss por re-resolución de deps → qtbase más completo con SSL, fuentes). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
8eb168e168 |
kde/H-Qt: ADR 0011 → gate PASADO; lecciones durables + snapshot para reanudar
Registra el resultado (qtbase sella b3:7c7c747d, app Qt rc=0 offscreen), las 3 lecciones de-Alpinización (wrapper zig/CMake, tensión estático↔dinámico = deuda Capa 0, hidratación runtime musl) y el snapshot hammer-kde-qtbase-2026-07-11 (img 407438750) para arrancar futuras farm-up sin re-pagar el build. Próximo: qtdeclarative/QML (H-QML) + saldar deuda Capa 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
5447c175f9 |
docs: ADR 0011 — roadmap campaña escritorio KDE Plasma 6
Fija la política (capa GUI adopta linking DINÁMICO: Qt carga plugins .so en runtime, incompatible con el static-musl del userland CLI), el orden topológico en 5 capas (sustrato→Qt6→KF6→Plasma→apps), 6 hitos verificables (gate = H-Qt: qtbase + una ventana Qt bajo el régimen dinámico) y la estrategia de granja (cola aislada incoming-kde/, cadencia por capa, qtwebengine diferido). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
c7d1d9cf5c |
arranque-grafo: paso 4 (cero-parpadeo) cerrado en el ADR 0010 (pasos 1–5 ✅)
Validado por captura de framebuffer OVMF-GOP: el kernel metal con deferred- takeover + quiet no toca el framebuffer (Δ=0). Falta sólo el seamless i915 y el metal real. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
72af9a802f |
arranque-grafo: diagnóstico preciso del cero-parpadeo (ADR 0010 paso 4)
Evidencia por captura de framebuffer en OVMF-GOP: el kernel metal pinta todo el log de boot sobre el FB gráfico (simpledrm 160x50). Quitar console=tty0 no alcanza (VT_CONSOLE=y auto-registra); quiet/loglevel por LoadOptions no frena (fbcon redibuja el ring buffer + ignore_loglevel horneado fuerza salida). Fix identificado (requiere rebuild del kernel metal, cambia hash firmado): CONFIG_FRAMEBUFFER_CONSOLE_DEFERRED_TAKEOVER=y + cmdline horneada quiet loglevel=3 vt.global_cursor_default=0 sin earlyprintk/ignore_loglevel (CONFIG_LOGO ya off). Seamless i915 sólo validable en metal Intel real. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
6921dfead3 |
arranque-grafo: validación 2-stage del instalador live EFI en OVMF (ADR 0010 paso 5)
efi-install-test.sh cierra el lazo completo bajo UEFI (hermano de iso-install-test.sh que es BIOS): (1) ISO live EFI=1 INSTALLER=1 AUTO_INSTALL en OVMF con disco en blanco → hammer-install detecta /sys/firmware/efi y toma la rama EFI (MBR+ESP-0xEF) desatendido → HAMMER-INSTALL-OK; (2) el disco instalado bootea SOLO en OVMF → firmware → BOOTX64.EFI → pivote → arje-zero PID1, sin GRUB. VERDE end-to-end. Prueba el instalador live REAL corriendo dentro del live, no sólo sus artefactos. ADR 0010: paso 5 cerrado salvo metal. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
4e00178fc5 |
arranque-grafo: rama EFI-stub en el instalador live (ADR 0010 paso 5)
hammer-live-install.sh detecta /sys/firmware/efi y ramifica a EFI-stub soberano: MBR con ESP tipo 0xEF (UEFI arranca una ESP MBR igual que GPT ⇒ sólo busybox fdisk/mkfs.vfat/mount, sin GPT-tool ni mtools en el live). Arma el mismo initramfs de pivote (findfs LABEL=hammer-root → switch_root) que el builder host-side; /sbin/init = wrapper hammer-recover→arje-zero (el pivote ya montó store/estado por LABEL). La rama BIOS+GRUB queda intacta tras la detección. iso-image.sh INSTALLER=1 bundlea el kernel metal EFI-stub (bzImage-efi) en el payload; el genérico no sirve (no hornea initrd=/rdinit= en la cmdline). MBR+ESP-EF spot-checked en OVMF (firmware → BOOTX64.EFI → pivote → arje-zero). ADR 0010 actualizado: pasos 2 y 5 ✅. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
3c343bee9a |
arranque-grafo: paso 3 del ADR 0010 — grafo de estados + hammer boot
El menú de arranque como navegación del grafo content-addressed de estados (no una lista de kernels). Sube el modelo de generaciones in-place a un grafo navegable y define el contrato de datos con mirada. - hammer_upgrade::boot_graph: BootGraph/BootNode (formato exacto del contrato), build() arma el DAG desde las generaciones (id = of_tree sin b3:, parents = of_tree del padre, Base para la raíz del linaje), nodo Recovery sintético colgando de la viva, emit() atómico a /run/hammer/boot-graph.json. - activate(): resuelve el id content-addressed a una generación y deja el sistema en ese nodo — no-op si ya viva, rollback paso a paso a un ancestro (reusa el rollback E4), recovery = un rollback, error honesto ante un "redo" hacia una generación huérfana (pide re-aplicar el árbol). - CLI `hammer boot graph [--out|--stdout]` y `hammer boot activate <id> [--from-select]` (lee el id que mirada deja en /run/hammer/boot-select). - Tests: DAG + activate a ancestro + recovery + redo-falla; rfc3339 sin deps. Validado e2e por el binario (3 generaciones → grafo → activar → recovery). Cierra el lado `proceso` de SDD 15 §H4 (arrancar = activar un nodo del DAG). mirada ya puede maquetar contra el grafo real (HANDOFF-arranque-grafo.md). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
7e21b0184b |
docs: ADR 0010 — arranque por grafo (menú de boot = navegación del grafo de estados, mirada+hammer)
El menú de arranque soberano (sin systemd-boot, sin GRUB) no es un port de GRUB: es la navegación del grafo content-addressed de estados del sistema, renderizado por mirada sobre KMS y respaldado por las generaciones/DAG de hammer. Cierra el lado 'proceso' de SDD 15 §H4 (arrancar = activar un nodo). Fija la división de labores, el contrato de datos (/run/hammer/boot-graph.json + hammer boot activate), el invariante cero-parpadeo (simpledrm→i915 seamless) y la vía del loader (squashfs+overlay → initrd chico → EFI-stub directo viable). Espejo en tawasuyu/HANDOFF-arranque-grafo.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
b173630e81 |
query: exports:<elf> — procedencia por-símbolo (puente barato de ADR 0009)
Extiende el parser ELF (parse_elf_needed→parse_elf_info): además de DT_NEEDED, recorre .dynsym (DT_SYMTAB) y extrae los símbolos EXPORTADOS (defined + global/weak). Nuevo término `exports:<path>` en el mini-lenguaje de queries; fluye por el bus (query expr) sin cambios. Metadata descriptiva, fuera de hash_inputs (misma disciplina que la evidencia de H1). Refactor: helper vaddr_to_file_off compartido por strtab/symtab + cstr_at. nsyms vía layout convencional (strtab sigue a symtab); best-effort → [] si no se puede acotar con seguridad. Validado end-to-end: 2873 símbolos reales de /lib/libc.so.6 (aguanta glibc y musl). Tests: term-parse + missing-file + host-gated real-.so. Workspace verde (33 suites). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
236aac7c7f |
docs: ADR 0009 código direccionado por contenido (H3a) — registrar visión Unison
Design-doc de la idea 3 (SDD 15 §H3): decide NO reescribir la unidad de compilación. Puente barato = procedencia por-símbolo como metadata (extender el parser ELF parse_elf_needed para emitir exports de .dynsym), fuera de hash_inputs — misma disciplina que la evidencia de H1. El grano fino real (función-por-hash) se difiere a H3b sobre wasm, gateado por el determinismo H2. Marca §H3a ✅ en la frontera. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
989e0d2c7f |
bootstrap: seed como toolchain del sandbox (Lote 3) + decisiones ADR 0008
SeedSpec::toolchain_dir resuelve el dir del compilador dentro de la semilla sellada, listo para que el sandbox lo bindee como /opt/zig; seed_build_config deriva una BuildConfig que cross-compila con el seed pinned, no con el zig del host. Stage 0 sigue siendo ingesta pura: el layout se resuelve al usarlo (zig en la raíz o en el hijo versionado), con error claro si falta o es ambiguo. Decisiones del Lote 4 registradas en ADR 0008 / SDD 11: - coreutils+shell = busybox (balsa desechable; GPLv2 no contamina el final) - init arje diferido (Stage 1 con init mínimo provisional; CRASHED real luego) - layout de semilla: ingesta pura + resolución al usar +5 tests (layouts: raíz / hijo versionado / no-sellada / ambiguo / build_config). Workspace verde. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3d6d25661c |
Track posterior: Stage 0 del bootstrap (crate hammer-bootstrap) + ADR 0008
Primer eslabón ejecutable del bootstrap from-scratch (SDD 11). Stage 0 ingiere un
toolchain semilla pinned al store sin compilarlo:
- SeedSpec{kind,version,url,sha256}. seed_hash() deriva el ArtifactHash de la
identidad pinned (clase+versión+sha256), no de rutas del host ni del url → el
mismo tarball desde otro espejo produce el mismo artefacto.
- stage0(seed, store): descarga (curl, file:// offline-ok), verifica sha256
ANTES de extraer, untar y seal en el store. Idempotente si ya está sellado;
un sha erróneo no sella nada.
- Reusa el lab: hammer_build::download (+ nuevo verify_sha256 público), Store::seal.
Staging bajo el root del store para que el rename de seal sea atómico.
ADR 0008 fija la decisión (3 stages, zig semilla primaria, musl-cross-make
escotilla, ingerir-no-compilar el Stage 0). Renumerado a 0008 porque el 0007 lo
tomó la decisión de adoptar arje como init; SDD 11 y refs alineados con arje
(init supervisado que entrega el CRASHED real, ADR 0007).
4 unit tests (hash por identidad, ingest+seal+readonly, idempotencia, rechazo de
sha) sin red (file://). Workspace 218 verde.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
ea10c9f716 |
docs(adr): 0007 — adoptar arje como init del track posterior
Coordina con tawasuyu/03_ukupacha/arje/PLAN-ATESTACION-Y-HAMMER.md: - arje (init from-scratch ya existente, PID1 + supervisión real) cubre el CRASHED diferido de la Fase 5 en vez de escribir init nuevo. - frontera: PID1 fino, un solo bus (arje-bus + hammer-core::proto), CAS unificado en BLAKE3, confianza en capas (procedencia hammer + atestación arje; el expected_hash del .swm = el blake3 que arje atesta). - caveat: la cage glibc es del mundo hammer/Linux, fuera de PID1. Puntero añadido en docs/10-roadmap.md §Track posterior. |
||
|
|
8bf1623044 |
Scaffold inicial: workspace Rust + SDDs completos
Arranque del proyecto hammer (distro AI-nativa: laboratorio funcional en el sótano, terminal mutable clásica arriba, integración de IA programadora). - Workspace Rust (compila, tests verdes): hammer-core, hammer-build, hammer-cli (bin `hammer`), hammerd. - SDDs 00-10 + 6 ADRs en docs/ con toda la arquitectura. - Esqueletos navegables mapeados a las fases del roadmap; Fase 0/1 listas para implementar el sandbox real. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |