3870ca3566c4b9cec838dfd34a3489f8b3a33ae4
193
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
be383ea485 |
netup: la puerta puede estar FUERA del prefijo — sin ruta on-link la caja queda con IP y sin salida
Medido en la caja `takana` (Hetzner Cloud, hel1) el 2026-09-10. Hetzner entrega la IPv4 así:
inet 2.29.29.217/32 scope global eth0
default via 172.31.1.1 dev eth0
172.31.1.1 dev eth0 scope link <-- ESTA es la que faltaba
La dirección es un /32 y la puerta no pertenece a ninguna red conectada. `add_default_route` mandaba
`RTA_GATEWAY` con scope UNIVERSE y nada más, así que el kernel rechazaba la ruta.
**Y el modo de fallo es el peor**: la máquina arranca, `netup` consigue el lease, se pone la IP,
arje-zero levanta sshd... y no se puede entrar, porque las respuestas no tienen por dónde salir. Un
`ssh` desde fuera da `Connection timed out` y desde dentro no hay nada que se vea roto.
Se agrega `add_onlink_host_route` (dst=/32, scope LINK) y se llama ANTES del default. Es lo que hace
`ip route add <gw> dev <if> scope link`, y es literalmente lo que el propio rescue de Hetzner tiene
en su tabla. No es fatal si falla —un EEXIST no debe tumbar la red—; el default sí lo sigue siendo.
**Control causal**, en un netns con una dummy y una /32:
ip route add default via 172.31.1.1 dev dummy0 => Error: Nexthop has invalid gateway.
ip route add 172.31.1.1 dev dummy0 scope link => ok
ip route add default via 172.31.1.1 dev dummy0 => ACEPTADO
O sea: no es que "ayude", es que era exactamente eso. netup se validaba contra el DHCP de QEMU slirp,
que da un /24 con la puerta dentro — por eso nunca apareció hasta tocar una nube de verdad.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
|
||
|
|
008dd3925e |
renombre: los instaladores pasan a takana-* — y el peligro no era el fichero, era el PROTOCOLO
ADR 0016 los listaba entre los CONGELADOS; el usuario pidió descongelarlos al abrir el SDD 28. La
enmienda queda escrita en el propio ADR, que si no la etiqueta deja de describir el hecho.
`hammer-install.sh` → `takana-install.sh`, `hammer-live-install.sh` → `takana-live-install.sh`,
`hammer-banner.txt` → `takana-banner.txt`, `BRIEFING-hammer.md` → `BRIEFING-takana.md`.
**Lo que hacía caro esto no es el nombre del fichero.** El instalador se inyecta en el ISO como
`/usr/bin/hammer-install` y su éxito se detecta con un `grep` de `HAMMER-INSTALL-OK` desde TRES
scripts de prueba. Renombrar un solo lado los deja casando NADA — sin fallar —, que es literalmente
el modo en que `atribuir-fallos.py` quedó mudo cuando el renombre movió el target de `tracing`.
Se renombraron las dos puntas en el mismo commit (`/usr/bin/takana-install`,
`TAKANA-INSTALL-OK/FAIL`, `TAKANA_INSTALL_*`, `work/takana-install.img`, `/run/takana-install`),
se comprobó por `grep` que no quede ningún token viejo fuera del ADR, y —lo que decide— se CORRIÓ
`install-tui-test.sh`: 4/4 casos verdes.
Las tres `TAKANA_INSTALL_*` caen al nombre viejo (`${TAKANA_INSTALL_X:-${HAMMER_INSTALL_X:-}}`):
el llamador puede ser un ISO anterior al renombre. Misma convención que `TAKANA_ROOT_PW` unas
líneas más arriba en ese mismo script.
NO se tocó `/usr/sbin/hammer-recover` ni su hook de arranque —renombrarlo rompe máquinas YA
INSTALADAS, no el repo—: sobrevive intacto dentro del script renombrado, verificado por conteo
antes y después (8 ocurrencias). Tampoco `hammerd`, `hammer-edit` (su `name` está en la ruta del
store), `/var/lib/hammer`, `HAMMER_LIVE` ni los siete literales de hash.
De paso: `scripts/.hammer-banner.txt.kate-swp` era un swap de editor commiteado por error. Fuera.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
|
||
|
|
e20a5f44d8 |
takana: --takana-bin, con --hammer-bin como alias
Era la ultima superficie de CLI que quedaba con el nombre viejo. Mismo patron que forja: el canonico es el nuevo, el viejo sigue aceptandose como alias. El default pasa a .../release/takana. El DESTINO no se toca: el binario se sigue instalando en /usr/bin/hammer dentro del rootfs del producto, porque ese rootfs se hashea. Moverlo cambia el hash del producto y obliga a rehacer el baseline del selfhost — es etapa 6 y decision aparte, no efecto colateral de renombrar un flag. Verificado en el subcomando correcto (bootstrap builder, no product): las dos formas se aceptan. 605 tests en verde. |
||
|
|
b818f5249f |
takana etapa 5d: comentarios de crates, CLAUDE.md y el skill de granja
205 lineas en 73 ficheros de crates, mas la prosa de CLAUDE.md y del skill, que se me habian quedado afuera de los barridos anteriores (no eran ni recetas ni docs/ ni scripts/). EL BARRIDO ANCHO ESTUVO A UN COMMIT DE ROMPER EL CORPUS ENTERO. El primer intento reescribia los .rs completos, no solo los comentarios. Entre las lineas de codigo que tocaba estaban SIETE etiquetas de separacion de dominio, que son ENTRADA DE HASH: b"hammer-tree-v1" <- el prefijo de ArtifactHash::of_tree (hash.rs:60) b"hammer-seed-v1" la funcion que hashea TODOS los artefactos: b"hammer-stage1-rootfs-v2" cambiarla mueve los 4750 hashes del store b"hammer-product-rootfs-v3" b"hammer-product-attested-v2" b"hammer-builder-rootfs-v1" b"hammer-attest-dev-rootkey-0001!!" <- clave raiz de atestacion, [u8;32] Revertido y rehecho solo sobre comentarios, esquivando ademas las cadenas crudas de Rust (r#"..."#) porque el SYSTEM_PROMPT del traductor tiene lineas que empiezan como comentario. Controles: las 7 etiquetas siguen ahi, el diff toca CERO lineas de codigo, 605 tests en verde y el hash de zlib sigue en b3:dc363f26. La leccion es la misma de toda esta etapa: un literal que parece prosa puede ser entrada de hash, y la unica forma de saberlo es mirar donde se usa. |
||
|
|
1c3e185167 |
takana: las variables de entorno leen las dos formas, gana la nueva
TAKANA_X con caída a HAMMER_X (ADR 0016). No es un sed: estas variables son
contrato de usuario —knobs del instalador, el entorno del worker, mirror-env.sh—
y viven en perfiles de shell y units FUERA del repo, así que un renombre duro no
falla ruidosamente: la variable no aparece, se toma el default y el build se
comporta distinto sin que nada lo diga.
Rust: takana_core::env::{var,var_os} toma el nombre canónico y deriva el viejo
cambiando el prefijo — se le pasa TAKANA_* para que un grep del nombre nuevo
encuentre todas las lecturas. 5 tests, con DOS controles negativos: sin ninguna
de las dos no hay valor, y un nombre sin prefijo no inventa una caída.
Migrados los 17 sitios directos y los indirectos que el grep no mostraba
(bases_de_mirror, las constantes de kernel_cmd, ROOT_ENV de qorpa, env_path de
recover). takana-recover lleva la caída inline: es un mini-binario que se copia
a /usr/sbin y no vale arrastrarle una dep entera por dos líneas.
Scripts: 30 lecturas pasan a default, manteniendo el
nombre INTERNO de la variable para no tocar sus 190 usos.
Y donde el script EXPORTA en vez de leer, se ponen LAS DOS (mirror-env.sh y el
fragmento in-VM de bootstrap): ahí el lector puede ser un binario viejo —worker
sin recompilar, el /usr/bin/hammer pinado del baseline— que sólo conoce HAMMER_*.
La caída sirve para lectores nuevos; los viejos necesitan que la vieja siga puesta.
Verificado de punta a punta con el binario, no sólo con unit tests: HAMMER_LAB
sigue surtiendo efecto, TAKANA_LAB hace lo mismo, y con las dos gana TAKANA_LAB.
605 tests en verde y el hash de zlib sigue en b3:dc363f26… , intacto.
Cambio de comportamiento que va aparte y hay que decir: el hostname por defecto
de una instalación nueva pasa de 'hammer' a 'takana' (sólo si no se fija ninguna
de las dos variables).
NO se tocan: las rutas /var/lib/hammer de sistemas instalados, el volid
HAMMER_LIVE del ISO, ni el namespace HARKAQ_*, que es de otro subsistema.
|
||
|
|
24bcf1783c |
takana etapa 4: los 10 crates de librería y el CLI pasan a takana-*
hammer-{core,build,bootstrap,overlay,journal,mirror,upgrade,agent,recover,cli}
→ takana-*, con sus deps de workspace, sus identificadores en el fuente y las
referencias -p de los scripts.
VERIFICADO que no mueve nada del corpus: `takana hash recipes/zlib.toml`
devuelve b3:dc363f26… , idéntico a antes del renombre. Los nombres de crate no
entran en hash_inputs, pero eso se comprueba, no se supone. 600 tests en verde.
DOS BINARIOS SE CONGELAN, y no por prolijidad:
- `hammerd` — paquete Y binario. Es componente de Stage 1 de la distro (musl,
busybox, hammerd, arje-zero), lo supervisa arje-zero en el sistema arrancado,
`PRESEED=hammerd` lo nombra en selfhost-verify y sus bytes anclan el baseline
of_tree. El nombre del crate va en los símbolos ⇒ renombrarlo mueve los bytes.
- `hammer-recover` — el PAQUETE se renombra a takana-recover, el BINARIO no.
hammer-live-install.sh lo copia a /usr/sbin/hammer-recover en sistemas ya
instalados y hornea un hook de arranque que lo invoca por ese nombre:
renombrarlo rompe máquinas instaladas, no el repo.
Consecuencia que hay que anotar igual: al renombrar hammer-core, los bytes de
hammerd cambian de todos modos porque linkea contra un crate con otro nombre.
El baseline of_tree del selfhost hay que rehacerlo — es efecto de la etapa 4,
no de un cambio de hammerd.
Las referencias en comentarios de recetas y docs (rutas hammer-core/src/…)
quedan para la etapa 5: son texto, no mueven hash.
|
||
|
|
80319d9bab |
takana: etapa 2 del renombre + los dos puntos de la hoja de marca
Etapa 2 (ADR 0016): el binario canónico es `takana` y `hammer` se sigue emitiendo. Son DOS [[bin]] al mismo main.rs, no un symlink: la siembra de la granja excluye /target (un symlink del hub no existiría en el worker) y `cargo clean` lo borraría. Ningún llamador tocado; los 124 siguen andando. Adoptados los dos puntos de la hoja de marca que chocaban con contratos: - `forja` como ALIAS de clap sobre `build`, no como reemplazo. El canónico sigue siendo el inglés, que es lo que usan scripts, cron y runbooks. Y se enmienda la regla 4 de CLAUDE.md en el mismo commit: cambiar el comportamiento dejando escrito el contrato viejo es lo peor de las dos opciones, porque el otro agente del repo aplica lo que lee. - `.tkn` como extensión de paquete. Salió barato y por una razón medida: la extensión no es lógica sino salida — se escribe en UN solo lugar (main.rs:1800) y el descubrimiento va por índice, no por glob (PackageEntry.file, repo.rs:75). Los repos con entradas .swm siguen resolviendo y un repo mixto es válido; cero ficheros .swm versionados. Los tipos Swm/SwmBuild/swm_bridge no se tocan: son internos, van en la etapa 4. 287 tests en verde (hammer-cli + hammer-core), incluidos los que fabrican repos con nombres .swm a mano — que son justamente la prueba de que la compatibilidad hacia atrás se sostiene. |
||
|
|
d13af44ddb |
kernel: el contrato admite capacidades de INTERFAZ — el test llevaba rojo desde el 03-09
`cargo test` del workspace fallaba en `el_contrato_del_repo_cierra_sobre_si_mismo`: «proceso-por-descriptor no declara símbolos». No es un descuido del alta: `abda7d3` dio de alta pidfd con `symbols = []` y dedicó ocho líneas a explicar por qué va vacío — pidfd no depende de ningún `CONFIG_*`, es interfaz del core desde Linux 5.3. Lo que no se actualizó fue el invariante, así que el dato quedó bien y el test quedó rojo. Dos días sin que nadie lo viera, que es lo que pasa cuando una suite falla por algo que «ya se sabe»: deja de mirarse entera. El invariante correcto no es «toda capacidad tiene símbolos» sino **«toda capacidad afirma algo comprobable»**: sin símbolos vale, pero entonces hay que nombrar la INTERFAZ. Sin una ni la otra, la capacidad no dice nada que se pueda verificar y lo más probable es que sea un campo a medias — que es el caso que el assert original quería atrapar y sigue atrapando. Hoy hay exactamente una capacidad así y declara `interface = ["pidfd_open(2)", "pidfd_send_signal(2)"]`. Workspace: 541 tests en verde, 0 fallos. |
||
|
|
114aeba514 |
fetch: materializa los submódulos git, que git archive no exporta
`fetch_git` clona a un mirror y materializa el árbol con `git archive | tar -x`, a propósito, para
no pagar un worktree. Pero `git archive` NO emite nada por una entrada gitlink (modo 160000): un
árbol con submódulos salía INCOMPLETO y el fallo aparecía recién en la fase configure, minutos
después y hablando de un fichero "que no existe".
Por eso un `--recurse-submodules` en el clone no arreglaba nada: el que los pierde es el archive,
no el clon. Lo que hace falta es leer `.gitmodules` + los SHA de gitlink DEL COMMIT, mirrorear cada
submódulo y archivarlo en su subruta, recursando. Encaja con el ADR 0006 sin ceder determinismo: un
submódulo ya viene pineado por SHA en el commit del padre.
Dos detalles que no son obvios:
· La URL se reescribe SSH→HTTPS. `.gitmodules` suele declarar `git@github.com:X/Y.git` y nuestro
fetch es anónimo, sin claves. Apunta al mismo repo y el commit está pineado, así que el
contenido no puede diferir: no añade nada que verificar. Las relativas (`../l10n.git`) se
resuelven contra la URL del padre tratada como DIRECTORIO, que es lo que hace git — `..` se
come el nombre del propio repo, no el del directorio que lo contiene.
· `.gitmodules` se lee del COMMIT (`git config --blob`), no del working tree, porque no hay
working tree. Y los gitlinks se listan con `ls-tree -z`: sin `-z` git escapa las rutas con
espacios entre comillas.
El test reproduce el caso completo con repos locales: comprueba primero que sin esto el gitlink
queda como directorio VACÍO —el síntoma exacto que costó el diagnóstico— y después que con esto el
fichero del submódulo llega.
|
||
|
|
54fa4a941c |
hammer: source.dir — el modo de fuente que faltaba para las recetas DERIVADAS
Al ir a escribir `recipes/atuq.toml` apareció un muro que el SDD 26 no había visto: `[source]` era
obligatoriamente git o tarball (`Source::kind` no tiene tercera salida), así que una receta cuyo
contenido no viene de upstream sino de NOSOTROS —un envoltorio sobre otro artefacto, un tema, una
configuración— no se podía ni escribir. Los rodeos posibles eran todos peores: fetchear una fuente
upstream que después se ignora MIENTE sobre la identidad del artefacto, y colgar el overlay de un
repo aparte obliga a que el worker tenga acceso de lectura a un repo privado.
`dir = "atuq"` apunta a un árbol dentro del propio repo, relativo al directorio de la receta.
SE HASHEA POR CONTENIDO, NO POR RUTA. `ArtifactHash::of_tree` ya existía (lo usa Stage 2 para
verificar bit-reproducibilidad) y hace exactamente lo que hace falta: rutas ordenadas, bit de
ejecución, contenido, sin seguir symlinks. Es la misma disciplina que ya tenían los `patches`, que
entran al hash por bytes y no por nombre. Comprobado a mano: editar un CSS del overlay mueve el
ArtifactHash y revertirlo lo devuelve exacto.
`dir` es EXCLUYENTE con repo/tarball y se comprueba primero. Declarar las dos cosas no es una
ambigüedad para resolver por precedencia: es un error de quien escribió la receta, y decirlo antes
del fetch evita bajar algo que después se pisa.
Los cuatro sitios que hacían match sobre `SourceKind` se cierran a mano y no con un `_`:
- `swm.rs` (×2) y `swm_bridge.rs` (×2): un `.swm` es un manifiesto COMPARTIBLE y necesita un puntero
que el otro lado pueda resolver (commit o sha256). El árbol de una derivada vive en este repo y no
hay puntero que mandar ⇒ error explícito en vez de emitir un manifiesto con el source vacío, que
viajaría bien y rompería del otro lado. Error y no `unreachable!`: esto es librería, y un panic
mataría al llamador por una receta mal escrita.
- `hammer pin`: una derivada ya está anclada por contenido ⇒ no hay ref flotante que fijar, lo dice
y sale con 0.
- `hammer` → file_drop: mismo tratamiento que un source que no se puede expresar.
Dos tests: que `dir` resuelve y excluye a los otros dos, y que el mensaje de «source vacío» nombra
los TRES modos — ese texto es la única guía de quien escribe una receta a mano, y si sumamos un modo
sin tocarlo mandamos a la gente a buscar un campo que no existe.
⚠ Al correr la suite aparece un fallo AJENO a esto y que NO toqué:
`kernel::contract::tests::el_contrato_del_repo_cierra_sobre_si_mismo` — «proceso-por-descriptor no
declara símbolos», del frente kernel (commit
|
||
|
|
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 |
||
|
|
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 |
||
|
|
832ade63b4 |
repo: el índice firmado ancla el digest del .swm, y --repo acepta lista de orígenes
La cadena de confianza estaba rota en el último eslabón y no se veía. La firma del índice cubre la lista de entradas, pero `file` es una RUTA, no un contenido: quien sirviera los bytes podía devolver otro .swm bajo el mismo nombre y la firma seguía casando. La red de aguas abajo no alcanza — `expected_hash` es opcional y el .swm se lee mucho antes (el gate de colisiones de `install` ya decide con su contenido). `PackageEntry::digest` (BLAKE3, la misma función que sella artefactos) se sella al publicar y se verifica antes de escribir un byte: raíz → firma del índice → digest → bytes. Con eso el origen deja de necesitar confianza, que es la condición para replicar en N espejos. Ausencia ⇒ se sigue al siguiente origen. Contenido distinto ⇒ ABORTA, no cae al siguiente: el fallback ahí convertiría una manipulación en silencio, con el paquete instalándose desde el espejo bueno y nadie enterándose de que uno miente. Campo Option con skip_serializing_if ⇒ un índice ya firmado serializa idéntico y su firma sigue siendo válida (test). Sin digests informa CUÁNTOS no verificó, para que un índice viejo servido desde un espejo ajeno no parezca verificado. El mensaje nombra el origen que sirvió de verdad, no la lista entera. 2 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CK6HpSoHcN9M4GBpqRSusR |
||
|
|
927dee60cf |
fuentes: HAMMER_MIRROR es una LISTA — un espejo propio era el mismo punto único de fallo
El ADR 0013 existe para no depender de 79 servidores ajenos, y lo resolvió creando una dependencia de UNO nuestro. Las dos variables aceptan ahora varios orígenes separados por comas y se prueban todos antes de caer a upstream. El orden se ROTA con una semilla determinista tomada del hash del propio objeto: el mismo objeto sale siempre del mismo origen (un fallo se reproduce), pero objetos distintos se reparten ⇒ una tanda del corpus ejercita la lista entera. Con orden fijo, el segundo origen no se tocaría jamás hasta la emergencia — que es literalmente «un espejo que nadie prueba», la lección del 0013 un nivel más arriba. La parte pura sale a `rotar_bases` para poder probarla sin tocar el entorno del proceso (que es global y haría los tests dependientes del orden en que corren). 3 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CK6HpSoHcN9M4GBpqRSusR |
||
|
|
1df77985fa |
tasas: T19 — el sandbox apila una capa overlay por dep, y lo caro no es lo que parece
Nadie había medido qué cuesta el apilado de capas de `bwrap_args`. El 95% de las recetas (1 108 de 1 171) construye con las deps APILADAS, hasta ~27. Un lookup que FALLA cuesta +1 925 ns por capa (a 64 capas, 124 µs = 1 580 syscalls nulas), pero se paga UNA VEZ POR RUTA: la dentry fusionada queda cacheada y el montaje dura toda la fase. Un `cc -O2 -c` toca 381 rutas distintas que fallan ⇒ 2,9 ms por fase a 24 capas, la misma décima de por ciento que la jaula de T17/T18. ⇒ `merge_deps_layer` no es una optimización de velocidad: sólo se pagaría sola a partir de ~35 200 primeros fallos en una misma fase. Es el rodeo de un muro, y el muro quedó bisecado al byte: `mount(2)` monta 41 capas (4 059 B de opciones) y falla en 42 con un `ENOENT` que miente. El `mount(8)` de util-linux se rinde entre 8 y 12, así que bisecarlo desde la shell da un muro FALSO. Consecuencias anotadas en sandbox.rs: no bajar el umbral «para que vaya más rápido», y podar `.dmerge` es gratis en rendimiento. Y hay puerta medida: la API de montaje nueva (`fsconfig lowerdir+`) monta las 64 capas que `mount(2)` rechaza. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih |
||
|
|
9142cd0565 |
upgrade: el mismo agujero que hydrate, en el proyector de generaciones
project_plan es target_root.join(rel) + escribir por ruta, igual que hydrate: un symlink de directorio dejado por una generación anterior se seguía. Reproducido — la generación 2 escribía usr/share/pkg/archivo FUERA del root y ApplyReport salía en verde. Con --root / no cambia nada (todo empieza por /); el caso real es , que es como se arma una imagen. Se mide el DIRECTORIO PADRE, no el destino: un Replaced sobre un symlink que apunta afuera es legítimo porque rename pisa el symlink en vez de escribir a través de él, y medir el destino lo rechazaría por error. Y raiz_real() resuelve el tramo existente del root dejando pegado el que todavía no existe, para que la comprobación valga también sobre una imagen nueva. Dos tests, uno por dirección: el que sale falla ruidoso, el que se queda dentro (usr-merge, temas de iconos) sigue proyectando. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih |
||
|
|
726068e653 |
hidratación: no se escribe a través de un symlink que sale del root (SDD 25 H3)
Buscando el sitio de H3 —«harkaq valida rutas a mano»— resultó que harkaq NO valida rutas a mano: delega en Landlock. Donde no se validaba NADA era en hydrate. Reproducido: hidratar es target_fhs.join(rel) y escribir por ruta, así que un symlink de directorio ya puesto en el FHS se sigue. Artefacto A trae usr/share/pkg → /algún/lado (los symlinks se replican literales, y así debe ser); al hidratar B, usr/share/pkg/archivo aterrizaba FUERA del root y hydrate devolvía Ok(1). Con --into sobre una imagen eso es escribir en el anfitrión diciendo que todo fue bien. Cuánto se estaba disparando: cero. En el store hay 160 symlinks absolutos y NINGUNO apunta a un directorio; de ~42 000 relativos ninguno sale de su artefacto. Mina desactivada, no incendio. La comprobación va por DIRECTORIO (un canonicalize por fichero sería un realpath por entrada, T8) y la semántica es la de RESOLVE_IN_ROOT, no la de NO_SYMLINKS: un symlink que se queda dentro tiene que seguir andando —usr-merge /lib → usr/lib, los 22@2x → 22 de los iconos— y hay test para cada dirección. Lo que queda de la propuesta (la versión sin carrera, con openat2 + linkat/renameat) queda escrito con su precio: arrastra el camino de patchelf. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih |
||
|
|
8faede66bd |
kernel contract: un objetivo sin sellado vigente NO es un objetivo aprobado
Faltaban dos agujeros del mismo tamaño que el que este guardián vino a tapar: 1. La receta derivada de gioser vive fuera de recipes/ a propósito, así que sus deps no resolvían y su hash no se podía calcular ⇒ su sellado viejo se comprobaba como si fuera el vigente. Ahora se le presta el catálogo (base_dir), salvo que traiga patches — que base_dir también los resuelve y moverlo los rompería en silencio. 2. Un objetivo del contrato cuya receta de hoy no tiene NINGÚN sellado simplemente no aparecía en el barrido, y no aparecer se leía como que no había nada que objetar. Ahora se nombra con el hash que le tocaría y sale != 0: no comprobar no es aprobar. Hoy eso dice, con nombre y hash, exactamente los 4 kernels que hay que construir para dar H1 por pagado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih |
||
|
|
697d3f21a1 |
tasas H4: un test hace regla lo que era suerte — cero pre_exec en el workspace
pre_exec apaga el camino posix_spawn de Rust y devuelve el spawn a fork+exec: 231 µs contra 3 949 µs con 256 MB tocados en el padre (SDD 25 T1). hammer no usa ninguno hoy, pero nadie lo comprobaba. El test se comprobó en los dos sentidos: inyectando un pre_exec en sandbox.rs se pone rojo, y si el barrido no encuentra fuentes también falla — un guardián que aprueba por vacío es el fallo de CLAUDE.md §3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih |
||
|
|
3c716dfd9f |
kernel contract: el sellado VIGENTE bloquea, el superado se informa
El store guarda todos los sellados, no el último. Como `--sealed` los miraba a todos por igual, al cambiar una receta de kernel el gate quedaba rojo para siempre por artefactos que nadie va a volver a construir — y un portón que no puede ponerse verde deja de leerse. Ahora clasifica cada sellado contra el ArtifactHash de la receta de hoy (misma vigencia que `hammer hash --check`, lab incluido): el vigente bloquea, el superado sale en su propia sección con lo que le falta, porque sigue siendo cierto que una máquina que arranque ese kernel corre sus Cards sin tope. Dos negativas explícitas: si no se puede resolver la vigencia se comprueba TODO y se dice por qué; y cero vigentes con superados a la vista sale != 0 en vez de verde — el vacío leído como presencia es justo el fallo que este guardián vino a arreglar. Recetas derivadas incluidas: --recipes mira `recipes/` y `docs/state/kernel-plans/`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih |
||
|
|
7433bcba83 |
kernel: un contrato de capacidades que hace RUIDOSA la falta de MEMCG
SDD 25 §4 dejó el hallazgo escrito y sin guardián: los kernels de hammer se construyen sin CONFIG_MEMCG, `memory.max` no existe, y arje descarta el error al escribirlo ⇒ una Card pide un tope de memoria, corre SIN tope, y la única huella es un `warn!`. Nadie lo veía porque la máquina de desarrollo SÍ trae MEMCG: el fallo sólo existe del lado del artefacto sellado. `hammer kernel contract` declara qué pedazos de interfaz de kernel usa el userland POR NOMBRE (con consumidor, fichero y CÓMO FALLA HOY si no está) y los comprueba contra un `.config` YA PRODUCIDO — no contra la receta: entre el `scripts/config -e X` y el `.config` hay un `olddefconfig` que puede tragarse el símbolo en silencio. Por perfil, no global — misma lección que el gate de hardware: `linux.toml` es el kernel de QEMU del selfhost-verify, no hospeda Cards, y su hash es load-bearing del baseline `of_tree`. Exigirle contabilidad de memoria sería rechazar una receta sana. Medido sobre el store: **2 de 11 configs sellados cumplen su perfil**; los 9 `anfitrion-cards` fallan por MEMCG (apagado A MANO: `# CONFIG_MEMCG is not set`) y les falta PSI. El kernel vivo de esta máquina pasa las 11 exigidas — que es exactamente por qué el bug sobrevivió. Distingue apagado explícito de ausente (un símbolo que el .config ni nombra puede ser un renombrado entre versiones), y un kernel sin perfil declarado queda SIN COMPROBAR en vez de contar como aprobado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih |
||
|
|
ccb3d6af2f |
hydrate: un artefacto VACÍO ya no se proyecta como éxito
Tercer sitio con la misma pregunta mal hecha, encontrado con el grep que la lección del commit anterior pedía hacer: `has` (tapado 2026-08-10), `seal` (tapado hoy) y `hydrate`, que seguía con `is_dir()` pelado. Un directorio vacío pasaba el chequeo, proyectaba 0 ficheros y devolvía un HydrateReport de éxito. Duele especial acá porque el escritorio se hidrata así (`hammer hydrate <hash> --into`, 137/137 en KDE): un FHS proyectado desde nada sale «OK» y falla mucho más tarde, sin rastro de qué artefacto faltaba. Queda a propósito sin tocar `hammer-mirror::index`: ahí el vacío se autocorrige, porque indexa por `of_tree` y el contenido difiere del origen ⇒ el sync lo trae. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4 |
||
|
|
1eb39976b0 |
store: un destino VACÍO ya no se traga el sellado en silencio
`has()` define presencia como «tiene al menos una entrada» —hay test desde hace tiempo— pero `seal()` usaba `is_dir()`, que también es cierto para un directorio vacío. Con eso `seal` devolvía Ok, tiraba el árbol recién construido, y el build logueaba `sealed` y salía 0. Dos nociones distintas de «está» en el mismo struct, y la que decide si el trabajo se guarda era la mala. Medido en el worker dev.gioser.net: 1457 directorios vacíos en el store se comían cada sellado. `openssl-threads` compilaba entero —headers, libcrypto.a, libssl.a, los .pc— y sellaba NADA; python3 se construía después sin openssl y quedaba sin `_ssl`, y con eso morían glib y 4 recetas más de GNOME. El error salía a tres recetas de distancia de la causa y no nombraba openssl ni una vez. Probado quitando el vacío: el MISMO build sella 150 ficheros con OPENSSL_THREADS, mismo hash. Se usa `remove_dir` y no `remove_dir_all`: sólo tiene éxito sobre un vacío, así que si algo dejó contenido ahí entremedio falla ruidoso en vez de borrar un artefacto bueno. No mueve hashes (testigo zlib idéntico). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4 |
||
|
|
7106ec0645 |
sandbox: forzar archivos ar deterministas — dos builds sellaban bytes distintos
CÓMO SE VIO. Construir zlib en gioser y en el worker nuevo dio el MISMO ArtifactHash y BYTES DISTINTOS. `hammer why-differs` lo nombró exacto: 13 entradas idénticas, 1 diverge — `usr/lib/libz.a`, cabecera `ar` del miembro `/` (la tabla de símbolos), mtime 1787326910 vs 1787950875. Es la hora de pared de cada build. CAUSA, AISLADA POR BISECCIÓN EN EL LAB (no por lectura de código): · `zig ar` (llvm-ar) → mtime 0 ✅ · `ranlib` del rootfs Alpine → mtime 0 ✅ (Alpine compila binutils con --enable-deterministic-archives) · `ar` y `ranlib` de NUESTRO recipes/binutils.toml → hora de pared ❌ · los mismos con `-D` → mtime 0 ✅ La receta de binutils no pasa --enable-deterministic-archives, así que toda receta que lo materialice como build-dep y llame a ar/ranlib hereda la hora. Y el comentario de SOURCE_DATE_EPOCH en este mismo fichero AFIRMABA cubrir `ar`. Era falso: binutils no lee esa variable. Corregido, porque una suposición escrita en un comentario es la que impide que alguien vuelva a medir. ARREGLO. Wrappers `ar`/`ranlib` en /usr/local/bin (que gana al /usr/bin del PATH) que anteponen `-D`. Mismo patrón que ya usa el sandbox para arreglar esta CLASE de bug sin re-hashear: -mcpu=baseline, CARGO_PROFILE_RELEASE_CODEGEN_UNITS, CARGO_BUILD_JOBS. `-D` antepuesto y no como modificador de la cadena de operación, porque la operación la pone el llamador en "$@" y no se puede reescribir. Verificado: la secuencia exacta de zlib (`ar rc` + `ranlib`) da mtime 0 y el índice del archivo sigue siendo legible (`ar t`, `nm -s`). POR QUÉ AQUÍ Y NO EN LA RECETA. Arreglar recipes/binutils.toml es la cura de raíz, pero `yupana radio binutils` da 421 dependientes transitivos y 365 sellados a deuda, en las siete imágenes — desproporcionado frente a los 9 artefactos realmente contaminados (medidos barriendo los 185 artefactos con .a del store: elfutils, elfutils-libdw ×4, zlib, musl-fts, musl-obstack, argp-standalone). La cura de raíz se agenda para el próximo rebuild del corpus que ya haya que hacer por otro motivo. PENDIENTE Y DICHO EN VOZ ALTA: esto cambia los BYTES sin cambiar la DIRECCIÓN de esos 9. Hay que re-sellarlos a propósito — es el mismo peligro que la huella del lab existe para eliminar, y no se cierra solo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4 |
||
|
|
c2b1cafff1 |
mirror git: soportar pines que son objetos tag anotados
diffutils, findutils-xargs y kustomize pinean el SHA de un tag, no de un commit. Rompía las dos
puntas por la misma suposición: el poblador apuntaba una rama al tag (imposible) y hammer escribía
ese SHA en el fichero shallow (que sólo admite commits).
El poblador pela con ^{commit} y manda el objeto tag aparte en refs/tags/hammer-objeto; sin él el
cat-file -e del otro lado no encuentra lo que la receta pide. hammer lee la frontera del bundle con
git bundle list-heads, que no necesita los objetos, y trae refs/*:refs/*.
Los 339 bundles del formato viejo siguen sirviendo: comprobado con una receta de cada forma contra
un repo que no resuelve por DNS.
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
|
||
|
|
6d99ca669b |
sandbox: el wrapper no inyecta -mcpu=baseline en modo preprocesador (-E)
util-linux —perfil BASE— no construia: `zig: error: unsupported option '-mcpu=' for target`. En modo `-dM -E -` zig traduce `-mcpu=baseline` a un `-mcpu=` VACIO y lo rechaza, asi que `errnos.h` no se generaba. La receta YA trae un apaño para esto: parchea sus generadores all_errnos y all_syscalls para filtrar el flag. Pero filtra `"$@"`, y el flag NO viene ahi — lo inyecta el wrapper DESPUES, en el `exec`. El apaño quedo inerte cuando se introdujo el wrapper, y no hay forma de esquivarlo desde una receta. Omitirlo con `-E` es inofensivo por construccion: preprocesar no genera codigo, asi que no hay CPU al que apuntar. ⚠ MISMO PATRON QUE make/--export-dynamic: el wrapper cambia lo que producen los builds FUTUROS sin mover ningun hash (no esta en hash_inputs), asi que la rotura quedo congelada por cache-hit hasta que el corpus se reconstruyo. Este cambio tiene la misma propiedad ⇒ se hace AHORA, con el corpus a medio reconstruir y casi nada sellado, no dentro de un mes. Verificado: util-linux sella (370530ff...). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1940a94a11 |
lab: los -dev al rootfs — es el unico sitio donde _curses se puede resolver
Decision del usuario. CPython construye sus modulos opcionales como .so
COMPARTIDOS y las .a del corpus NO son PIC («relocation R_X86_64_PC32 against
symbol 'stdscr'»), asi que declararlas en [deps] NO funciona: se probo una
por una. Las libs del rootfs son compartidas y si sirven.
Añadidos: libffi-dev ncurses-dev readline-dev sqlite-dev bzip2-dev xz-dev
expat-dev. Resultado VERIFICADO: el guardian de python3 importa los OCHO
modulos (_ctypes _curses readline sqlite3 bz2 lzma zlib pyexpat) y pasa. De
paso `libffi` sale de [deps]: ya lo aporta el lab.
openssl-dev NO va, deliberado: se de-Alpinizo porque el host-tool del kernel
enlaza el openssl del corpus desde el overlay; devolverlo podria cambiar el
artefacto del kernel. python3 sigue sin `ssl`.
Y SE CIERRA EL AGUJERO QUE ESTO ABRIA. Esas libs del rootfs ahora entran en
el CONTENIDO de un artefacto, asi que su version es parte de su identidad ⇒
van a TOOLCHAIN_PREFIXES. Sin eso habriamos reabierto en pequeño el mismo
fallo que
|
||
|
|
d797fc472d |
kernel: el gate contra un .config producido — ve los huecos que la receta base ya traía
El gate que había mira lo que apaga el PLAN, así que sólo ve regresiones que introduce el plan: un hueco que ya venía en la receta base le pasa por debajo. El modo nuevo compara DOS configs y define regresión como «funcionaba y dejó de funcionar», que es la formulación literal del #6 del handoff: hammer kernel gate --config <producido> [--baseline /proc/config.gz] --objective X El referente por defecto es /proc/config.gz: el kernel que arrancó esta máquina es la prueba viva de qué hace falta para arrancarla. Y comparar DOS configs, en vez de mirar sólo el nuevo, mata de raíz un falso positivo que tenía: los nombres de módulo cortos colisionan. El driver que /sys llama `usb` mapea a QE_USB (el USB de las QUICC Engine de Freescale) y `port` a PORT_CHAN. Mirando sólo el config nuevo aparecen como perdidos y el gate bloquearía un plan sano; exigiendo que estuvieran encendidos en el referente, el falso positivo se cae solo. Con test. Probado contra gioser (Hetzner vServer, 38 drivers bindeados) partiendo de linux-metal: destapó cuatro pérdidas que NINGÚN bundle causaba — aer, iTCO_wdt, lpc_ich y pcspkr están encendidos en el kernel que corre y linux-metal no los enciende nunca. El gate viejo no podía verlas por construcción. De paso, un mensaje que mandaba a buscar donde no está: sin culpable atribuido decía «lo apaga una perilla», cuando la causa es que la receta base no lo enciende. Catálogo, tres entradas nuevas nacidas de medir esta máquina: · bundle sin-gpu-intel — DRM_I915 es de los drivers más grandes del kernel y no sirve en una VM con virtio-gpu. NO apaga DRM: el vídeo sigue por simpledrm/EFI o virtio-gpu. · knob invitado-virtio — VIRTIO_BALLOON y HW_RANDOM_VIRTIO no vienen en el defconfig y ninguna receta del repo los enciende; en gioser los dos están BINDEADOS. Un kernel sin ellos arranca, pero la VM pierde el globo de memoria y la entropía del anfitrión. · knob plataforma-pc — PCIEAER, LPC_ICH, INPUT_PCSPKR y el watchdog ITCO_WDT. El watchdog necesita además WATCHDOG, que linux-metal apaga a propósito: por eso va en una perilla y no en la base. En una máquina sin acceso físico, el watchdog es lo que la reinicia cuando se cuelga. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
58d31618b7 |
hash: el toolchain del lab entra en hash_inputs — el corpus entero se re-hashea
Decision del usuario tras la medicion de los 4 kernels: dos labs con distinto rustc producian bytes distintos en la MISMA direccion, y el store no tenia como notarlo. Ahora el toolchain es una entrada del ArtifactHash. QUE ENTRA: 29 paquetes del rootfs cuya VERSION puede cambiar los bytes — compiladores/enlazadores (gcc, clang, llvm, binutils, rust, cargo), las libs de codegen de gcc (gmp, mpfr4, mpc1, isl), el runtime que se enlaza (musl, libgcc, libstdc++, libatomic, libgomp) y los headers que se compilan dentro (linux-headers, fortify-headers). NO entra el rootfs entero: cada paquete de mas invalida el corpus en cada bump, y con edge rodante curl se actualiza sin que cambie una sola instruccion emitida. Quedan fuera a proposito, y no es una afirmacion de que no influyan: los autotools y las shells pueden cambiar ficheros generados. Es una decision de coste. Si algun dia se ve una divergencia que rastree ahi, se anaden — y ese dia el corpus se re-hashea otra vez. DE DONDE SALE: del apk db del rootfs REAL (cfg.rootfs, que respeta HAMMER_LAB/HAMMER_ROOTFS), no de docs/state/lab-toolchain.lock. El lock sigue siendo el registro legible que viaja por git; hashearlo permitiria sellar con un lab distinto del declarado. Derivar la ruta del padre del store se descarto: esa suposicion ya rompio al worker cuando su store se anclo a un volumen (ver defaults_for_store_with_lab). SIN CAMINO SILENCIOSO: el parametro es obligatorio, no Option. Sin rootfs falla y dice que hacer. Un default aqui reintroduciria la divergencia que esto cierra. Trae test de regresion de un fallo MUDO: la primera lista de prefijos llevaba el guion de version (`gcc-`) y en el apk db el campo P: es solo el nombre (`gcc`) ⇒ no casaba ninguno y la huella salia la del conjunto vacio. Un hash valido, constante e inutil, que mirando el hash no se nota. COSTE, medido y no estimado: sealed 768 -> 0, debt 777. Los 1745 artefactos del respaldo quedan SUPERADOS, no perdidos. Ninguna imagen queda lista. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ca9bde0251 |
kernel: la clausura contrastada contra un olddefconfig de verdad — 65/68 con 0 falsos positivos
Hasta acá el armador era análisis puro: la clausura decía qué debía morir y nadie lo había
contrastado con el resolvedor real. NO hace falta construir un kernel para hacerlo: lo caro
es la fase compile (35-60 min); toda la cadena del armador vive en configure y corre en
segundos.
Método: árbol 6.16.12 entero, `make defconfig` de base (el config de linux.toml NO sirve de
base: ya apaga wifi/audio/fs a mano, así que el lado disable no apagaría nada y la
predicción no se pondría a prueba — fue mi primer error), el fragmento del plan, y
olddefconfig. Verdad de campo = los símbolos que pasaron de encendidos a apagados.
sólo depends on ....... clausura 1775 aciertos 64/68 SOBRA 0 falta 4
+ huérfanos select .... clausura 1794 aciertos 65/68 SOBRA 0 falta 3
+ comparaciones ....... clausura 1795 aciertos 65/68 SOBRA 0 falta 3
SOBRA 0 en las tres: el predictor nunca dice que muere algo que sobrevive, que es la única
dirección en la que puede equivocarse sin fabricar un ladrillo.
Dos refinamientos que salieron de la medición, cada uno con su test:
· HUÉRFANOS DE SELECT. Un símbolo sin prompt no se marca a mano: sólo entra por select.
Si caen todos sus selectores, cae él, aunque nadie dependa de él (caso ACPI_NHLT). Se
exige >=1 selector: sin ninguno entra por un default, y darlo por muerto mataría media
tabla.
· `X = y` SÍ ES DEPENDENCIA DURA. Medio drivers/video/fbdev declara su dependencia de FB
como `depends on (FB = y) && ARM`. Tratar toda comparación como opaca dejaba esos
drivers fuera. `X = n` sigue fuera a propósito: con X en n es VERDADERA. De regalo, las
fugas select sin declarar de sin-graficos cayeron de 8+ a 1.
Los 3 que faltan NO son un fallo, son otra pregunta: CRYPTO_LIB_ARC4, REGMAP y
SYSTEM_DATA_VERIFICATION tienen selectores FUERA de la clausura (PPP_MPPE, 111 usuarios más
de REGMAP…). Se quedaron sin usuarios en ESE config; encendés PPP y ARC4 vuelve.
«Inalcanzable» y «apagado ahora» no son lo mismo, y la clausura contesta la primera.
Y un bug que sólo aparece corriendo el resolvedor: el diff-back contaba como promesa
incumplida todo símbolo pedido ausente del .config. Pero Kconfig NO EMITE un símbolo cuyas
dependencias no se cumplen ⇒ un `-d WLAN` cuya raíz ya cayó simplemente no sale. Con esa
cuenta un plan perfecto se reportaba roto (2 falsos incumplidos de 16). Ahora: ausente +
se pedía apagar = éxito; ausente + se pedía encender = fallo. La corrida real sale 14
cumplidos, 0 incumplidos.
GUARDIÁN NUEVO, y hacía falta: las cuatro recetas de kernel NO son el mismo kernel — linux
y linux-metal van por 6.16.12, linux-metal-dual y linux-generic por 7.1.2. Planear una
contra el árbol de la otra calcularía clausuras sobre símbolos que ahí no existen, y
saldría SIN RUIDO. KconfigTree lee ahora su versión del Makefile de arriba y `plan` FALLA
si no coincide con la de la receta (los diagnósticos sólo avisan). Aviso de la sesión de
granja/store, verificado antes de implementarlo.
Catálogo: las dos fugas que destapó la clausura más grande quedan declaradas con motivo
(FB_SYSMEM_HELPERS_DEFERRED por HID_PICOLCD_FB; DRM_DISPLAY_DP_TUNNEL_STATE_DEBUG por
DRM_I915_DEBUG), y las notas sobre THUNDERBOLT/REISERFS_FS pasan a pasado: ya se
corrigieron en
|
||
|
|
fe382b0d26 |
kernel: el gate de no-regresión, por objetivo — el mismo plan pasa en QEMU y bloquea en metal
Paso 3 del §8 del SDD 22, y cierra el orden que fijaba: reversa, clausura, gate, diff-back. La pieza que faltaba no era el gate sino el MAPA driver → símbolo. El kernel sabe qué driver tiene bindeado cada dispositivo, pero no de qué CONFIG_* salió: esa relación sólo existe en los Makefiles de kbuild. modmap.rs lee 15.789 reglas obj-$(CONFIG_X) += y.o en 3182 Makefiles. Dos trampas de nombres, cada una con su test: · el módulo cargado usa _ donde el fichero usa - (snd-hda-intel.o → snd_hda_intel) · un módulo puede salir de VARIOS símbolos, y sobrevive si sobrevive cualquiera Sin resolverlas el gate no encontraría nada y diría que todo está bien, que es el peor resultado posible para un portón. POR OBJETIVO, no global. La regla es "todo dispositivo en uso debe seguir teniendo driver"; aplicada global rechazaría recipes/linux.toml, que apaga USB, HID e INPUT A PROPÓSITO por ser el kernel de QEMU con consola serie. El gate NO CORRE sin --objective, y un allow_bundles con un id mal escrito es error de CARGA del catálogo (si no, autorizaría nada y bloquearía sin que se entienda por qué). Medido con el mismo plan (sin-usb + sin-entrada-humana + sin-graficos + sin-wifi) y el hardware real de gioser: qemu-serial ........ PASA — 5 pérdidas autorizadas metal-escritorio ... BLOQUEA — las mismas 5 como regresiones, con el bundle culpable Ése es todo el punto del §5. Y lo que el gate no puede comprobar, lo dice: de los 38 drivers bindeados, 15 no se mapearon a ningún símbolo (pcieport, serial8250 — built-ins cuyo nombre de driver no coincide con el del módulo). Quedan listados como SIN COMPROBAR, nunca como aprobados. hammer kernel hw vuelca la huella y los drivers de la máquina DESTINO, que no tiene por qué ser la de build — el SDD lo pedía explícitamente. Cuatro objetivos en el catálogo (qemu-serial, servidor, metal-escritorio, portatil) y un runbook nuevo: docs/runbooks/armador-de-kernel.md, con el ataque de punta a punta y una lista honesta de lo que todavía NO está (sonda en VM, atestación por huella, bisección, curación del delta con modelo, perillas side=recipe). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
02991dd9d8 |
kernel: plan (receta derivada) y diff-back — dos hashes para el mismo código fuente
Paso 4 del §8 del SDD 22, y el corazón del armador.
hammer kernel plan --recipe recipes/linux.toml --bundle sin-wifi --bundle sin-audio
--bundle solo-ext4 --knob jaula-y-eio-moderna
base b3:cb926743…
derivada b3:47a52b2e… (16 banderas, 1775 símbolos con su clausura)
El config vive en la fase `configure` y las fases entran en hash_inputs ⇒ el config ES la
identidad del artefacto. Por eso el plan emite una RECETA DERIVADA y no finge que el
kernel sea un binario parametrizable. Y como el plan DETERMINA el artefacto, el JSON lleva
su ArtifactHash: la UI puede decir "esto ya está construido y firmado" sin construir nada.
Tres decisiones que no eran obvias:
· Se emiten RAÍCES, no clausuras: 16 banderas, no 1775 líneas. La clausura la calcula el
olddefconfig del propio kernel. hammer la sabe sólo para poder explicarla — la app
nunca escribe un .config.
· La fase derivada AÑADE una segunda ronda (…&& scripts/config … && make olddefconfig)
en vez de reescribir la base: no hay que parsear el shell de nadie, olddefconfig es
idempotente, y la base sigue siendo literalmente la de siempre en el diff.
· Los conflictos se RECHAZAN, no se ordenan. Resolver por orden de aparición sería una
respuesta plausible y arbitraria. Y hay un segundo conflicto que el símbolo solo no
delata: encender algo que cae DENTRO de la clausura de lo que otro bundle apaga —
olddefconfig lo descartaría sin decir nada.
diff-back: la mitad que faltaba del §6 del handoff. Clasifica cada símbolo pedido en
cumplido / INCUMPLIDO (el .config dice otra cosa) / ausente (el kernel ni lo menciona: la
bandera fue un no-op), con la procedencia de quién lo pidió, y sale != 0 si el config no
honra el plan. Probado contra /proc/config.gz de gioser: 3 incumplidos, 1 ausente.
La procedencia por símbolo (#7 del handoff) sale de regalo: cada bandera carga quién la
pidió y por qué (raíz del bundle / fuga select cerrada / perilla).
Y el orden del fragmento es estable a propósito: ese texto entra al hash, así que un orden
que dependiera del recorrido daría dos hashes para el mismo plan.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
cf2cf54914 |
kernel: modo reversa y catálogo de bundles — y las cuatro recetas apagan dos símbolos que ya no existen
Paso 1 del §8 del SDD 22 (va después de la clausura porque necesitaba el grafo). Sigue sin
compilar ni escribir nada.
hammer kernel probe — lee /proc/config.gz (o /boot/config-<release>) y muestra el kernel
que YA CORRE por el lente de los bundles: cuánto de cada uno rige, qué capacidad carga
esta máquina y no usa, y dónde el hardware CONTRADICE a un bundle aplicado. Corrido en
gioser: 10.551 símbolos, 20 dispositivos PCI, 38 drivers bindeados, 15 bundles, 7 con
capacidad que este hardware no usa.
hammer kernel bundles [--check] — el catálogo con las clausuras resueltas contra un árbol
concreto, y el control de frescura.
Piezas nuevas en hammer-core/src/kernel/:
catalog.rs bundles N1 y perillas N2. El campo `side` NO es decorativo: la mitad de N2
son variables de receta, no símbolos; mueven el hash igual pero se aplican en
otra fase y fallan distinto. Una perilla side=recipe sin recipe_field es
error de carga, porque es un diff que la UI no podría explicar.
hw.rs huella DMI+PCI+flags de CPU. El USB se LEE y se REPORTA pero NO se hashea: un
pendrive no puede cambiar la clase de hardware bajo la que se cachea un
kernel. Tampoco entra el serial: la huella agrupa máquinas, no las identifica.
Tres tests fijan esas tres propiedades.
reverse.rs el análisis. Y una tercera salida que no estaba pedida: cada fuga `select`
que entra a un bundle y NO está declarada en el catálogo es un símbolo que
upstream agregó y nadie revisó ⇒ la mitad barata de la curación del delta
(§3 del handoff) sale de comparar grafo con catálogo, sin IA.
docs/state/kernel-bundles.toml — 15 bundles N1 y 8 perillas N2, cada fuga resuelta a mano
una vez: `close_leaks` (se apaga también al que la provoca) o `accept_leaks` (se deja
abierta a sabiendas, con el motivo escrito). Ejemplo de por qué hacían falta las dos:
"sin-audio" NO cierra — DRM_I915/NOUVEAU/AMD_DC hacen select del códec HDMI, y cerrarlo
sería quedarse sin GPU. Se acepta y queda por escrito.
LO QUE DESTAPÓ EL CONTROL DE FRESCURA: las CUATRO recetas de kernel (linux, linux-metal,
linux-metal-dual, linux-generic) apagan `THUNDERBOLT` y `REISERFS_FS`, y 6.16.12 NO TIENE
NINGUNO DE LOS DOS. Thunderbolt se llama USB4 desde que upstream lo fundió con USB4;
reiserfs fue retirado. Los dos `-d` son no-ops silenciosos: el driver USB4 sigue entrando
por el defconfig mientras la receta dice que está apagado. NO las toco — cambiarlo mueve
el ArtifactHash de los cuatro kernels y es una decisión, no una limpieza.
Y el propio probe destapó un desajuste que ahora avisa: el config vivo de gioser es de la
serie 7.1 y el catálogo se revisó contra la 6.16 ⇒ las clausuras son aproximadas. Se dice
en vez de callarlo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
040c768def |
kernel: el lector de Kconfig y la validación del §3 — la clausura reproduce los bundles a mano
Primer paso del SDD 22 (armador de kernel), en el orden que fija su §8. Regla dura
respetada literalmente: hammer LEE el grafo de Kconfig, no lo resuelve — el .config lo
sigue produciendo el olddefconfig del propio kernel.
hammer-core/src/kernel/: lector tolerante (18.212 símbolos, 1646 ficheros, 0 avisos de
parseo sobre 6.16.12) + lector de .config. hammer kernel {stats,closure}.
La semántica de arista, que el §3 pedía definir antes de escribir el predicado:
· dependencia dura = símbolo en posición CONJUNTIVA (en "A && (B|C)" sólo A). La
disyunción, la negación y las comparaciones no aportan. Conservador a propósito:
apagar de menos se nota, apagar de más hace un ladrillo.
· símbolo con varias definiciones ⇒ INTERSECCIÓN entre ellas, no unión.
· select es el portillo, no una arista más: fuerza el destino IGNORANDO sus depends.
select_leaks las enumera; closure_off_fixpoint cierra el bundle contra ellas y REPORTA
el precio en vez de aplicarlo solo.
La medición que decide §2.1, contra el bundle N1 hecho a mano de recipes/linux.toml:
clausura estricta de WIRELESS ......................... 350
punto fijo (3 fugas: WLAN, IWLEGACY, GELIC_WIRELESS) .. 406, cierra en 1 ronda
bundle a mano ......................................... 421
SOBRA 0 · falta 15
Los 15 son todos RFKILL, que no es wifi sino el interruptor de radio compartido con
bluetooth y NFC. El humano apagó DOS bundles en la misma línea ⇒ el catálogo necesita
"sin radios" como entrada propia. §2.1 es viable.
Y el punto fijo también dice cuándo no: cerrar "sin audio" exige tragarse DRM_I915/
NOUVEAU/AMD_DC, que hacen select del códec HDMI. En linux.toml sale gratis porque los
gráficos ya están apagados; en un escritorio sería una decisión.
De regalo: linux.toml apaga REISERFS_FS, que 6.16.12 ya no tiene. Un -d a un símbolo
inexistente se pierde HOY en silencio — justo lo que el diff-back (paso 4) va a atrapar.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
0377f4fa7a |
guardianes: un artefacto VACIO deja de contar como presente
Dos eslabones de la misma cadena, que el 2026-08-10 dejo cuatro recetas con OK sin producir un solo fichero. Store::has era path_of(..).is_dir(): un directorio vacio contaba como sellado, asi que build() hacia cache-hit y devolvia Ok sin construir. Ahora exige al menos una entrada. NO exige el sidecar .hammer/recipe.toml aunque seria mas expresivo: ese lo escriben los llamantes, no seal(), y hammer-bootstrap sella sin el ⇒ pedirlo lo haria reconstruir siempre. Va con test de regresion. --listar armaba el manifiesto con `ls`, que lista NOMBRES: un vacio es identico a uno bueno, y de ahi build-state.py lo daba por sellado. Ahora usa `du -s` (8,6 s sobre 1751, frente a un ls instantaneo), separa los vacios a work/respaldo-vacios.txt y los DICE siempre, tambien cuando son 0. Cuidado con el orden en ese awk: recortar la ruta antes se come el contador de bloques y el filtro compara el nombre en vez del tamano — daba 406 vacios falsos. Primero filtrar por numero, despues recortar. Quedan 3 vacios sin curar en el respaldo (dbus x2 y un libxkbcommon de hash superado); no caen en ninguna clausura construida. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
fbd586f9b2 |
etapa 2: el split de debug FUNCIONA — y el piloto destruyó los artefactos antes de lograrlo
RESULTADO, con los tres criterios medidos a la vez sobre las dos recetas que divergían: bison 6M → 3M · 0 ficheros vacíos · 0 secciones .debug_ · «bison (GNU Bison) 3.8.2» appstream 56M → 18M · 0 ficheros vacíos · 0 secciones .debug_ · «AppStream version: 1.0.5» y las DOS pasan de DIVERGIR a REPRODUCIR. O sea que un solo cambio recupera espacio Y cierra la fuga de reproducibilidad, como predijo el §1.bis. Pero se llegó ahí después de tres errores que conviene dejar escritos. 🧨 1. `zig objcopy --strip-debug X X` (mismo fichero de entrada y salida) TRUNCA EL FICHERO A 0 BYTES. Destruyó los artefactos del piloto — y lo grave es que LOS TRES INDICADORES DECÍAN QUE IBA BIEN: el tamaño cayó 84% (porque los ficheros quedaron vacíos), `why-differs` dijo REPRODUCE (porque dos árboles vacíos son idénticos) y no quedaban secciones .debug_ (porque no quedaba ninguna sección). Se cazó al EJECUTAR el binario: 0 bytes. ⇒ La verificación de un artefacto tiene que incluir que SIGA FUNCIONANDO, no sólo que pese menos y reproduzca. Un artefacto vacío cumple las dos y no sirve para nada. Es la lección de esta campaña aplicada a la campaña misma: una métrica que parece éxito. 2. Al arreglarlo con fichero temporal, el strip pasó a ser un NO-OP SILENCIOSO: los binarios quedaban intactos y el tamaño no bajaba, porque no se pudo confirmar que `zig objcopy` acepte `--strip-debug`. Cambiado al `strip` de binutils, que sí funciona, a costa de declarar la dep. ⇒ Preferible una dep explícita que funciona a una comodidad que no se sabe si hace algo. 3. Con el strip real, apareció una fuga NUEVA: los artefactos seguían divergiendo, ahora por la CABECERA `ar` de los `.a` — `strip` los reescribe con los timestamps de cada corrida. Lo nombró `why-differs` exacto («archivar en modo determinista»). Arreglado con `strip -D` (= --enable-deterministic-archives). ⇒ Arreglar media causa deja el invariante igual de roto: el debug ya no divergía y el archivo sí. DISEÑO: `strip_debug` es un campo de la receta que ENTRA en `hash_inputs` y sólo si está fijado (mismo patrón que `zig_version`). Las dos mitades importan y están clavadas en un test: si no entrara, el lab cambiaría el contenido del artefacto sin mover el hash y el store MENTIRÍA; y al entrar sólo si está fijado, se despliega receta a receta sin re-hashear las 1161 de golpe — verificado: con el campo añadido al código, los 1161 hashes existentes NO se movieron. Va como paso del lab y no en la fase install de cada receta porque 383 de las 1161 no tienen install explícita: meterlo receta a receta obligaría a escribir a mano ese install por defecto en las 383, con riesgo de no clavarlo exacto. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |