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
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
El gate de SDD 25 §4.ter existía y NADIE lo corría. Un kernel reconstruido sin MEMCG volvería
a pasar inadvertido — y ese fallo no se ve del lado del desarrollo, porque el kernel de la
máquina de trabajo sí trae MEMCG: sólo se ve comprobando el artefacto.
Mismo cable que el vigía de fuentes y por la misma razón que está escrita ahí arriba: lo que
nadie refresca envejece hacia el optimismo. Ahora cada ciclo deja el veredicto en
docs/state/kernel-contract.txt y el git log lo muestra como el resto del khipu.
El fichero se escribe por temporal y sólo se mueve si tiene contenido: contract sale != 0
cuando el contrato no se cumple —y ese rojo es justo lo que hay que guardar—, pero un fichero
vacío se leería como «nada que objetar».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
Se construyeron en gioser, no en el worker: el LXC dev.gioser.net lleva ~14 h sin ssh.
Quedan `cosmic-applets` y `cosmic-settings`, que superan el techo de memoria de la caja
INCLUSO con un solo core (7,7 G con un suelo de 2,3 G que ponen gitea y los agentes) — no
es paralelismo, es el pico de un `rustc` solo.
El método que las trajo: capar por entorno con `taskset` (nunca `-j` en la fase, que
cambiaría el ArtifactHash) bajando 3→2→1 cores, con un vigía que corta el lote si el swap
libre baja de 250 MB. Cada bajada compró recetas. El vigía cortó cuatro veces y la máquina
sobrevivió las cuatro; el LXC pasó por el mismo pico sin guardián y no volvió.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
hammer kernel contract --sealed: 5 de 5 vigentes cumplen su perfil, exit 0. Antes eran 2 de 11.
Una Card que pide memory.max ahora encuentra el fichero en vez de dejar un warn! y correr sin
tope.
Lo medido, no lo supuesto:
- el x86_64_defconfig de 7.1 ya NO trae MEMCG=y ⇒ había que pedirlo explícito;
- MEMCG no tiene ningún depends on, así que el olddefconfig no podía tragárselo — comprobado
igual contra el .config producido, que es la regla del guardián;
- precio: +80 KiB en linux-generic (+0,49%), +84 en linux-metal, +88 en linux-metal-dual,
+80 en linux-gioser (+0,71%). Medio punto de kernel por contabilidad por unidad y presión.
Evidencia cruda en docs/evidencia/tasas-kernel-2026-08-29/contrato-h1-pagado.txt, con el
diff-back del plan de gioser sobre su config nuevo (35 cumplidos, 0 incumplidos).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
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
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
La receta derivada inlinea la fase configure de su base, así que el MEMCG de linux-metal sólo
llega acá regenerando. Aprovecha el re-hasheo obligado para soltar la concesión del -j1: vuelve
al -j"$(nproc)" que emite el armador, que es la regla del repo — capar el paralelismo en la
receta cambia el ArtifactHash aunque el binario sea idéntico, y el throttling va por entorno.
b3:b026fe14… reemplaza a b3:8ffd5710… (que era la variante -j1 sin MEMCG).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
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
Sin CONFIG_MEMCG el fichero memory.max no existe y arje-incarnate descarta el error: una
Card pedía un tope de memoria y corría sin ninguno, dejando sólo un warn!. El x86_64_defconfig
de 7.1 ya no trae MEMCG=y, así que había que pedirlo explícito.
Va en tanda con PSI (que el contrato declara como wants) para pagar de una sola vez el
re-hasheo: la fase configure ES la identidad del artefacto. linux.toml queda intocado a
propósito — es el baseline of_tree del selfhost-verify y no hospeda Cards.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
`diff-back` contesta «¿el .config honra el PLAN?»; el contrato contesta la otra
mitad, «¿el kernel trae lo que el userland usa por nombre?». Son distintas: un
kernel puede honrar su plan al pie de la letra y no tener MEMCG, que nunca
estuvo en ningún plan porque nadie lo pidió.
Queda dicho también que hoy sale ROJO a propósito (2 de 11) y por qué.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
- §0 y §4.bis: el «sin MEMCG» deja de ser deducción del defconfig. Los ONCE
.config sellados traen `# CONFIG_MEMCG is not set` y `# CONFIG_PSI is not
set`, y ninguno tiene CFS_BANDWIDTH (⇒ tampoco existe `cpu.max`; hoy nadie
lo pide). Y se nombra por qué sobrevivió: el kernel de la máquina de
desarrollo SÍ los trae — el fallo sólo existe del lado del artefacto.
- §4.ter: el guardián `hammer kernel contract`, con las tres decisiones de
diseño (por perfil, contra el .config y no la receta, apagado ≠ ausente).
- §3: de tres sorpresas a cinco (taskstats no es más rápido que /proc; el
sondeo es ciego, no lento).
- §9: reescrita. Cuatro de los seis experimentos nombrados quedaron medidos
con root; siguen abiertos el iterador BPF (detrás de H7) y el Intel con PTI.
- §5, §7 y §10: filas medidas, H1 marcado como pendiente-pero-vigilado,
H8 (el contrato es parte del release) y H9 (reflink alimenta SDD 18).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
SDD 25 §9 listó seis experimentos que no se pudieron correr y por qué. Cuatro
de ellos sólo pedían privilegios, y esta sesión los tuvo. Programas y salidas
crudas en docs/evidencia/tasas-kernel-2026-08-29/.
1. proc connector (bench_connector.c) — el sondeo de /proc no es lento, es
CIEGO: 200 procesos de vida mínima lanzados uno cada 10 ms, sondeando cada
100 ms se ven **0 de 200**; por connector, **200 de 200**. Y esperar el
evento no cuesta nada frente a waitpid (116,0 vs 114,8 µs, dentro del ruido),
con 0 eventos perdidos en 1400.
Sorpresa: taskstats por genetlink **no es más rápido** que parsear
/proc/self/stat (2 688 vs 2 697 ns). Su valor es el CONTENIDO (delay
accounting), no la velocidad.
2. reflink (bench_reflink.c) — el «copiar en O(1)» existe y NO está en ext4:
FICLONE no soportado, y copy_file_range ≈ read+write (13,7 vs 14,5 ms).
En xfs y btrfs es PLANO: 64 MiB en 0,010 ms y 512 MiB —ocho veces más— en
0,020 ms, contra 512,7 ms de read+write. **25 600× a 512 MiB.**
3. io_uring SQPOLL (bench_sqpoll.c) — no rescata el veredicto de T12: sobre
datos cacheados pierde igual. ext4: pread 554 ns < io_uring 618 < SQPOLL 868.
tmpfs: pread 749 < SQPOLL 1 409 < io_uring 2 030 (ahí sí mejora al io_uring
normal, 1,4×, pero sigue perdiendo contra la syscall). Y se come un core.
4. syscalls por invocación de compilador — con el strace DEL PROPIO CORPUS
(store/3c1fd8c0…-strace), que es la respuesta a «no hay strace en esta
máquina». gcc: 1 803 syscalls por un `-c` trivial, **1 095 con error (61%),
919 de ellas readlink** — la canonicalización de rutas, que T8 no medía.
Pero al piso de 78,7 ns son 0,14 ms sobre 30,5 ms de compilado: **el cruce
al kernel es el 0,5%**. La tormenta de rutas vale milisegundos, no minutos.
Condiciones distintas a la corrida original (load ~1,3 sobre 4 vCPU contra 7,3)
⇒ los absolutos NO se comparan entre corridas, sólo los cocientes dentro de una.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
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
En Debian `patch` entra de regalo dentro de `build-essential`; al desglosar la rama dnf a
gcc/g++/make sueltos se cayó sin que nadie lo notara. El campo `patches = [...]` de una
receta lo aplica hammer DEL LADO DEL HOST, fuera del sandbox, así que no alcanza con que
el rootfs lo traiga —lo trae, `/usr/bin/patch` está en el alpine de los dos labs— y por
eso el diagnóstico "rootfs laptop ≠ worker" no aplicaba acá.
Medido en dev.gioser.net: `cairo-shared` moría con `spawn patch: No such file or
directory` y detrás caían `pango` y `gtk4`. El stack GUI entero de GNOME por un binario
de 129 KB ausente en el host.
Regla que queda escrita: toda herramienta que hammer invoque HOST-SIDE va en esta lista,
no en `[deps]` de la receta — declararla en la receta re-hashearía cairo y todo GNOME
debajo, para arreglar algo que no es de la receta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
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
`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