.farm-harvest/ son 438 dirs <hash>-<nombre>: artefactos del store cosechados de la granja, 26G.
Nació de un rsync a mano — ningún script, doc ni commit del repo lo referencia — y quedó SIN
ignorar, así que un `git add -A` habría intentado commitear el store entero. Justo el accidente
que la regla 'nunca git add -A' (otro agente comparte el working-tree) existe para evitar.
Las 438 están YA en ./store, verificadas una a una (no por muestreo). Como el store es CAS —el
nombre ES el hash del contenido— mismo nombre = mismos bits por construcción ⇒ es 100% redundante.
No lo borro acá: 26G de disco son del usuario, no míos. Queda ignorado, que era el riesgo real.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El smoke de harvest-go.sh ejecutaba cada binario cosechado con el cwd en la RAÍZ DEL REPO, así que
un binario que escribe estado al arrancar dejaba basura entre las fuentes. Pasó: `gocron version`
—y `version` es justo el PRIMER flag que prueba el smoke, así que se disparaba siempre— sembró
config/{config.yaml,db.sqlite} (5 jobs de ejemplo + una sqlite) el 2026-07-04, y quedó sin trackear
hasta hoy. Medido, un flag a la vez, en cwd limpios:
[version] DEJO: ./config ./config/db.sqlite ./config/config.yaml
[--version] limpio [-v] limpio [--help] limpio [-h] limpio
Un `--help` no debería poder tocar el repo. El smoke ahora corre en un mktemp -d que se borra.
Vale para cualquier herramienta futura, no sólo gocron.
Sin regresión en el veredicto: en tmpdir `gocron version` panica (le falta web/index.html), el
smoke ya trata el panic (continue) y pasa a --version, que funciona ⇒ gocron sigue aprobando.
config/ borrado (no trackeado, sin una sola referencia en scripts/recetas/código).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
756928a generalizó de más. El alcance real, medido dentro del sandbox con readelf sobre un
`int main(){return 0;}` trivial:
zig 0.13.0: -static → NEEDED=1 (dinámico) ← el bug
zig 0.13.0: -target x86_64-linux-musl -static → NEEDED=0 (estático)
zig 0.16.0: -static → NEEDED=0 (estático) ← default, sano
O sea: el -static roto es de ZIG 0.13.0, NO del framework. Con el zig default el -static que el
lab exporta por link="static" funciona ⇒ CC sin -target (sandbox.rs:415) está BIEN y NO hay que
re-hashear los 841 sellados, al revés de lo que decía 756928a. samurai lo sufría por caer en la
intersección de dos rarezas: pinea zig 0.13.0 Y no usa libtool (nada absorbía el -static).
Las otras 4 recetas con zig 0.13.0 + link=static (mtools/openssh/openssl/xorriso): auditadas,
0 mienten — usan libtool, que absorbe el -static por su cuenta.
pkgconf: el diagnóstico ORIGINAL (libtool se come el -static) era el correcto. Usaba el BuildSys
automático ⇒ fases explícitas sólo para meter -all-static en compile Y en install. Estático de
verdad (NEEDED=0).
Van 2 de 11: samurai, pkgconf.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
samurai declaraba link=static y salía DINÁMICO (NEEDED: libc.so). No usa libtool, así que el
patrón -all-static de jq/parted/shadow no aplicaba. Medido DENTRO del sandbox con readelf, sobre
un `int main(){return 0;}` trivial:
zig cc -mcpu=baseline -static → NEEDED=1 (dinámico)
zig cc -mcpu=baseline -target x86_64-linux-musl -static → NEEDED=0 (estático)
El lab exporta CC="zig cc -mcpu=baseline" SIN -target (sandbox.rs:415) ⇒ zig compila NATIVO,
detecta la musl de Alpine del rootfs (/usr/lib/libc.a existe) y enlaza contra ella IGNORANDO el
-static que el propio lab exporta por link="static". El -target fuerza la musl bundleada de zig.
Las recetas YA declaran target = "x86_64-linux-musl": el campo existe y no llega al CC. El fix
de framework re-hashea los 841 sellados ⇒ decisión aparte; por ahora va por receta.
samurai: estático de verdad (NEEDED=0), corre en el host, samu --version = 1.9.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
recipes/incoming/ es la cola del stack gráfico tawasuyu, de otro agente. Sus 7 recetas son
imports crudos de Alpine con sha256='FIXME-sha256' y deps sin expandir (clang$_llvmver, _dev,
py3-gpep517) ⇒ NO construibles por construcción. El worker las fallaba en CADA vuelta (CPU
pagada) y farm-down repetía los 6 errores al bajar, con pinta de ser nuestros.
Su trabajo YA aterrizó por la vía canónica (7131cd4 'MESA iris-only CONSTRUIDA — stack gráfico
COMPLETO'), así que la cola quedó obsoleta. Pero NO es nuestra para borrarla: 370e7b7 ya la
parqueó una vez como cruft y 5126a8b tuvo que revertirlo. Dejamos sus ficheros en paz y sólo
los sacamos de QUEUES.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Petición explícita del usuario: gioser.net es FIJO y NO TIENE BACKUP. Vive en el
MISMO proyecto hcloud que los workers, y Hetzner no da tokens por-recurso: el
token que el dead-man switch necesita para auto-borrarse puede borrar CUALQUIER
server del proyecto. farm-down era peor: borraba por NOMBRE leído de .fleet sin
verificar NADA — un nombre equivocado en esa lista y adiós.
Dos capas, porque una sola no basta cuando el fallo es irreversible:
1. LISTA NEGRA por nombre (gioser*) — explícita y legible.
2. LABEL role=hammer-worker — sólo se borra lo que NACIÓ de farm-up. Ésta es la
capa fuerte: no depende de mantener una lista al día. Un server que no es
worker no se borra, punto.
VERIFICADO contra los servers vivos, no en teoría:
gioser labels=map[] ⛔ PROTEGIDO
hworker-4 labels=map[role:hammer-worker] BORRABLE
Y .fleet sólo contiene hworker-4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dos fallos de raíz, no uno:
1. HABÍA DOS CAMINOS de crear workers. harkaq-vol.sh ya tenía volumen Y
automuerte desde la 1ª campaña; farm-up.sh no tenía ninguna de las dos.
hworker-4 nació por farm-up ⇒ 5 días idle con 37G que nadie salvó. La
protección existía y el camino que usé la esquivaba. Ahora farm-up monta el
MISMO volumen (harkaq-cosecha) ⇒ collect/close cosechan y clausuran ambos.
2. NI harkaq-vol salvaba los artefactos: su rsync excluye /store y el volumen
sólo guardaba verdicts/ y logs/. Los 438 artefactos KDE se habrían perdido
igual.
FIX: el store del worker ES /mnt/cosecha/store (symlink desde /store).
Sellar YA es persistir: cada artefacto está en el volumen en el instante en que
se crea. "Guardar lo generado" deja de ser un paso que puede fallar antes de
morir y pasa a ser la estructura.
⇒ la automuerte puede ser INCONDICIONAL. Quitada la guarda "no borrar si hay
cosecha pendiente": era el bug de hworker-4 con otra cara — el worker se queda
VIVO justo cuando hay trabajo que salvar. Un switch que se desarma solo cuando
más falta hace no sirve. Antes de morir sólo sync+umount: no es guardar (ya está
guardado), es cerrar la puerta al salir.
El volumen sobrevive al server a propósito (~€0.044/GB/mes). El baseline €0 lo
da harkaq-vol.sh close, que es decisión del usuario, no de un timer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hworker-4 estuvo 5 DÍAS idle (carga 0.00, sin crontab, sin loops, 37G de KDE
sin cosechar) quemando dinero. El usuario había pedido esto explícitamente
—"los vps se autoapagan cuando se detectan idle por un tiempo"— y de sus 3
puntos (volumen / auto-apagado / cosecha) implementé el 1 y el 3. Éste faltaba.
EL FALLO ERA ESTRUCTURAL, no un olvido: farm-down.sh existe pero es MANUAL. El
modelo "efímero" dependía de que el agente se acordara de llamarlo — y si su
contexto se corta, o la sesión muere, el server queda vivo para siempre. Un
invariante que necesita que alguien recuerde NO es un invariante. Por eso el
switch vive EN EL WORKER: para apagarse no necesita ni al hub ni a mí.
BORRA, no apaga: en Hetzner un server apagado SIGUE COBRANDO (disco + IP). Un
poweroff daría sensación de ahorro y seguiría facturando. Precio: el token vive
en el worker (/etc/hammer-deadman.env 0600). Riesgo real y consciente; se acepta
porque la alternativa MEDIDA fue peor: 5 días de VPS idle.
Cuenta por INACTIVIDAD CONTINUA, no por antigüedad: cualquier señal de trabajo
(hammer build, worker-loop, heartbeat <30min) resetea los ticks. Guarda
/var/lib/hammer-no-borrar aborta el borrado si hay cosecha pendiente.
systemd timer y NO cron, por la lección medida: un cron "validado a mano" nunca
disparó porque el PID 1 de aquella máquina (arje-zero) no tenía crond. Validar
la LÍNEA no es validar que un demonio la ejecute. y el
farm-up comprueban que el timer quedó ACTIVO — evidencia, no fe.
Cableado en farm-up: todo worker NACE con el switch puesto.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Segunda de las 3 con gcc oculto en las fases (tras bzip2). La receta declaraba
compiler=zig-cc y su compile pasaba CC=gcc ⇒ el gcc de Alpine hacía el trabajo.
El frente contaba compiler="gcc" y no miraba las fases, así que pigz nunca
figuró como deuda. Queda cargo-edit de esa terna.
CC="$CC" usa el zig cc que el sandbox exporta por default (lib.rs:269,
Compiler::ZigCc no pisa CC).
Verificado: 0 NEEDED, corre en el host (pigz 2.8), comprime/descomprime,
bit-repro b3:f5614e5b6b0a ×2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
367 ficheros, lista intacta vs el sellado viejo. b3:2a937a913b52
LDFLAGS="-all-static -no-pie" inyectado en las líneas make de compile e install.
samurai revertida: con -static su binario (samu) SIGUE dinámico ⇒ el control la
echó atrás. libcap revertida: mi regex rompió el TOML (parse error línea 37);
su make ya pasa CC/BUILD_CC/AR explícitos y necesita mano, no regex.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El audit tomaba 'ls -d store/*-<n> | head -1' = orden ALFABÉTICO POR HASH, no el
artefacto vigente. El store guarda TODOS los sellados de una receta (expat tenía
5, de junio a hoy), así que auditaba uno de hace tres semanas. Mismo bug de
clase que comparar libpng.a con libpng16.a por find|head -1.
Con 'ls -dt' (más reciente): 28 → 11. Tres de las supuestas mentirosas
(cargo-audit, git-absorb, yazi) nunca lo fueron: su artefacto reciente ya era
estático y el audit leía el viejo.
Lo que NO cambia: el hallazgo central es real y verificado a mano. libtool
ignora el -static del lab, el curl sellado NO arrancaba en el host, y de las 14
que arreglé cada una se verificó contra sus artefactos previos (expat: los 4
viejos con NEEDED=1, el nuevo con 0). Estaban rotas de verdad.
HONESTIDAD escrita en el script: 'más reciente' ≠ 'vigente'. Lo vigente sería el
artefacto cuyo hash corresponde a la receta de HOY, y hammer no lo expone sin
construir (no hay hammer hash / dry-run). Por eso el audit va DESPUÉS del
rebuild, nunca antes.
Quedan 11: file, helix, libcap, libgcrypt, pcre2, pkgconf, samurai, tuc,
util-linux, xz + naabu (nuevo: Go con libc.so.6 de GLIBC, otro caso).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fases multilínea: LDFLAGS inyectado en las líneas make de compile e install.
Control de lista de ficheros intacta: libwebp 33, kbd 689, sudo 32, lsof 4.
libwebp b3:138b70a6962f · kbd b3:248de1120197 · sudo b3:6fdbe83990ef ·
lsof b3:d30ddb01b86e
mandoc revertida: no usa libtool ("Unknown Clang option: '-all-static'") y con
-static pierde binarios (apropos, demandoc) ⇒ el control la echó atrás. Necesita
otro enfoque, como xz y pcre2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
parted es el caso didáctico: YA tenía -all-static en compile — era el precedente
que citaban jq/shadow/procps-ng — y aun así salía dinámico, porque le faltaba en
INSTALL. libtool RELINKEA el binario al instalar y ahí pierde el flag. El
patrón citado estaba a medias, y por eso el propio precedente mentía.
libarchive: mismo patrón, forma de fase con comillas dobles.
Control de lista de ficheros intacta: parted 23, libarchive 55.
parted b3:1f2046dde4dd · libarchive b3:238f1fdfc704
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mismo patrón de libtool: LDFLAGS="-all-static -no-pie" en compile Y en install.
El control duro (lista de ficheros vs sellado viejo + 0 NEEDED en TODOS los
ejecutables, si no revierte) hizo su trabajo: pcre2 salió del build con
pcre2grep y pcre2test todavía dinámicos y se revirtió sola. Sin ese control
habría entrado como "arreglada" — es la misma trampa de xz, que devolvía rc=0
con el artefacto mutilado.
sqlite b3:235b18d98f18 (8 ficheros) · libgpg-error b3:8ccbe32ccb41 (36) ·
fontconfig b3:ad960af29ef5 (65) — todas con la lista de ficheros intacta.
Quedan 20: 9 con forma de fase distinta (mandoc, libwebp, kbd, parted, sudo,
libarchive, lsof, samurai, pkgconf, libcap: van a mano), pcre2 y xz que
necesitan otro enfoque, y 5 Rust (helix, yazi, git-absorb, cargo-audit, tuc)
que arrastran libgcc_s por crt-static, no por libtool.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mismo patrón que curl: LDFLAGS="-all-static -no-pie" en compile Y en install,
porque libtool relinkea al instalar.
Verificado con un CONTROL que xz obligó a añadir: no basta rc=0 + hash. El
build de xz con -all-static devolvió rc=0 y hash válido, pero el artefacto SALIÓ
SIN NINGÚN BINARIO (el viejo tenía xz, xzgrep, xzdiff, xzless, lzmainfo): con
-all-static, libtool no produce la lib compartida (liblzma.la -rpath) y el
enlace de los ejecutables se saltea EN SILENCIO. Un "éxito" que mutila el
paquete. xz queda revertida: necesita otro enfoque (separar la lib de los
binarios), no este patrón.
⇒ el criterio ahora compara la LISTA DE FICHEROS del artefacto nuevo contra la
del viejo, además de NEEDED=0. Los tres pasan: 15, 13 y 114 ficheros, idénticos
a sus sellados previos.
expat b3:e6990b3e555f · libpng b3:f65887051274 · libxml2 b3:c4c65c4b17b8
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El frente contaba `compiler = "gcc"` y no miraba las FASES. bzip2 declaraba
compiler=zig-cc y decía "migrado de gcc, matar-gcc 2026-07-16" en la cabecera,
mientras su compile pasaba `CC=gcc`: migración cosmética, cambió el campo y no
el build. El Makefile de bzip2 además HARDCODEA CC=gcc, así que un `make` a
secas tampoco habría usado zig.
Barrido: 7 recetas invocan gcc en sus fases sin declararlo. 4 son escapes ya
conocidos (los 3 kernels + cmake); las otras 3 son deuda que nadie contaba:
bzip2 (ésta), pigz y cargo-edit.
De paso cumple link=static: el -static que hammer exporta (lib.rs:261) se
pierde si la receta no lo pasa al make de un Makefile custom.
OJO -static y NO -all-static: -all-static es flag de LIBTOOL (patrón de
jq/parted/shadow/procps-ng); bzip2 usa Makefile crudo y el flag llega tal cual
al compilador → "error: Unknown Clang option: '-all-static'".
Verificado: 0 NEEDED, corre en el host, comprime/descomprime, bit-repro
b3:48b91bb1b658 ×2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Primera de las 28 que static-audit.sh destapó. libtool ignoraba el -static del
lab (lo lee como "preferí mis .a"), así que el curl sellado salía dinámico con
NEEDED libz.so.1 + libc.so — y libc.so es el soname de la musl de zig, que en
el host son 255B de linker script. Resultado: el artefacto NO arrancaba fuera
del sandbox ("Error relocating /lib/libz.so.1: __snprintf_chk").
Fix: LDFLAGS="-all-static -no-pie" en compile Y en install (libtool relinkea
al instalar); nunca en configure, donde rompería los link-tests.
Verificado: 0 NEEDED, corre en el HOST (curl 8.20.0, OpenSSL/3.5.4, zlib/1.3.1)
y hace HTTPS real (http=200). Bit-repro: b3:19a919b28c25 ×2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Destapado migrando procps-ng. libtool lee el -static del lab como "preferí mis
.a", NO como flag al linker: el binario sale dinámico y la receta jura que es
estático. Con gcc pasaba igual — no es regresión de zig, es un agujero que
nadie había medido.
Tres consecuencias medidas, no teóricas:
1. NO CORREN. El curl sellado en el host: "Error relocating /lib/libz.so.1:
__snprintf_chk: symbol not found". Su NEEDED libc.so es el soname de la musl
de zig; en el host /lib/libc.so son 255B de linker script. El artefacto sólo
funciona dentro del sandbox.
2. ARRASTRAN GCC. helix, yazi, git-absorb, cargo-audit y tuc traen libgcc_s.so.1
de Alpine DENTRO del binario. Deuda de matar-gcc que ningún compiler="gcc"
declara: invisible para el frente entero hasta ahora.
3. Un NEEDED es una entrada no declarada — lo que harkaq mide en build, pero en
RUNTIME. Rompe el cono de affected.py (SDD 17 §4): un CVE en zlib no
alcanzaría a un curl que se declara estático.
Script, no gate duro: fallar hoy rompe 28 selladas de golpe, varias del sistema
base (curl, util-linux, sudo). Primero se arreglan con evidencia, después se
cierra la puerta. Exit 1 mientras haya mentirosos ⇒ sirve de gate en CI cuando
la lista llegue a cero.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
31 migradas. La justificación escrita ("gueto, como vim/nano/htop") había
caducado: vim/nano/htop ya estaban en zig-cc. Tres problemas reales, ninguno
"zig miscompila":
1. rpl_realloc: la musl que zig bundlea (estática) devuelve NULL para
realloc(p,0); AC_FUNC_REALLOC acierta y hace #define realloc rpl_realloc,
pero procps-ng no trae esa función (la pide por AC_LIBOBJ y ningún
Makefile.am usa @LIBOBJS@) ⇒ roto upstream en cualquier libc que conteste
"no". Se suprime el renombrado por cache vars; no afirmamos que realloc sea
GNU-compatible. Afectará a todo autoconf con AC_FUNC_REALLOC/MALLOC.
2. SIGSEGV: el binario salía musl-DINÁMICO con NEEDED libc.so. libtool lee el
-static del lab como "usá mis .a", NO como flag al linker ⇒ link=static
nunca se cumplió, ni con gcc. Fix con el patrón de jq/parted/shadow:
LDFLAGS="-all-static -no-pie" en compile Y en install (libtool relinkea al
instalar), nunca en configure.
3. UBSan: zig-cc lo activa por defecto; ps --sort=-rss aborta en sortformat.c
(offset sobre puntero nulo, UB genuino de upstream pero inocuo). Patrón de
libarchive/dwarves: -fno-sanitize=undefined.
Bit-repro verificado; 18 binarios responden y ps --sort da salida idéntica a gcc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sin parches ni flags: el config.sub de wget 1.25.0 ya conoce -musl* y el
configure no se atraganta con AR="zig ar". Bit-repro verificado (2 builds
desde cero, mismo hash) y la ruta openssl viva (descarga HTTPS real, exit 0).
Como link=static, el ELF no tiene sección dinámica: cero NEEDED ⇒ no arrastra
libgcc_s/libstdc++ de Alpine. El comentario viejo listaba gcc como parte de la
de-Alpinización, que era justo al revés; corregido.
30 migradas. Quedan 17: 12 Rust con sys-crate C (cc-rs invoca el compilador del
sistema desde los build-scripts), 4 matraca dura, procps-ng en vuelo.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
zlib-ng: el veredicto T5 hecho receta — dispatch SIMD en runtime en vez de
variantes v3; modo ZLIB_COMPAT, zlib.toml intocada (dep del frente rust).
dwarves: pahole para destrabar sched-ext; el kernel NO lo declara aún (el
análisis de repro BTF/pahole-skew va primero). Ambas al worker.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- harkaq-trace.c: fanotify FAN_MARK_FILESYSTEM — agnóstico de mount-ns, el
tracer en el HOST ve los open() de dentro del bwrap (ptrace lo deniega el
seccomp D4; audit sólo emite denegaciones). Crudo y tonto a propósito
(reparto Q1c); D9: sin fanotify ⇒ exit 3 SinEvidencia, jamás traza vacía.
- harkaq-trace-norm.py: canoniza (drop /proc /sys /dev /tmp /out /src, dedupe,
orden estable — la lección nº1 de META MODE) + atribución por dep con
--resumen = la señal de PODA (T1.4): dep declarada con 0 toques = grasa.
- VALIDADO: modo --fs como root (worker, 5s, sin tocar colas): capturó
exactamente zlib.h + 3×libz.a de un cat/head; laptop sin privilegio ⇒
SinEvidencia honesto. Piloto completo (traza de un hammer build real +
resumen de poda) pendiente de un rebuild natural en el worker.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
El bug que declaró `busybox` en 29 recetas y rompió binutils: harkaq-suggest veía
`/bin/busybox` denegado, encontraba `recipes/busybox.toml` en el store y decía
"declarar dep: busybox" — sin mirar que la política YA lo concede (`ro /bin/busybox`:
el sandbox corre `sh -c` y /bin/sh→busybox ⇒ es CONTRATO, no dep).
Fix: harkaq-suggest ahora lee los paths CONCEDIDOS de la política (`ro`/`rw`/`list`),
no sólo los `# expect`. Si un path está concedido ⇒ categoría RUNTIME BASE: no se
declara, aunque el store lo provea.
Las 4 clasificaciones verificadas tras el cambio:
/bin/busybox → RUNTIME BASE (ya concedido; NO declarar) ← el fix
/usr/bin/make → DECLARABLE (declarar dep: make)
/usr/lib/libstdc++.so → IRREDUCIBLE (nadie lo provee)
/usr/bin/gcc en broot → DEUDA DE COMPILADOR (compiler=gcc)
/usr/bin/gcc en zlib → sin deuda (sonda esperada; zlib compila con zig)
Con esto puesto, busybox JAMÁS se habría declarado. Cierra el TODO que dejó el
revert: la campaña automática ya no puede cimentar contrato como dependencia.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DOS ERRORES MÍOS, uno de dirección y otro técnico, los dos en la cosecha automática:
1. DIRECCIÓN: declaró `busybox` como dep en 29 recetas — cuando la Etapa C lo está
ELIMINANDO (USERLAND_COMPONENTS=[uutils,findutils,…], ya cerrada; joyas-reusables
§5 lo confirma: "uutils… Ubuntu 25.10 los envía como default"). Estaba cimentando
la deuda que el roadmap borra.
2. TÉCNICO: busybox YA está en el runtime base de harkaq (`ro /bin/busybox`, `ro
/bin/sh` — el sandbox corre `sh -c` y /bin/sh→/bin/busybox). Es CONTRATO, no dep.
Declararlo apila el busybox de hammer sobre el de Alpine y ROMPE el build:
binutils daba "cannot run C compiled programs" en las DOS máquinas. Verificado:
sin busybox declarado, binutils construye (b3:f4507dcd…). Y la cadena se explica:
binutils roto ⇒ zlib (que lo declara) tampoco construía.
Auditoría de la cosecha (diff real, no la línea completa del +): añadió sólo 5 deps
distintas — make ×73, busybox ×29, perl ×8, pkgconf ×5, binutils ×1. Sólo busybox
estaba mal; las otras 4 son deps reales medidas. busybox revertido de 30 recetas
(queda sólo en busybox.toml, pre-existente).
Es el mismo error que ya me habían señalado con otro disfraz: MEDIR BIEN Y ACCIONAR
MAL. harkaq midió correcto (el build toca /bin/busybox: es el shell); la acción
correcta no era declararlo sino reconocerlo como contrato.
+ COORDINACIÓN del §3 con Fable 5 (mismo diseño, tareas repartidas):
- Su lección casper queda CONFIRMADA y REFORZADA: la clausura de build no sólo le
FALTAN las clases del mundo (offline) — también le SOBRA casi todo (headers, gcc).
Medido: htop (estático, 0 NEEDED) no toca NADA al correr ⇒ política = su binario.
- Su "la clase viaja como campo de la ConcesionCapacidad, sin formato nuevo" se
cumple LITERALMENTE: lo firmado es format::Permisos = u32 bitmask en 36 bytes
canónicos (Ring 0) ⇒ las clases SON los bits. La cripto no se toca.
- Diseño unificado: frontera (clases, u32, declaradas) + detalle (paths, Landlock,
medidos). D3 rige en ambos.
- Reparto: clases→Fable 5; medición/harness→Opus. CONTACTO: runtime-policy.sh ahora
emite la CLASE detectada (/etc/resolv.conf→dns, /etc/ssl/certs→tls-certs, …), no
sólo el path: la medición alimenta la tabla, la tabla decide el bit.
- Consumidor esperando: plan-jaula-juegos F1 (Steam que no puede leer ~/.ssh).
+ juez.sh (§10): nombre único por corrida (con uno fijo pega en caché ⇒ falso
"sin evidencia"). Fue el juez quien destapó todo esto en su primera corrida real.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deps de Kconfig verificadas contra el tag v6.16.12 (kernel.org):
NTSYNC sin deps; SCHED_CLASS_EXT depende de DEBUG_INFO_BTF ⇒ tarea propia
(receta dwarves + repro de BTF). Re-sellado disparado en el worker.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
El SDD 16 dejó abierto: '¿el vector page-cache de copy.fail sobrevive a un
ruleset que deniegue escritura a nivel de inode? Investigar antes de afirmar
nada'. Investigado en un fs AISLADO (loop en un worker efímero, nunca el fs del
usuario):
escribir al artefacto sellado → EPERM
dd como ROOT → Operation not permitted (ni root)
machacar los bytes POR DEBAJO (device)+leer→ Input/output error
El tercero ES el vector de copy.fail: modificar el fichero por debajo del fs. El
kernel detecta la corrupción AL LEER (el Merkle no cuadra) y rechaza la lectura.
⇒ fs-verity mata el vector. Landlock read-only no alcanzaba; fs-verity sí.
Costo medido, y corrige al SDD 17 §5: 'hash = identidad' es FALSO — fs-verity usa
Merkle SHA-256, hammer direcciona con BLAKE3 ⇒ son DOS hashes, no uno. Y ext4
exige -O verity Y blocksize = PAGE_SIZE (con 1024 el mount falla:
'Unsupported blocksize for fs-verity'). No es gratis, pero es barato para lo que da.
NO se tocó el fs del laptop (habilitar verity pediría tune2fs sobre la partición
del usuario): el experimento corrió en un loop device de un worker descartable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`scripts/affected.py <pkg> [--agenda] [--sellados]`. Un CVE no obliga a
reconstruir el catálogo: obliga a reconstruir el CONO de lo que depende del
paquete, y nada más. El grafo declarado ya lo sabe.
CVE en zlib → 65 de 761 recetas (8%) — el resto NO se toca
CVE en expat → 20 de 761 (2%), las 20 selladas = el trabajo real
agenda: fontconfig → fcft → cairo → pango → dbus → wayland…
Es el `dirty_cone` de `dataflow` (crate no_std de tawasuyu) + `topo_order` como
agenda (una dep antes que su consumidor), calculados sobre las deps declaradas.
`--sellados` separa lo que hay que reconstruir de verdad (tiene artefacto) de lo
que no existe aún.
CAE SOLO DEL INVARIANTE: las deps están declaradas por hash ⇒ el cono es
computable y EXACTO. Una distro sin clausura declarada tiene que adivinar o
reconstruir todo por las dudas. Ésa es la tesis del SDD 17.
Honestidad grabada en el propio output: la frontera es exacta respecto de lo
DECLARADO. Lo que una receta usa sin declarar queda FUERA del cono — y ese es el
agujero que harkaq cierra (SDD 16). La campaña de esta semana declaró ~84 deps
que el kernel midió y nadie había escrito: sin eso, este cono habría sido
optimista. **El cono es tan bueno como la clausura**, y por eso los dos frentes
se necesitan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El #3 decía "la clausura medida por harkaq en build = permisos del binario
instalado". MEDIDO: es falso en dos ejes, y por eso NO se implementó así.
1. SUJETO: la clausura de BUILD (zlib.h, make, gcc) no es la de RUNTIME. Un build
lee headers; el binario resultante no. Dos clausuras, dos sujetos.
2. GRANULARIDAD/CRIPTO: lo que la ConcesionCapacidad firma NO es card-core::
Permissions (struct) sino format::Permisos = u32 bitmask, dentro de
`mensaje_capacidad` = hash(32)||permisos_le(4) = 36 bytes canónicos, zero-alloc,
con espejo no_std en wawa-kernel (Ring 0). Meterle paths rompería TODAS las
firmas y contradice el "capacidad = frontera física, no tabla" que el propio
plan cita como referencia.
⇒ Camino elegido: la concesión firmada QUEDA INTACTA (frontera gruesa, Ring 0) y
la clausura granular se deriva por MEDICIÓN como política Landlock en userspace,
donde Vec<String> sí cabe. Coexisten: el kernel verifica la frontera, Landlock
aplica el detalle.
runtime-policy.sh: corre el binario bajo la jaula con política mínima DERIVADA
(harkaq-base-closure, no a mano) y resta el BASELINE del lanzador (el sh deniega
locale/ld.so.cache; sin restarlo, la política del binario se lleva esa basura).
Demo real — htop (musl-estático): baseline 3 paths del sh; htop --version no tocó
NADA propio ⇒ su política de runtime es sólo `ro <su binario>`. Medido, no supuesto.
htop tiene 0 NEEDED: para un estático la política NO sale de las libs, sólo de
correrlo — lo que confirma que la técnica de harkaq es la única vía para el #3.
+ FIX del lector (lo cazó el chequeo anti-pérdida `nº ACCESS == denials`): descartaba
los ACCESS previos al canario porque hasta verlo no sabe su domain= — pero el sh
deniega ANTES del probe y esas son suyas. El kernel contaba 4 y el lector recibía 1.
Ahora se bufferizan con su domain y se rescatan retroactivamente al ver el canario.
Sin ese chequeo habría emitido una política con 1 de 4 accesos, perfectamente creíble.
(Primer intento de fix —drenar el socket al ver el deallocated— era plausible y NO
era la causa: el contador siguió en 4 vs 1.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El #2 de SDD 17, y encadena con el #1: cuando da DIVERGENCIA, esto
dice qué difiere y por qué. Hoy debuggear una no-reproducción es artesanal; esto
la vuelve mecánica y legible por máquina (lo que el bucle agéntico necesita).
Algoritmo (cruce con de tawasuyu): árbol = grafo blob+árbol hasheado ⇒
dos subárboles idénticos colapsan al mismo hash ⇒ descender SÓLO donde difiere.
O(diferencia), no O(tamaño).
Clasifica la causa, que es lo que decide la acción: solo-en-A/B (no-determinismo
de estructura) · tamaño · build-id · timestamp · path-embebido · codegen ·
contenido.
Probado: control (A vs A) → 'árboles IDÉNTICOS' ✓. Caso real (bzip2 gcc vs zig):
de 21 ficheros sólo difieren los 4 binarios → causa .
DOS VECES tuve que arreglar el diagnóstico por confundir SÍNTOMA con causa:
1. Chequeaba build-id PRIMERO ⇒ reportaba 'build-id' cuando la causa era el
codegen. El build-id es un HASH DEL CONTENIDO: difiere siempre que difiera
el código.
2. El umbral era '<1% del binario' — sonaba bien y clasificaba 1196 bytes como
build-id. Un build-id son 20 bytes (sha1). Atado al TAMAÑO REAL (<=64).
Un diagnóstico plausible no es un diagnóstico correcto.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El top-1 de SDD 17 (mayor ventaja competitiva por esfuerzo). N builders
INDEPENDIENTES reproducen el mismo hash ⇒ el binario es confiable SIN FIRMA.
Convierte la repro de QA en primitiva de distribución: cualquiera puede ser
mirror, nadie puede envenenar el repo.
scripts/consenso.sh recipes/bzip2.toml 2
builder 1: hub local (Artix) → b3:86a33b76…
builder 2: worker efímero (Ubuntu) → b3:86a33b76…
✅ CONSENSO (2/2, bit a bit)
Dos máquinas físicas, dos distros, mismo hash byte a byte. Nix/Debian no pueden
ofrecerlo (repro incompleta). Cae SOLO del invariante que hammer ya paga — que es
la tesis del SDD 17: buscar qué cae del invariante, no qué frontera agregar.
Lo originó una observación accidental: al hacer el rollout de cmake noté que el
laptop y el worker daban el MISMO hash sin que nadie lo buscara. Eso ya era
consenso ocurriendo; esto sólo lo formaliza.
Divergencia ⇒ exit 1 y apunta a why-differs (§2): una divergencia YA es
información (un canal impuro encontrado). NO firma ni publica: sólo el veredicto.
El log de transparencia (fork-proof: hash-encadenado + detección de equivocation)
ya está escrito en tawasuyu — no hay que construir un rekor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Debí evaluar esto ANTES de migrar, no después: las 29 tienen 36 consumidores,
incl. toda la cadena GUI (cairo/gtk4/pango/harfbuzz/fontconfig/libadwaita) +
mesa/mirada. 6 migradas son deps de ese stack, que por regla no se rebuildea en
el laptop.
No se pudo construir la GUI completa en ninguna máquina (laptop: python3 no
construye — gotcha conocido, verificado que es PREEXISTENTE revirtiendo mis deps;
worker golden: busybox no construye — store parcial). Así que se verificó lo que
decide el link: el ABI.
freetype 90 · expat 12 · libpng/libpng16 385 · libffi 68 símbolos globales
→ IDÉNTICOS entre gcc y zig ⇒ la GUI linkea igual.
Dos falsos positivos cazados al medir: . comparaba libpng.a contra
libpng16.a (libs distintas), y cuenta símbolos INTERNOS de los
.o que difieren entre compiladores sin tocar el ABI (294 'diferencias' espurias).
Lo correcto: mismo fichero + --extern-only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PLAN-KIKIN §4.bis + HANDOFF-KIKIN-DESDE-HAMMER: el sustrato que kikin asume
(J4 = io_uring, J4b = Landlock/seccomp del anfitrión, eBPF = observabilidad)
tiene que venir COMPILADO en los kernels que hammer distribuye — «al azar del
defconfig» no es un contrato.
- linux-generic 7.1.2: re-sellado en el worker (5c5ef06c) y VALIDADO en QEMU:
config =y los cuatro, landlock_create_ruleset/io_uring_setup/bpf_prog_load
en kallsyms, y el re-test de input de mirada pasa (5 dispositivos, salida de
emergencia procesada). Imagen mirada-usb re-empaquetada con este kernel.
- linux-metal: mismos flags; se re-sella en el próximo build de imagen metal.
- linux.toml (soberano): NO tocado — su of_tree es load-bearing del
selfhost-verify; deuda anotada en la receta para el próximo re-ancle.
- SECURITYFS queda fuera (introspección opcional; harkaq usa el probe
landlock_create_ruleset(VERSION), que no lo necesita).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contrato PLAN-KIKIN §9 (respondido en HANDOFF-KIKIN-DESDE-HAMMER.md): mirada
escribe /run/hammer/boot-select y dispara el reboot SIN salir; /run es tmpfs,
así que la selección se aplica en el apagado o nunca. arje-zero corre esto en
toda secuencia de apagado:
- sin fichero o vacío → no-op limpio (exit 0)
- con selección → activa (rollback E4) y CONSUME el canal; si falla, el fichero
queda para diagnóstico
- --select parametrizable (testeable); 3 tests nuevos (d/e/f)
- 'boot menu' documentado como HARNESS de dev/VM: en producción mirada no sale
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cerré el frente C-puras. Las 4 que no migran son matraca DURA con gcc
justificado (no inercia):
file → el binario zig SEGFAULTEA generando magic.mgc (persiste con -O0
⇒ codegen, no optimización)
linux-pam → 'cannot run C compiled programs' (binario zig no corre)
libgcrypt/libsodium → no construyen (asm criptográfico)
De 47 compiler=gcc: 29 migradas + 4 matraca dura (gueto gcc legítimo) + 11 Rust
diferidas + 3 no medidas. compiler=gcc: 47 → 18.
harkaq hizo el ciclo completo: reveló las 47 (invisibles bajo 'gcc esperada'),
midió cuáles migran, y el residuo quedó JUSTIFICADO con diagnóstico, no supuesto.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
arje inserto StopCardFromDisk en el medio de BusRequest el 2026-06-26 y
corrio Subscribe a indice 14 — el puente estaba suscribiendose con el byte
viejo desde entonces (roto en silencio; lo delato el golden test de arje-bus
el 2026-07-16, lado tawasuyu 41f4accc7). Ademas el espejo de BusEvent no
conocia EnteParked/EnteRefloored: se agregan como consumo-sin-senal
(to_lifecycle -> Option). Tests unit+e2e actualizados y verdes.
Las 2 'sin confirmar' del balance: el smoke-test no las verificaba por ser libs
sin binario, pero generan la lib con zig (libevdev.so, libmtdev.a). Migradas.
mtdev queda COMPLETAMENTE de-Alpinizado: config.sub soberano (patch de esta
sesión) + zig compiler ⇒ cero dependencia de Alpine. Era la única deuda de
soberanía del barrido harkaq y ahora está cerrada por entero.
Total matar-gcc: 29 recetas C migradas. Quedan 18 compiler=gcc (4 matraca-C
dura + 11 Rust + 3 no medidas).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Faltaba persistir en harkaq-farm-setup.sh las 2 sondas que añadí a base.policy
al revisar needs-review (cpp=frontend gcc, getent=NSS musl). Sin esto, un worker
nuevo derivaría una base.policy sin ellas y volvería a marcar irreducibles
falsos (dhcpcd/strace).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
curl (dep de muchos) y libevent verificadas en laptop: construyen+corren con zig.
Total sesión: 27 recetas C migradas gcc→zig-cc.
BALANCE de las 47 que harkaq nombró:
27 MIGRADAS ✅ (deuda histórica: zig 0.13/0.16 segfaultaba, zig mejoró)
4 matraca real: libgcrypt/libsodium (crypto asm), file (magic), linux-pam
2 sin confirmar: libevdev, mtdev (libs, smoke-test no verifica)
11 Rust diferidas (gcc para el C embebido de sys-crates, otra evaluación)
matar-gcc pasó de '47 invisibles' (harkaq las reveló al leer el compiler=) a
27 cerradas + cola nombrada. Reporte: tandas/needs-review-harkaq/migracion-zig.md
OJO: re-hashea las 27 del índice firmado 748 ⇒ re-firma en próximo packaging.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Segunda tanda. Candidatas que el VPS marcó migrable; re-verificadas en el LAPTOP
(store completo, fiable) build+corre — 14/14, cero matraca:
freetype jq libassuan libgpg-error libssh2 libuv libxml2 nano pcre2 pigz socat
tig tmux vim
pcre2 tuvo una discrepancia transitoria (falló en una prueba local temprana con
"linker version script", migró en el VPS); re-verificado limpio en el laptop:
construye Y corre. Era estado transitorio, no incompatibilidad.
Confirma el patrón: el VPS acierta en las MIGRABLES (build+corre son evidencia
real); sólo erraba en las matraca (build-falla espurio por store parcial). La
medición fiable es el laptop.
Total matar-gcc esta sesión: 25 recetas C migradas (11+14). El frente pasa de 47
compiler=gcc a ~22 (las 25 migradas + las que faltan medir + las 11 Rust). OJO:
re-hashea, índice firmado 748 necesita re-firma en el próximo packaging.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Primera tanda del rollout aprobado. harkaq nombró 47 recetas compiler=gcc;
migra-zig.sh midió que las C-puras migran a zig (~85%). Estas 11 se verificaron
en el LAPTOP (store completo, fiable): construyen con zig-cc Y —las que tienen
binario— corren limpio.
bzip2 expat json-c gzip htop less libpng libyaml zstd libffi xz
El compiler=gcc era deuda histórica: se marcaron con zig 0.13/0.16 (que
segfaultaban C clásicos), zig mejoró, el gcc quedó por inercia. Verificado
in-place: bzip2→b3:86a33b76, json-c→b3:9cbe76e3.
11 dependencias menos del gcc de Alpine. Quedan ~25 C-puras candidatas del VPS
por verificar en el laptop (el store parcial del VPS daba build-falla espurios
por deps faltantes — la medición fiable es la del laptop) + las 11 Rust
(sys-crate C, otra evaluación).
OJO: re-hashea estas 11 (estaban en el índice firmado 748); el índice necesita
re-firma en el próximo packaging.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Causa raíz del viaje 2026-07-16 (pantalla viva, teclado/mouse muertos): el apk
de mesa arrastra eudev-libs y el 'cp *.so*' del overlay pisaba el libudev.so.1
de libudev-zero. Sin udevd, el libudev de eudev no expone ID_INPUT y libinput
ignora TODOS los dispositivos en silencio; la GPU no lo delata porque smithay
la encuentra por syspath. Reproducido y verificado A/B en QEMU (usb-kbd):
eudev = 0 dispositivos; libudev-zero = 5 y Ctrl+Alt+BackSpace corta el
compositor al instante.
- 5c-bis: restaurar libudev-zero tras el overlay + aserción cmp (regresión = build roto)
- MIRADA_DEBUG_DIR → run-NNNN del pendrive (eventos.log y panics ya no mueren en RAM)
- imagen re-empaquetada con el fix + gpt en la cmdline
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
¿Las 47 compiler=gcc que harkaq nombró son factibles de migrar a zig, o hay que
matracar? MEDIDO, no adivinado.
migra-zig.sh: para cada receta, build con zig-cc + SMOKE-TEST del binario —
porque el motivo del gcc era segfault en RUNTIME, no fallo de build; un artefacto
que sella pero segfaultea es peor que gcc. Idempotente, acumula por lotes (probar
47 no cabe en un timeout).
Muestra de 13 C-puras: 11 MIGRAN (bzip2 expat json-c gzip htop less libpng
libyaml zstd libffi xz), 2 MATRACA (pcre2: linker version script que zig ld no
soporta; file: subdir magic). ≈85% yield.
VEREDICTO: es GRANJEADA para las ~36 C-puras. El compiler=gcc era deuda histórica
— marcadas con zig 0.13/0.16 (que segfaultaban C clásicos), zig mejoró, el gcc
quedó por inercia. ~85% migran solas; el residuo (~15%) es matraca individual por
causas concretas del build-system.
Las 11 Rust con sys-crate C son otra evaluación (gcc para el C embebido de
libgit2-sys, no el Rust) — se deja para después.
Rollout = decisión del usuario (re-hashea recetas del índice firmado 748). Correr
migra-zig.sh sobre las 47 en la granja VPS da la lista final; promover las
migrables re-firma el índice. Reporte: tandas/needs-review-harkaq/migracion-zig.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El firmware del tester reparo el GPT dd-eado en el POST y dejo el header
primario invalido (entradas apuntadas a LBA 2016, presentes en LBA 2):
Linux sin gpt en cmdline no cae al respaldo, no vio particiones y la
telemetria MIRADALOG murio en RAM. Con gpt el kernel usa el respaldo.
Documentado: greeter por nouveau GL validado en metal; input muerto queda
abierto con el plan de evidencia para el proximo viaje.
El límite del modelo que mtdev destapó, cerrado. harkaq clasificaba /usr/bin/gcc
como "sonda esperada" SIEMPRE, así que marcaba `Hermetico` a las recetas que
compilan CON el gcc de Alpine (compiler=gcc = CC=gcc, musl+GNU ld). Estuvo ciego
a la deuda de compilador todo el barrido.
Fix en harkaq-suggest: 5º arg = la receta medida. Si usa compiler=gcc, los
frontends de gcc (gcc/cpp/c89/c99/g++/cc/ld/as) dejan de ser "esperada" y pasan a
DEUDA DE COMPILADOR (matar-gcc). Para compiler=zig-cc (default, 714 recetas) gcc
sigue siendo sonda de configure — esperada, como antes. Verificado: mtdev(gcc) →
deuda de compilador; zlib(zig) → sin deuda.
EL HALLAZGO: matar-gcc no es "{kernel, cmake}" como creía la memoria del proyecto.
Son 47 recetas (36 C puro + 11 Rust con sys-crate C) que dependen del gcc de
Alpine. harkaq lo reveló sólo cuando aprendió a leer el compiler= de la receta —
antes gcc-como-sonda-esperada ocultaba una deuda mayor que toda la que sí detectó.
Reporte: tandas/needs-review-harkaq/deuda-compilador-matar-gcc.md.
Cada una usa gcc porque zig-cc la miscompila (documentado en su receta). Acción
por receta: migrar a zig cuando zig mejore, o aceptar el escape como deuda
conocida. Ahora es una deuda NOMBRADA y contada, no invisible.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El único caso de deuda de soberanía del barrido: mtdev copiaba el config.sub/guess
del autoconf de ALPINE (/usr/share/autoconf/build-aux/) porque el suyo (1.1.6) no
conoce *-musl. harkaq lo cazó como lectura no declarada del rootfs.
CERRADO con recipes/mtdev-config-sub-musl.patch: 11 líneas, +`-musl*` a la
whitelist de OS del config.sub DEL PROPIO TARBALL. Ahora es input soberano
(hasheado con la receta), sin leer Alpine. Se quitó el `cp /usr/share/autoconf/...`
del configure. Verificado: mtdev construye (b3:47708e40) sin tocar /usr/share/autoconf.
Método (con un tropiezo honesto): casi afirmo que el `cp` era espurio porque el
config.sub en work/sources/mtdev daba exit 0 con musl — pero ese source estaba
MUTADO por un build previo (el cp lo había reemplazado por el de Alpine). El
tarball ORIGINAL fresco NO conoce musl (exit 1). Casi valido sobre un source
contaminado; me salvó chequear el exit code, no el output.
DESCUBIERTO AL CERRARLO: mtdev tenía DOS deudas de Alpine, no una. También usa
`compiler = "gcc"` (gcc de Alpine, no zig). Bajo la jaula la política deniega
/usr/bin/gcc (sonda esperada) y el configure da "C compiler cannot create
executables". Es parte de matar-gcc (kernel, cmake, mtdev), frente aparte.
LÍMITE DEL MODELO (nuevo, documentado en needs-review): harkaq clasifica gcc como
"sonda esperada" SIEMPRE, así que marca la fase Hermetico — pero para una receta
compiler=gcc, gcc es ESENCIAL y el build no compila sin él. harkaq no distingue
"gcc sondeado (ignorable)" de "gcc = el compilador". Caso límite real del
clasificador; no invalida el cierre de config.sub (producción no usa la jaula).
make declarado en mtdev (deuda declarable restante). config.sub fuera de
needs-review; queda anotado el gcc-compilador.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Test real (worker + volumen + helper), no en el papel. Las 3 piezas:
1. worker midió lz4+bzip2 → volumen
2. tras 1 ciclo idle SE AUTO-ELIMINÓ (verificado: el server desapareció solo)
3. volumen SOBREVIVIÓ con los 2 verdicts; helper efímero ccx13 los recogió al
hub y se auto-destruyó
3 bugs cazados EN EL TEST (no adivinados):
- falta --exclude /work: work/=69GB colgó el rsync del up horas
- cpx11 'unsupported' en hel1 (AMD sin stock) → ccx13
- collect llamaba harvest con 'local' (rsync a un worker inexistente): ahora
sólo recoge; clasificar es paso aparte (worker mide, hub clasifica)
+ auto-delete por API REST (curl+metadata), NO hcloud CLI (la golden no lo trae).
El ID propio sale del metadata service, no del nombre.
Cierra los 2 huecos de la 1ª campaña: idle cobrando (ahora auto-delete) y
dependencia de que yo esté viva (ahora el volumen persiste). Runbook:
docs/runbooks/harkaq-granja-volumen.md. Volumen clausurado tras el test (€0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
La arquitectura que la 1ª campaña pidió a gritos (el worker quedó idle ~4h
cobrando). Tres piezas en harkaq-vol.sh {up|collect|close|status}:
1. VOLUMEN persistente (harkaq-cosecha): los workers escriben verdicts a
/mnt/cosecha, que sobrevive a su destrucción ⇒ NO dependo de estar viva para
no perder el trabajo.
2. AUTO-SHUTDOWN (harkaq-campana-vol.sh): tras IDLE_CICLOS ciclos con 0
mediciones nuevas, el worker SE AUTO-ELIMINA. En Hetzner un server apagado
sigue cobrando ⇒ apagar de verdad es delete. Vía API REST + curl + metadata
service (NO hcloud CLI: la golden Ubuntu no lo trae; curl siempre está). El
ID propio sale del metadata, no del nombre.
3. collect: monta el volumen en un helper barato efímero (o un worker vivo),
rsync al hub, corre el harvest, destruye el helper. close: detach+delete del
volumen (deja de cobrar, ~€0.44/mes mientras exista).
+ dos fixes de la 1ª campaña horneados en el bucle: borra el artefacto -hkm tras
medir (envenenaba la caché) y los dos gates (ABI>=7 + binario-con-jaula).
El token va al worker (para auto-delete): riesgo acotado — worker efímero, sin
servicios entrantes salvo SSH-por-clave, destruido al terminar.
Bug cazado antes de gastar: las llaves {..} dentro del mensaje de ${1:?...}
cerraban la expansión (CMD salía 'status}').
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cosecha desatendida de la campaña harkaq. Cada dep de acá la denunció el audit de
Landlock en un build real: el kernel vio al build usarla sin declararla, y el
store del hub confirma qué artefacto la provee (deuda DECLARABLE, §4.5).
Recetas:bash binutils busybox bzip2
No se tocó ninguna receta con deuda IRREDUCIBLE: declarar algo que el store no
provee rompería el build en vez de arreglarlo. Esas van a tandas/needs-review-harkaq.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cosecha desatendida de la campaña harkaq. Cada dep de acá la denunció el audit de
Landlock en un build real: el kernel vio al build usarla sin declararla, y el
store del hub confirma qué artefacto la provee (deuda DECLARABLE, §4.5).
Recetas:dhcpcd strace
No se tocó ninguna receta con deuda IRREDUCIBLE: declarar algo que el store no
provee rompería el build en vez de arreglarlo. Esas van a tandas/needs-review-harkaq.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ninguna de las 5 necesitaba una receta nueva. Eran falsos irreducibles por
política incompleta + un caso real de de-Alpinización.
FALTABAN 2 SONDAS EN LA POLÍTICA: /usr/bin/cpp (frontend del preprocesador de
gcc, mismo grupo que gcc/c89/c99) y /usr/bin/getent (utilidad NSS de musl).
hammer NO las construye ⇒ se deniegan y clasifican (D3), como gcc. Sin ellas en
`# expect`, arrastraban la receta entera a irreducible. Añadidas a base.policy y
a harkaq-farm-setup.sh (para los próximos workers). Re-clasifiqué SIN re-medir
(la clasificación lee la política; los veredictos crudos no cambian).
nm/ranlib NO se añadieron como sondas: los provee binutils ⇒ son DECLARABLES.
Ponerlos como esperadas dejaría a una receta usar el ranlib de Alpine en vez de
decir "declará binutils" — lo contrario de lo que se busca.
RECUPERADAS (declaradas y fuera de needs-review):
dhcpcd → make, pkgconf strace → make
QUEDAN 3, cada una una historia distinta (README en needs-review):
ncurses fs.make_dir /usr/lib — ESCRITURA en install, no lectura de Alpine
(DESTDIR de la receta), NO de-Alpinización.
mtdev config.guess/sub del autoconf de Alpine — LA ÚNICA deuda de soberanía
genuina del barrido (parchar mtdev o escribir recipes/autoconf.toml).
perl /etc/hosts,/etc/resolv.conf — config de sistema para tests de red; el
build completa igual. Runtime base o denegación benigna, no Alpine.
Resultado del barrido completo: de 83 recetas, la deuda de soberanía REAL es UN
caso (mtdev/autoconf). Todo lo demás era declarable o sondas.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El laptop corre arje-zero como init (no systemd, systemctl no existe) y no tiene
crond activo. El crontab que instalé anoche corrió 0 veces: validé que la LÍNEA
corría a mano, nunca que un daemon fuera a dispararla. Mismo patrón que me mordió
toda la sesión — validar la pieza, no el sistema.
Disparo real: scripts/farm/harvest-loop.sh con setsid nohup. Sobrevive el fin de
sesión, no un reboot (eso pediría un servicio /etc/init.d, con root).
+ 2 bugs del harvest arreglados: flock anti-solape (dos ciclos editando+commiteando
a la vez = árbol corrupto) y el rescate rechazaba recetas legítimas por la línea
en blanco que add-deps añade (el + vacío no matcheaba [deps].build).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cosecha desatendida de la campaña harkaq. Cada dep de acá la denunció el audit de
Landlock en un build real: el kernel vio al build usarla sin declararla, y el
store del hub confirma qué artefacto la provee (deuda DECLARABLE, §4.5).
Recetas:coreutils elfutils expat file flex gawk
No se tocó ninguna receta con deuda IRREDUCIBLE: declarar algo que el store no
provee rompería el build en vez de arreglarlo. Esas van a tandas/needs-review-harkaq.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cosecha desatendida de la campaña harkaq. Cada dep de acá la denunció el audit de
Landlock en un build real: el kernel vio al build usarla sin declararla, y el
store del hub confirma qué artefacto la provee (deuda DECLARABLE, §4.5).
Recetas:sassc scdoc sed shadow socat sqlite tar tmux tree tzdata util-linux vim wget when which wpa_supplicant xorriso xz zlib
No se tocó ninguna receta con deuda IRREDUCIBLE: declarar algo que el store no
provee rompería el build en vez de arreglarlo. Esas van a tandas/needs-review-harkaq.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cosecha desatendida de la campaña harkaq. Cada dep de acá la denunció el audit de
Landlock en un build real: el kernel vio al build usarla sin declararla, y el
store del hub confirma qué artefacto la provee (deuda DECLARABLE, §4.5).
Recetas:gettext-tiny giflib git gnupg gperf gzip htop iproute2 jq kbd libassuan libcap libevent libffi libgcrypt libgpg-error libksba libnl libpng libsass libsodium libssh2 libudev-zero libusb libwebp libxml2 libyaml linux-generic linux-headers linux-metal linux-pam linux lz4 mandoc mtools musl nano npth openssh openssl parted pciutils pcre2 pigz procps-ng python3 readline rsync samurai ca-certificates curl doas dosfstools e2fsprogs fontconfig freetype
No se tocó ninguna receta con deuda IRREDUCIBLE: declarar algo que el store no
provee rompería el build en vez de arreglarlo. Esas van a tandas/needs-review-harkaq.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
La golden hornea un target/release/hammer viejo y el rsync excluye /target ⇒ si
el cargo build del provisioning no corre, el worker construye alegremente SIN
JAULA y produce cero veredictos.
PASÓ: la primera corrida molió 2 recetas con un binario del 29/06 (0 ocurrencias
de 'harkaq' en strings) y las marcó 'sin veredicto' como si fuera culpa de las
recetas. Ocho horas así son la noche entera perdida, y el log habría dicho
'salteada' — nunca 'estoy ciego'. Es el falso Hermetico de D9 mudado al
orquestador: la ausencia como respuesta tranquilizadora.
Ahora aborta si strings no encuentra harkaq en el binario. Mismo criterio que el
gate del ABI: no medir es mejor que creer que se mide.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Estado escrito en el repo y no en mi cabeza: la campaña tiene que sobrevivir a
que yo desaparezca al final del turno. Qué corre, dónde, cómo revisarlo por la
mañana, cómo pararlo, y los rastrillos que ya se pagaron (el cd del cron, el
pull sucio, cargo fuera del PATH).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sin esto el `git pull --rebase` del cron de cosecha falla con 'unstaged changes'
y la cosecha nunca sincroniza con el otro agente: idle silencioso de noche, que
es justo lo que la campaña existe para no tener.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El problema real no es el coste de la tanda: es que una pregunta a las 3am son 8
horas de idle. Estas 3 piezas están hechas para que la noche no se desperdicie.
harkaq-campana.sh (worker) bucle autónomo: construye cada receta bajo la
jaula y guarda los veredictos crudos. Idempotente, saltea
lo medido, duerme y re-escanea. NUNCA bloquea esperando: un
build que falla no detiene la cola (y su veredicto interesa
igual — es cuando más importa saber qué tocó fuera). Gatea
por ABI>=7: moler la noche para producir "no sabemos" es
peor que no moler.
harvest-harkaq.sh (hub, cron) hermano de harvest-go.sh: baja veredictos,
clasifica contra el store COMPLETO, aplica las deps
DECLARABLES y commitea con `git add` explícito. La deuda
IRREDUCIBLE NO se toca (declarar algo que el store no
provee rompe el build en vez de arreglarlo) → needs-review.
harkaq-add-deps.py editor de recetas conservador: idempotente, preserva las
deps existentes, y ANTE LA DUDA NO TOCA (valida que el
resultado siga parseando como TOML y no encoja). Corre de
noche sobre la fuente de verdad del catálogo: un fichero mal
editado a las 3am no da un error, da una receta corrupta que
nadie mira hasta el lunes.
POR QUÉ NO HAY REGEX EN EL CAMINO CRÍTICO: intenté sacar la lista de "a quién le
falta declarar make" con uno y falló dos veces en la misma tarde. `\bmake\b`
colaba `cargo-make` (el guión ES límite de palabra). La versión estrecha perdía
LAS CINCO recetas donde harkaq había medido la deuda de verdad (`compile = "make
…"`: el carácter previo es una comilla). El número bailó 69→45→99 según el
retoque. Construí una herramienta de medición y después intenté adivinar con un
regex — la lista buena la da el kernel. El regex queda sólo para ORDENAR la cola.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cmake reconstruido en un worker efímero (regla: la cadena GUI no se rebuildea en
el laptop, el zig-skew rompe cairo) y cosechado al hub:
b3:834132e7…-cmake NEEDED = sólo libc.musl-x86_64.so.1
El viejo (99864dcc…, con libstdc++.so.6 + libgcc_s.so.1) queda en el store por si
algo lo referencia.
LOS 10 CONSUMIDORES NO SE RECONSTRUYERON, Y NO HACE FALTA. El fix quita la
dependencia de RUNTIME del artefacto DE CMAKE. Los consumidores sólo lo usaron
como HERRAMIENTA DE BUILD — sus artefactos no linkean libstdc++. Rebuildearlos
sólo daría frescura de hash, y eso pasa solo en su próximo build. Confundir "usó
la herramienta" con "linkea la librería" habría costado un rebuild de gtk4 para
nada. (Yo mismo había propuesto los 11; el radio real es 1.)
REGALO DEL ROLLOUT: el worker (Ubuntu 24.04, ccx23) y el laptop (CachyOS)
produjeron cmake con el MISMO HASH, bit a bit — el invariante central de hammer
confirmándose entre dos máquinas distintas sin que nadie lo buscara. Y como los
bytes son idénticos, el NEEDED del worker queda verificado por transitividad: no
hizo falta readelf allá (que además no está instalado en la golden).
Limpieza: removido store/834132e7…-cmake-nogcc (mi artefacto experimental).
harkaq-suggest INDEXA el store, así que un artefacto huérfano aparecería como
"proveedor" de paths y ensuciaría los diagnósticos.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
recipes/linux.toml: -e SECURITY -e SECURITY_LANDLOCK -e AUDIT. Era lo único que
faltaba para que harkaq corra DENTRO de hammer (VM/metal) y no sólo en el laptop
y la granja. El CONFIG_LSM del defconfig ya lista `landlock` de primero ⇒ bastó
encenderlo, sin tocar la cadena de LSMs.
AUDIT se fija explícito aunque ya viniera =y por defconfig: sin él el kernel no
emite un solo registro y harkaq certificaría TODO como hermético en silencio
(§3.4). Es la precondición de la que cuelga la evidencia; no se deja al azar de
un default.
VERIFICADO BOOTEANDO, no leyendo el .config — que dice lo que se compiló, no lo
que el kernel hace al arrancar. Misma disciplina de §3.1 (el ABI se consulta por
syscall, jamás por versión) llevada a la verificación: el único que sabe si
Landlock está vivo es el kernel vivo. scripts/harkaq/vm-abi-probe.c es un /init
de initramfs mínimo que pregunta y apaga:
===== HARKAQ EN EL KERNEL DE HAMMER =====
LANDLOCK ABI = 7
audit de denegaciones (>=7): SI
=========================================
Kernel nuevo: b3:f2583d61… (el hash cambia, como se esperaba; el viejo
34755ff2… decía "# CONFIG_SECURITY_LANDLOCK is not set").
Con esto las 4 precondiciones de la Fase 1 están cerradas y harkaq corre en las
tres máquinas del proyecto: laptop (ABI 10), granja (ABI 7 tras el bump de la
golden) y el kernel propio de hammer (ABI 7).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
D4 declaraba seccomp "obligatorio, no opcional" y harkaq-exec tenía CERO
seccomp: el documento afirmaba algo que el código no hacía. Cerrado.
CORRECCIÓN DELIBERADA A D4: pedía ALLOWLIST por syscall, se implementó DENYLIST.
Un allowlist para builds ARBITRARIOS (compiladores, make, shells, linkers, perl)
es un blanco móvil que cada herramienta nueva rompe — el riesgo del §7 ("los
falsos positivos matan proyectos de sandboxing") aplicado a las syscalls, y con
peor final: un build que muere por una syscall legítima no da un diagnóstico
útil, da un misterio. Lo PELIGROSO sí es enumerable y estable: no hay build
honesto que cargue módulos, haga kexec o attachee un ptrace.
Denegadas: io_uring_*, ptrace, process_vm_{readv,writev}, bpf, userfaultfd,
keyctl/add_key/request_key, {init,finit,delete}_module, kexec_*,
perf_event_open, mount/umount2, open_tree/move_mount/fs*, setns.
pivot_root NO: bwrap lo usa ANTES de llegar a harkaq-exec.
EPERM y no KILL: matar deja un cadáver sin explicación; EPERM deja al build
fallar donde corresponde y al log decir por qué. Se instala DESPUÉS de
no_new_privs y de Landlock, justo antes del exec, y se hereda por fork/exec como
el dominio (D5). Si el kernel lo rechaza NO se corre el build (D7: media jaula
creyéndose entera es peor que ninguna).
Chequeo de arquitectura antes del número de syscall: los nros son POR ARCH y sin
el check un binario i386 podría colar otra syscall con el mismo número — el error
clásico de los filtros seccomp a mano.
COMPROBADO: ptrace → EPERM bajo la jaula (control positivo), y un build REAL de
zlib sale Hermetico ×3 con el hash del artefacto IDÉNTICO al de antes de seccomp
(b3:adc5c251…). Añadir media jaula no movió un byte — como debe ser: esto recorta
superficie de escape, no cambia el build.
El --seccomp <fd> de bwrap quedó SIN USAR: harkaq-exec ya corre dentro y con
no_new_privs puesto, así que instala el filtro él mismo — una pieza menos de
plumbing y el filtro queda al lado de la política que lo justifica.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
La única deuda irreducible del barrido (§4.10) era /usr/bin/perl en
ca-certificates y curl. Dos hallazgos al abrirla:
1. EL COMENTARIO DE ca-certificates.toml YA LO DECÍA, escrito por quien la portó:
"El resto del build (mk-ca-bundle.pl → cert.pem → split-ca-bundle.sh) usa el
perl + coreutils del rootfs". La deuda estaba escrita EN PROSA y era invisible
para el sistema; harkaq la convirtió en un veredicto. Es la tesis del §0 en una
línea: no hace falta que nadie DESCUBRA nada, hace falta que la máquina pueda
VER lo que ya se sabía.
2. LA RECETA TAMBIÉN EXISTÍA: recipes/incoming-kde/perl.toml, importada el 13/07
para la campaña KDE (syntax-highlighting e intltool exigen perl). CUARTA
convergencia independiente: dos campañas necesitaban el mismo binario y
llegaron por caminos que no se hablan.
Construye tal cual (b3:c7a899bd…) y con el artefacto en el store:
/usr/bin/perl → declarar dep: perl exit=0 ⇒ CERO deuda irreducible
EL BARRIDO DE 24 RECETAS QUEDA EN 0 IRREDUCIBLES. La métrica del §7 (<5%) se
cumple midiendo lo que la métrica quería medir: cosas que no sabemos construir.
Resultado: ninguna.
Receta promovida a recipes/perl.toml (no había canónica ⇒ sin colisión). Lo que
queda es mecánico y no es diseño: declarar `make` en las ~10 recetas que lo usan
y `perl` en ca-certificates/curl. OJO: eso re-hashea sus artefactos y son
paquetes del base-system (índice firmado 748) ⇒ decisión de rollout, no un sed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
harkaq-farm-setup.sh: provisiona un worker para barrer (idempotente; gatea por
ABI>=7 y aborta si no llega — con la golden vieja el barrido daría SinEvidencia
en TODO). Deriva el runtime base DEL ROOTFS DEL WORKER, no copia el del laptop:
es por-rootfs (§4.3) y los sonames dependen de la versión de Alpine.
Barrido: 24 recetas, 14 con veredicto. Primer número crudo: 4 irreducibles (29%).
ERA FALSO, y por dos motivos propios:
1. GAP DE POLÍTICA. Los `list` salían sólo de los ancestros de la clausura ⇒ una
receta SIN deps (bash) no generaba ni uno y todo escaneo del árbol caía como
denegación (/usr/lib). Listar NO es leer: Landlock separa READ_DIR de
READ_FILE ⇒ la estructura del árbol es CONTRATO. Se puede `ls /usr/lib` sin
leer un solo fichero no declarado; la deuda es leer lo ajeno, no saber que
existe. Idem /var/tmp: con --tmp-overlay / es scratch descartable, como /tmp.
2. La heurística del catálogo es por NOMBRE y un fichero no se llama como su
paquete: /usr/bin/ranlib lo trae `binutils`, /usr/bin/diff lo trae
`diffutils`. El único que sabe la verdad es el STORE (conoce la lista de
ficheros de cada artefacto) — y el worker tiene 103 artefactos contra los
cientos del hub.
⇒ EL WORKER MIDE, EL HUB CLASIFICA. Misma separación que lector/clasificador: el
que tiene el privilegio —o los datos— hace lo mínimo. Reclasificado en el hub:
bash ranlib,/usr/lib,/var/tmp → nada (gap de política)
doas /usr/bin/diff → nada (diffutils lo provee)
ca-certificates /usr/bin/perl → /usr/bin/perl
curl /usr/bin/perl → /usr/bin/perl
RESULTADO: la deuda irreducible de toda la muestra es UN path — /usr/bin/perl
(2 de 14 = 14%). No hay recipes/perl.toml ni artefacto: deuda genuina y nombrada.
Todo lo demás era declarable (make ×10, ranlib→binutils, diff→diffutils).
Sigue por encima del <5% del §7, pero el perfil es el que importa: NO hay cola
larga de sorpresas. La deuda de un catálogo entero se resume en "declarar make" y
"no tenemos perl".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Los 10 cierres que caen solos de los 3 invariantes (bit-repro, CAS,
clausura-como-política), cada uno cruzado con el crate/arquitectura
reusable de tawasuyu que lo abarata. Top-2: consenso de reconstrucción
(fork-proof+umbral) y hammer why-differs (format+reconcile).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
El VPS va a ser el compilador permanente ⇒ harkaq TIENE que correr ahí o el
frente queda como decoración de laptop. Verificado de punta a punta en un worker
efímero desde la golden, no en el papel:
antes: kernel 6.8.0-134 → Landlock ABI 4 → sin audit → SinEvidencia siempre
después: kernel 6.17.0-40 → Landlock ABI 7 → AUDIT ✓
`apt install linux-image-generic-hwe-24.04` (archivo estándar de Ubuntu 24.04, sin
PPA ni cambiar distro), reboot, y la cadena COMPLETA de evidencia corre igual que
en el laptop:
same-exec 2 registros · new-exec 0 (el ciego) · new-exec-logon 4
domain=14e9f83c2 blockers=fs.read_file path="/etc/passwd" dev="sda1" ino=133259
El store del catálogo (103 artefactos) sobrevivió el bump intacto.
NUEVA GOLDEN: snapshot 408909310 "hammer-golden-harkaq-6.17-2026-07-15".
farm-up.sh pasa a usarla por defecto. La vieja (405120842) queda como fallback.
+ harkaq-uapi.h — EL HALLAZGO QUE IMPORTA para un compilador continuo: **el kernel
y los headers envejecen por separado**. El worker corre 6.17 pero su
linux-libc-dev es 6.8 y NO define NADA de lo necesario: ni los flags de log de ABI
7/8, ni AUDIT_LANDLOCK_ACCESS/DOMAIN (¡los tipos de registro!), ni IOCTL_DEV de
ABI 5. El kernel puede; el compilador no sabe pedírselo.
Es el mismo error de §3.1 al revés: allá, deducir el ABI de la versión del kernel;
acá, de la versión de los headers. NINGUNA de las dos dice la verdad — la única
fuente es el syscall en runtime. Estas constantes son números de contrato de UAPI,
estables por definición, seguros de fijar con #ifndef.
Sin esto harkaq sólo compila en distros con headers al día, que es justo lo que un
compilador continuo NO puede exigir. Y el modo de falla habría sido el peor: sin
AUDIT_LANDLOCK_ACCESS el lector filtraría por un número que no conoce y vería CERO
denegaciones — el falso `Hermetico` de D9, esta vez por headers viejos.
Nota: ABI 7 da el audit (lo que el proyecto necesita); TSYNC (ABI 8) pide 7.0 y no
está — es robustez opcional de D5, no un bloqueo.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
harkaq (§4.6) dejó UN solo caso irreducible en el primer barrido: brotli pidiendo
libstdc++.so.6.0.34 + libgcc_s.so.1. Pero brotli es C, no C++ — la libstdc++ no
era suya: era de `cmake`, su dep de build.
store/99864dcc…-cmake/usr/bin/cmake
NEEDED: libstdc++.so.6, libgcc_s.so.1, libc.so
"gcc retenido para {kernel, cmake}" no era una concesión de BUILD-TIME como
sonaba: el artefacto de cmake arrastraba el runtime C++ de Alpine hacia dentro de
CADA build que lo declarara como dep. Un agujero de soberanía viajando por el
grafo de deps, invisible en la receta del consumidor. harkaq lo señaló desde el
consumidor, que es donde se ve.
EL ARREGLO NO TOCA EL COMPILADOR — gcc sigue compilando cmake (camino
conocido-bueno; zig c++ segfaultea el cmake mínimo). Sólo deja de enlazar su
runtime en dinámico:
CXX='g++ -static-libstdc++ -static-libgcc' LDFLAGS='-static-libstdc++ -static-libgcc'
Medido:
antes NEEDED: libstdc++.so.6, libgcc_s.so.1, libc.so
después NEEDED: libc.musl-x86_64.so.1 (y corre: cmake version 3.31.6)
EL PAGO: brotli —el único irreducible del barrido— pasa a Hermetico ×3 fases,
artefacto sellado b3:bc900676…. El barrido queda 0 irreducibles de 6. El caso que
iba a contar contra el <5% no era una receta mal escrita: era una herramienta de
hammer filtrando Alpine.
+ /proc como superficie de CONTRATO (lo destapó el configure de brotli, que lee
/proc/cpuinfo y /proc/meminfo): bwrap monta un /proc FRESCO dentro del pidns, no
sale del rootfs Alpine y no ve al host ⇒ contrato, no deuda. Mismo caso que
/cache. El hash del artefacto es idéntico antes y después de añadirlo: cambia el
veredicto, no el build.
COSTO DEL ROLLOUT: cambiar recipes/cmake.toml re-hashea cmake y sus 3
consumidores (brotli, libjpeg-turbo, libtiff = 6 sellados). Radio chico, PERO
libjpeg-turbo y libtiff son la cadena GUI y la regla es no rebuildearla en el
laptop (zig-skew rompe cairo) ⇒ el rebuild va al worker.
LO QUE NO CIERRA: /usr/bin/gcc, c89, c99, ldd y el plugin LTO (§4.7) siguen
siendo SONDAS — la jaula las deniega, los builds completan igual, y denegarlas es
lo correcto. El gcc de Alpine sigue en el rootfs y sigue haciendo falta para
{kernel, cmake} en BUILD-TIME. Lo cerrado es la filtración a RUNTIME, que es la
que contaminaba artefactos.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diagnosticar no vale nada si no se puede accionar. El bucle completo sobre
recipes/zlib.toml, cada paso guiado SÓLO por lo que dijo harkaq:
zlib tal cual → Impuro: /usr/bin/make → "declarar dep: make"
+ make → Impuro: /usr/bin/ranlib → "declarar dep: binutils"
+ binutils → Impuro: liblto_plugin.so → irreducible ⇒ clasificar
final → Hermetico ×3 fases, artefacto b3:adc5c251… SELLADO
Cada capa que se pela destapa la siguiente, y TODAS convergen en el gcc de
Alpine. El último hallazgo es el que no se encuentra a mano: los binutils DE
HAMMER —ya declarados como dep— cargan el plugin LTO del gcc DE ALPINE
(/usr/libexec/gcc/x86_64-alpine-linux-musl/15.2.0/liblto_plugin.so). No es un
fallo de declaración de la receta: es un agujero de soberanía DENTRO de un
artefacto que hammer construye.
Va a denegación esperada por la misma razón que /usr/bin/gcc (§4.2): el build
completa sin él ⇒ es una SONDA, no una necesidad, y bloquearla es DESEABLE —
impide que los binutils de hammer usen el plugin de Alpine por detrás. D3 otra
vez: no se calla, se clasifica.
Detalle que confirma el modelo: el hash del artefacto es IDÉNTICO antes y
después de clasificar la sonda (b3:adc5c251… las dos veces). Clasificar cambia
el VEREDICTO, no el BUILD. Es el principio de H1 sosteniéndose solo: la
evidencia es comportamiento, no identidad (recipe.rs:228, y por eso Evidence
está fuera de hash_inputs).
Y eso es lo que `Hermetico` significa ahora, con todo el peso: el kernel
certificó que ese build no usó NADA fuera de su clausura declarada, y el canario
prueba que el certificado no es el silencio de un lector roto.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
La métrica del §7 (<5% de recetas necesitan excepción) medía otra cosa. zlib
salió Impuro por /usr/bin/make y parecía runtime base: NO lo es. recipes/make.toml
existe, store/fbad44ac…-make está sellado y SWAP_MAKE es uno de los swaps del
selfhost-verify ⇒ a zlib le falta una LÍNEA en [deps], no una excepción.
declarable el store ya provee el path ⇒ arreglo mecánico → NO cuenta
irreducible nadie lo provee ⇒ receta nueva o runtime base → SÍ cuenta
harkaq-suggest.py cruza cada path de deuda contra el store (el mapeo de §4.1 al
revés: el path relativo dentro del artefacto ES el path del sandbox):
/usr/bin/make → declarar dep: make
/usr/lib/libz.so.1.3.2 → irreducible (la zlib de hammer da libz.a estático,
no .so ⇒ NO se arregla declarando)
El diagnóstico deja de ser "algo pasó" y pasa a ser "hacé esto".
PRIMER BARRIDO (6 recetas C, fuentes cacheadas):
Hermetico ............... 0
sólo deuda DECLARABLE ... 5 ← y la deuda es EL MISMO path en las 5: /usr/bin/make
con deuda IRREDUCIBLE ... 1 (brotli)
Lo que importa: la deuda de 5/6 es UN SOLO path y se arregla con una línea. No
hay cola larga de excepciones — que era el riesgo de muerte del §7.
Y el caso irreducible es la frontera CONOCIDA: brotli (C++) pide
/usr/lib/libstdc++.so.6.0.34 y /usr/lib/libgcc_s.so.1 — el runtime C++ del gcc de
Alpine, que hammer no construye. Es exactamente lo que la campaña "matar gcc" ya
tenía identificado. TERCERA vez que harkaq llega por su cuenta a una lista que
otro frente ya tenía: los swaps del selfhost-verify (§4.3), los binutils (§4.4)
y ahora el runtime C++.
Honestidad: 6 recetas no son 750 y son las fáciles. 1/6 = 17%, muy por encima del
<5% — pero el único caso es un problema conocido, nombrado y compartido con otros
dos frentes, no una cola de sorpresas. El barrido grande es trabajo de granja.
Bug del barrido encontrado por el propio barrido: copiar la receta a otro
directorio rompe la resolución de deps.build (son relativas al dir de la receta)
⇒ brotli fallaba con "no pude cargar la dep 'cmake'" y se descartaba EN SILENCIO
— o sea que se descartaban justo las recetas CON deps, las interesantes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
crates/hammer-build/src/harkaq.rs + Sandbox::bwrap_args_harkaq: harkaq sale de
`hammer build`, no de un script. Deriva la política de la clausura declarada
(D1), lanza el lector, mete harkaq-exec como último eslabón antes del `sh -c`, y
recoge el Verdict por FASE (configure/compile/install son bwraps distintos ⇒
dominios distintos ⇒ un veredicto cada uno).
EL HITO, sobre recipes/zlib.toml (receta real del catálogo):
configure → Hermetico esperadas (14): /usr/bin/gcc deuda: ninguna ✓
compile → Impuro DEUDA (1): /usr/bin/make → rc=126, la jaula lo frenó
El diagnóstico completo de zlib cabe en una frase: lo único que toma de Alpine
sin declararlo es `make`.
INERTE sin HARKAQ=1: mismos args de bwrap, mismo entorno, ningún proceso extra.
Requisito duro, no cortesía — 700+ artefactos sellados no pueden cambiar de hash
por encender un diagnóstico. Los 57 tests previos del crate siguen verdes sin
tocar, que es la prueba.
harkaq-policy es PURO y se testea sin kernel (4 tests nuevos, como pide §4):
traduce store→sandbox, excluye la metadata `.hammer/` del artefacto, y deja
listables los ancestros de cada fichero de la clausura.
La integración se delató sola en su PRIMERA corrida real: faltaba /cache como
superficie de contrato (ZIG_GLOBAL_CACHE_DIR=/cache/zig — zig CREA dirs ahí) y
la fase moría con `fs.make_dir /cache/zig/tmp`. De ahí que las superficies de
contrato las decida el Sandbox (que sabe qué montó) y no harkaq: /cache sólo
existe si hay cache_dir, y una política que nombre un path inexistente aborta el
build a propósito.
libc en hammer-build: Child::kill() manda SIGKILL y el lector moriría MUDO, sin
emitir el Verdict — justo lo que no queremos de un componente cuyo producto ES
el veredicto. Hace falta SIGTERM.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
harkaq-base-closure.py: deriva el cierre dinámico del runtime base. Es D1
aplicado a la base — mantenerla a mano sería el "alguien mantiene un perfil de
permisos" que D1 dice que mata a todos los sandboxes. No usa ldd (resolvería
contra el HOST, no contra el rootfs Alpine): lee los DT_NEEDED del ELF y
resuelve dentro del rootfs por las rutas de musl. Emite symlink Y destino: el
kernel denuncia el fichero real (libz.so.1.3.2) pero el build abre por el nombre
corto (libz.so.1).
VALIDACIÓN: el cierre derivado de {sh,busybox,bash,coreutils,env} reproduce
EXACTAMENTE las 7 librerías que las rondas 2-3 habían descubierto a mano, en una
sola pasada, y además caza los symlinks y libc.musl-x86_64.so.1 que se habían
escapado. El método es computable, no adivinado.
§4.4 — con el entorno del Sandbox real replicado (CC="zig cc -mcpu=baseline",
AR, SOURCE_DATE_EPOCH, LC_ALL=C), configure llega mucho más lejos: 205
denegaciones del kernel, y clasificadas:
esperadas (170): /usr/bin/gcc, /usr/bin/ldd ← sondas de compilador
DEUDA (34) en 8 paths:
/usr/bin/{ld,nm,objdump,strip} ← binutils de ALPINE
/usr/bin/{make,getconf}
/usr/lib/gcc/x86_64-alpine-linux-musl ← libdir del gcc de ALPINE
/opt ← bug de política
Es la tesis del §0 hecha dato: un configure que se creía hermético va a buscar
los binutils y el libdir de gcc de Alpine. Y la clasificación es lo que lo hace
legible — sin ella son 205 denegaciones planas y el hallazgo queda enterrado;
con ella son 8 paths accionables. Coincide con los swaps del selfhost-verify y
con la campaña "matar gcc" (recipes/binutils.toml existe justo porque zig provee
as/ld/ar pero el resto se toma de Alpine).
Bug de política encontrado por el propio experimento: /opt sale como deuda
porque la política concede `ro /opt/zig` pero no deja LISTAR el padre. Regla
general: todo directorio concedido necesita `list` en sus ancestros, o el escaneo
del padre es un falso positivo. harkaq-policy.sh ya lo hace para la clausura de
las deps; falta para las superficies de contrato.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
harkaq-verdict.py: clasifica las denegaciones crudas. Va DELIBERADAMENTE fuera
del lector — éste corre con CAP_AUDIT_READ y clasificar es POLÍTICA, no
privilegio (mismo argumento que Q1c). De yapa: se itera sin recompilar el
binario capabilitado y sin perder el setcap. Las expectativas viajan en la
propia política como `# expect <path>` (una sola fuente de verdad; harkaq-exec
las ignora como comentario). SinEvidencia manda sobre todo: si el lector no es
confiable no se clasifica nada — reinterpretarlo sería el falso Hermetico que el
canario existe para impedir.
MODE=base rc=0 Hermetico esperadas: /usr/bin/gcc deuda: ninguna ✓
MODE=zlib rc=1 Impuro DEUDA: /usr/lib/libz.so.1.3.2
§4.3 — ¿aguantan los 5 paths en otra forma de build? Medido con un configure de
autotools REAL (libgpg-error, MODE=configure):
ronda 1: /bin/bash, /bin/coreutils
ronda 2: libacl, libattr, libcrypto, libreadline, libutmps
ronda 3: libncursesw, libskarnet
ronda 4: /usr/bin/c89, /usr/bin/c99, /usr/bin/ldd, /usr/bin/make
NO: el runtime base es POR FORMA DE BUILD (~16 entradas para autotools, no 5).
Pero converge en 3 rondas (34 denegaciones → 5) y se queda chico ⇒ la métrica de
<5% de la Fase 2 sigue siendo plausible.
Hallazgo: **el runtime base no es una lista de binarios, es su CIERRE de .so**
(bash arrastra libreadline+libncursesw; coreutils arrastra libacl+libattr). Es
computable con ldd, no adivinable ⇒ harkaq-policy debe derivarlo igual que
deriva la clausura de las deps. Misma idea, otro origen.
Y el residuo es todo señal, nada ruido: c89/c99/ldd son sondas de compilador de
Alpine (→ esperadas, como gcc) y /usr/bin/make es una decisión de diseño real —
hoy los builds de hammer usan el make de Alpine sin declararlo. Es EXACTAMENTE
lo que los swaps del selfhost-verify reemplazan uno a uno: la lista de deuda de
harkaq y la lista de swaps del bootstrap son la MISMA lista, descubierta por dos
caminos independientes. Que coincidan es la mejor validación externa del método.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Método (lo importante): NO se adivina, se mide. MODE=base corre SIN ninguna dep
declarada ⇒ sin clausura que pueda explicarlas, toda denegación es runtime base
por definición. Aísla la respuesta sin juicio de valor.
MODE=base, hello.c mínimo con zig cc: 18 denegaciones, 4 paths distintos
(contador del kernel 19 = 18 + canario ✓):
8× /dev/urandom 6× /usr/lib/os-release 3× /dev/null 1× /usr/bin/env
Y el build salió rc=0: no son cosas que el build NECESITE, son cosas que INTENTA
— pero salen en todos los builds y sin tratarlas entierran la deuda real.
La partición cierra en TRES categorías, todas medidas (§4.2):
- Contrato del sandbox: /src /out /tmp /dev/null(rw) /dev/urandom /proc
/opt/zig — los monta BWRAP, no son Alpine ⇒ no son deuda. Ésta era la línea
divisoria que faltaba.
- Runtime base Alpine: /bin/sh /bin/busybox /lib/ld-musl /usr/bin/env
/usr/lib/os-release. CINCO. ⇒ el resto de Alpine es deuda genuina, y eso
hace viable todo el planteo.
- Denegación ESPERADA (no estaba en el diseño): /usr/bin/gcc. zig cc lo sondea
3× y el build sale rc=0 igual ⇒ NO es ruido a permitir, es la jaula haciendo
su trabajo: concederla dejaría a zig invocar el gcc de Alpine por detrás,
justo lo que la campaña "matar gcc" persigue a mano. Es D3 literal ("una
denegación esperada no se calla, se CLASIFICA") y confirma que no tener
`quiet` era correcto: silenciarla habría borrado el hallazgo.
El contraste con el mismo runtime base:
MODE=base rc=0 Impuro [ /usr/bin/gcc ] ← esperada
MODE=zlib rc=1 Impuro [ /usr/lib/libz.so.1.3.2 ] ← DEUDA PURA, sin ruido
Consecuencia de diseño para el Verdict: las denegaciones tienen que venir
CLASIFICADAS (base|esperada|deuda), no en lista plana. `Hermetico` debe
significar "cero deuda", no "cero denegaciones" — si no, ningún build real lo
alcanzaría jamás.
Gotchas medidos: /dev/null es destino de ESCRITURA (rw, no ro) y el piso duro es
/bin/sh→busybox→ld-musl (sin eso ni el canario corre ⇒ sólo SinEvidencia).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CORRECCIÓN ESTRUCTURAL del §4 (otra herencia del marco nix). El Draft 2 decía
`fs_ro: [store paths de la clausura]`. Falso: esos paths NO existen dentro del
sandbox. `Sandbox::bwrap_args` apila las deps con --overlay-src y las FUNDE en
el mismo /usr que el rootfs Alpine, a propósito, para que el compilador las
encuentre sin plumbing de flags. Medido:
/usr/include/zlib.h (dep DECLARADA) dev=63 ino=13641973
/usr/include/stdio.h (Alpine, NO declarado) dev=63 ino=4196788
/usr/include (el directorio) dev=61 ino=15 ← overlayfs, uno
Una regla sobre /usr concede las dos ⇒ la evidencia no valdría nada. Ni `dev`
distingue. ⇒ La clausura se enumera FICHERO A FICHERO (computable: en el host
sabemos qué aporta cada dep). Dos consecuencias: `list` ≠ `ro` (pkgconf NECESITA
escanear /usr/lib/pkgconfig, pero listar no es leer; Landlock separa READ_DIR de
READ_FILE) y máscara por tipo (derechos de-sólo-dir sobre un fichero ⇒ EINVAL y
la regla entera se cae).
Piezas nuevas: harkaq-policy.sh (deriva la clausura, D1) + q2-runtime-base.sh
(responde Q2 midiendo, no adivinando) + harkaq-exec con `list`/máscara por tipo.
Q2, primeros datos con un build REAL en el sandbox REAL contra la dep zlib:
estado: Impuro | canario: True | contador kernel: 3
fs.read_file /usr/bin/env
fs.read_file /usr/lib/libz.so.1.3.2 ← la zlib de ALPINE!
El build se linkaba contra la zlib de Alpine en vez de la dep declarada (que
aporta libz.a). Sin harkaq eso pasa en verde y el artefacto queda dependiendo de
Alpine. ES EXACTAMENTE EL BUG DEL §0, cazado en un build real.
Piso duro del runtime base: /bin/sh → /bin/busybox → /lib/ld-musl (sin eso ni el
canario corre ⇒ el mínimo es "lo que hace falta para que el canario pueda correr").
Dos rastrillos: el canario no puede depender de un binario (`cat` moría en
/bin/cat ⇒ ahora `read _ < $CANARY`, sólo builtins) ni vivir en /tmp (la política
concede `rw /tmp` ⇒ el canario caía DENTRO de la clausura y no denegaba nada).
El segundo lo cazó D9 MISMO: el lector se negó a certificar en vez de decir
Hermetico. El canario funcionando como se diseñó, sobre un bug de quien lo diseñó.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tres piezas nuevas, las del §4 del SDD:
- harkaq-audit.c lector de evidencia; HOST junto a hammerd, CAP_AUDIT_READ;
espera el canario (que revela el domain=), junta las
denegaciones de ESE dominio, cruza contra el contador del
kernel al liberarse, y emite el Verdict JSON de 3 estados.
- harkaq-exec.c aplica la política y execea el builder; DENTRO de bwrap,
estático (rootfs musl). Negocia el ABI por syscall (D7:
<min rechaza, <7 avisa SIN EVIDENCIA) y pone
LOG_NEW_EXEC_ON + TSYNC.
- harkaq-run.sh encadena lector+bwrap+exec, pone el canario con nonce y
espera a que el lector confirme que escucha (--ready-fd)
antes de arrancar el build: sin esa barrera el canario se
emitiría sin nadie escuchando y daría SinEvidencia por una
carrera, no por un problema real.
El hito, con comandos sintéticos sobre el kernel real (ABI 10):
limpio {"estado":"Hermetico","canario_visto":true,"contador_kernel":1,"denials":[]}
impuro {"estado":"Impuro","canario_visto":true,"contador_kernel":2,
"denials":[{"blockers":"fs.read_file","path":"…/README.md",…}]}
Canario visto y contador coherente en ambos ⇒ los 3 estados de D9 funcionan.
Q1c contestada en la práctica: el privilegio vive en un helper chico con
`setcap cap_audit_read,cap_audit_control`, NO en hammerd entero.
Sutileza medida: lo que DAC ya bloquea es INVISIBLE para harkaq (el hook del
LSM no llega a correr). Descubierto porque el escenario "impuro" con
/root/.bashrc (drwx------) dio Hermetico: DAC lo bloqueó antes que Landlock.
No rompe D2 (si DAC lo bloqueó, el build tampoco lo usó ⇒ Hermetico es cierto)
pero acota el diagnóstico, y al escribir tests hay que elegir paths que DAC
permita o el control no controla nada.
Falta para cerrar Fase 1: harkaq-policy derivando de la clausura real + el
contraste receta-de-Alpinizada vs. receta-que-no. El rebuild del kernel
(SECURITY_LANDLOCK) NO bloquea el hito: sólo hace falta para que harkaq corra
DENTRO de hammer; el laptop ya está en ABI 10.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Banco: scripts/harkaq/q1b-attrib.c. Dos builds CONCURRENTES en bwrap
--unshare-all (los ns de Sandbox::bwrap_args), sin privilegio (uid 1000),
canarios con nonce distinto, lector en el host como root.
(a) ✅ Los registros CRUZAN el userns/pidns/mountns (6 registros llegaron al
lector del host) y cruzan con builds SIN privilegio, que es como corren de
verdad. ⇒ harkaq-audit vive en el host junto a hammerd; la arquitectura
del §4 se sostiene.
(b) ✅ El canario con nonce ATRIBUYE: AAAA=1 BBBB=1, cero cruces con 2 builds
en paralelo. ⇒ la granja puede construir en paralelo sin contaminarse la
evidencia entre builds.
Tres hallazgos colaterales que valen más que el veredicto:
- El contador quedó VALIDADO: dos `deallocated denials=1`, uno por dominio,
cuadrando con 1 ACCESS cada uno ⇒ el chequeo anti-pérdida (nº ACCESS ==
denials) está medido, no supuesto.
- La identidad sobrevive a los ns: pid=8498 uid=1000 son del HOST (adentro
del pidns la víctima es pid 1).
- dev+ino identifican el OBJETO, no el BUILD: los 2 canarios dan el mismo
ino=4457713 (ambos bindean /etc/hostname). Atribuir por dev/ino habría
fundido dos builds leyendo el mismo fichero no declarado — el caso más
común que harkaq va a ver. La clave es domain=, y el canario la revela.
⇒ D9 hace TRES trabajos con un mecanismo: honestidad (§3.4), clave primaria
(§3.5) y, vía el contador, detección de pérdida.
Nota de método: esta corrida también falló 2 veces más, y las 2 el banco
afirmó algo falso con total confianza. (1) `cat $canario >/dev/null` — /dev no
estaba en la clausura ⇒ sh fallaba en el REDIRECT y cat no corría; el rc=1 no
era del canario. (2) Como root, bwrap no pudo ni ejecutar el binario y el
veredicto imprimió "NADA cruzó ⇒ replantear la arquitectura" a partir de cero
estímulo (causa: --unshare-all mapea sólo uid 0→0; CAP_DAC_OVERRIDE en userns
sólo vale sobre uids MAPEADOS ⇒ root-en-userns no atraviesa un home
drwx------). Fix: drop_privs() + gate de estímulo (exit 4 si una víctima no
sale con status 0).
Van 3 veces en una sesión que un cero fue el instrumento y no el kernel, con
quien escribía el banco mirando de frente ese riesgo. harkaq sin canario no es
un riesgo de configuración: es el comportamiento por defecto.
Quedan Q1c (privilegio de harkaq-audit: mejor un helper acotado que hammerd
entero) y Q1d (qué ruta reporta exe= con --bind /src).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Q1 era el gate del proyecto (¿llegan los registros AUDIT_LANDLOCK_* a un
lector?). Medido en el laptop (ABI 10) con scripts/harkaq/q1-audit.c:
✅ VIABLE. El multicast AUDIT_NLGRP_READLOG entrega
blockers=fs.read_file path="/etc/passwd" dev="nvme1n1p6" ino=4458369
+ exe=/comm= del que lo intentó. Es el diagnóstico de de-Alpinización
que el SDD promete, saliendo del kernel sin instrumentar nada.
Dos precondiciones que el Draft 2 no tenía, ambas medidas:
- audit_enabled=1 NO viene de fábrica (sin audit=1 en cmdline, sin auditd)
⇒ cero registros de cualquier escenario. AUDIT_SET o audit=1 horneado.
- LOG_NEW_EXEC_ON es OBLIGATORIO: same-exec=2 registros, new-exec=0,
new-exec-logon=4. harkaq restringe y DESPUÉS execea el builder ⇒ sin el
flag certificaría herméticos TODOS los builds, denials=[], sin un error.
D9 (nueva, no negociable): el canario lo fabrica harkaq. Se investigó si el
registro `deallocated denials=N` servía de verificación cruzada gratis: NO.
Medido con ventana de 6s, un dominio con flags default y denegación post-exec
no emite NADA (ni allocated, ni access, ni el contador); y el `allocated` es
perezoso (llega pegado al primer denial logueado) ⇒ su ausencia tampoco prueba
nada. Un build hermético y un lector ciego son bit-idénticos: denials=[]. El
Verdict pasa a 3 estados (Hermetico / Impuro / SinEvidencia).
El contador SÍ sirve para lo otro: nº ACCESS == denials caza registros
perdidos por backlog (riesgo real con la granja en paralelo).
Nota de método (§3.4): la 1ª corrida dio 0 en los 3 escenarios y "confirmaba"
la hipótesis. Era el instrumento roto (AUDIT_GET con NLM_F_ACK: el ACK llegaba
antes que la respuesta → enabled=-1 → nunca se encendía el audit). Se cazó
sólo porque el CONTROL (same-exec) también dio 0, y eso era imposible. Es el
modo de falla de harkaq en vivo sobre su propio banco. El test ahora aborta
(exit 4) si no confirma audit_enabled=1.
Nuevas Q1b (¿el lector ve a través del userns de bwrap? ¿cómo se atribuye un
registro a SU build con la granja en paralelo?) y Q1c (privilegio de
harkaq-audit).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El Draft 1 estaba escrito contra el sandbox de nix. hammer no construye con
nix: construye con bubblewrap (ADR 0004, hammer-build/src/sandbox.rs). Eso
invalidaba por irrelevancia el argumento anti-nix (§0), las FOD y su red
abierta (§6 + D8), Q5, y la Fase 3 del fetcher mediado — que el propio doc
marcaba como el mayor riesgo de cronograma, de "meses".
Fase 0 ejecutada (5 minutos, no días):
- ABI Landlock = 10 en el laptop (techo: audit/7, TSYNC/8, quiet/10).
- Kernel de hammer 6.16.12-metal: CONFIG_SECURITY_LANDLOCK ausente, pero
CONFIG_LSM ya lista landlock ⇒ falta un flag, no una versión. Q2 cerrada.
- Cero FODs: download.rs baja con curl en el HOST y --unshare-all incluye
--unshare-net ⇒ el fetcher mediado de D8 ya está construido. Fase 3 se
elimina.
La brecha real es otra y paga mejor: el sandbox monta un rootfs Alpine ENTERO
como capa base ⇒ política ⊋ clausura ⇒ una receta usa Alpine sin declararlo y
pasa en verde. Es el bug que la campaña de de-Alpinización caza a mano. D1 +
audit de ABI 7 lo vuelve mecánico (path/dev/ino de lo no declarado).
Encaje en el modelo de confianza (recipe.rs:229): H1 garantiza que lo
declarado PASA; harkaq que lo declarado BASTA. Complementos simétricos.
Sobreviven intactos D1 (política derivada), D2 (evidencia negativa) y D3
(cero quieting). Nuevo Q1 = el riesgo técnico real: cómo llegan los registros
AUDIT_LANDLOCK_* al lector (CAP_AUDIT_READ, auditd, userns, flags de exec).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TELEMETRÍA (el pedido): el rootfs de la imagen es un initramfs = RAM pura y los logs iban a /run (tmpfs)
⇒ al apagar la máquina ajena no sobrevivía NADA. Ahora la imagen lleva 2da partición FAT32 MIRADALOG
(512M), montada por findfs+LABEL con -o sync (un cuelgue de GPU o corte de luz no se lleva los logs).
Cada arranque vuelca run-NNNN/ con: resumen (modo/GPU/driver), hardware (PCI vía sysfs, módulos, DRM,
input, firmware), pantallas (status/modes por conector), dmesg ANTES y DESPUÉS de mirada, seatd, y el log
del compositor+greeter con RUST_LOG=wgpu_core=debug + EGL_LOG_LEVEL=debug + LIBGL_DEBUG=verbose (para ver
por qué wgpu eligió el backend y si hay float16). Shell de rescate + . FAT32 a propósito: el
pendrive se lee después desde cualquier SO.
metal-usb-sdboot.sh gana DATA_MB/DATA_LABEL genéricos (partición de datos opcional en la GPT).
GOTCHAs verificados antes de confiar: el kernel linux-generic SÍ trae VFAT_FS=y + NLS + USB_STORAGE=y
(sin eso no montaba); el busybox de Alpine NO trae el applet 00:00.0 Host bridge: Intel Corporation Tiger Lake-UP3/H35 4 cores Host Bridge/DRAM Registers (rev 01)
00:02.0 VGA compatible controller: Intel Corporation TigerLake-LP GT2 [Iris Xe Graphics] (rev 01)
00:04.0 Signal processing controller: Intel Corporation TigerLake-LP Dynamic Tuning Processor Participant (rev 01)
00:07.0 PCI bridge: Intel Corporation Tiger Lake-LP Thunderbolt 4 PCI Express Root Port #0 (rev 01)
00:08.0 System peripheral: Intel Corporation GNA Scoring Accelerator module (rev 01)
00:0a.0 Signal processing controller: Intel Corporation Tigerlake Telemetry Aggregator Driver (rev 01)
00:0d.0 USB controller: Intel Corporation Tiger Lake-LP Thunderbolt 4 USB Controller (rev 01)
00:0d.2 USB controller: Intel Corporation Tiger Lake-LP Thunderbolt 4 NHI #0 (rev 01)
00:12.0 Serial controller: Intel Corporation 500 Series Chipset Family On-Package Integrated Sensor Hub (rev 20)
00:14.0 USB controller: Intel Corporation 500 Series Chipset Family On-Package USB 3.2 Gen 2x1 (10 Gbs) xHCI Host Controller (rev 20)
00:14.2 RAM memory: Intel Corporation 500 Series Chipset Family On-Package Shared SRAM (rev 20)
00:14.3 Network controller: Intel Corporation Wi-Fi 6 AX201 (rev 20)
00:15.0 Serial bus controller: Intel Corporation 500 Series Chipset Family On-Package I2C Controller #0 (rev 20)
00:15.1 Serial bus controller: Intel Corporation 500 Series Chipset Family On-Package I2C Controller #1 (rev 20)
00:16.0 Communication controller: Intel Corporation 500 Series Chipset Family On-Package CSME HECI #1 (rev 20)
00:1c.0 PCI bridge: Intel Corporation 500 Series Chipset Family On-Package PCI Express Root Port #5 (rev 20)
00:1d.0 PCI bridge: Intel Corporation 500 Series Chipset Family On-Package PCI Express Root Port #9 (rev 20)
00:1f.0 ISA bridge: Intel Corporation 500 Series Chipset Family On-Package eSPI Controller (rev 20)
00:1f.3 Multimedia audio controller: Intel Corporation 500 Series Chipset Family On-Package High Definition Audio (HD Audio) (rev 20)
00:1f.4 SMBus: Intel Corporation 500 Series Chipset Family On-Package System Management Bus (SMBus) (rev 20)
00:1f.5 Serial bus controller: Intel Corporation 500 Series Chipset Family On-Package SPI (flash) Controller (rev 20)
39:00.0 Non-Volatile memory controller: Kingston Technology Company, Inc. NV2 NVMe SSD [SM2267XT] (DRAM-less) (rev 03)
3a:00.0 Non-Volatile memory controller: ADATA Technology Co., Ltd. IM2P33F3 NVMe SSD (DRAM-less) (rev 03) (sí findfs/blkid) ⇒ el dump de PCI
lee /sys/bus/pci/devices/*/ directo; sintaxis del init validada con busybox ash (no bash).
BUMP mirada: el pin era 9967b02c (18-jun) — 439 commits viejo. Ahora 9a17fefc, que incluye el fix de
portabilidad a musl que hice en tawasuyu (ioctl: glibc usa c_ulong, musl c_int ⇒ las constantes DRM
casteaban mal y mirada-compositor NO compilaba para musl). mirada-compositor ya selló (87ced528);
greeter/ctl en curso.
+ scripts/kde/{metal-desktop-image,plasma-start-metal}.sh del hilo KDE (imagen de escritorio en metal).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
spectacle 6.7.2 (7a6db883) → /usr/bin/spectacle. Cierre VERIFICADO, los 6 componentes de la cadena:
libKPipeWireRecord.so.6 + libopencv_core.so.410 + libopencv_imgproc.so.410 + libtesseract.so.5.5 +
libKF6PrisonScanner.so.6 + libKQuickImageEditor.so.1.
Cadena nueva completada en esta tanda:
ffmpeg 7.1 faafef5e libavcodec/avutil/avformat/avfilter/swscale — kpipewire los exige los
4 REQUIRED. ffmpeg NO usa autotools: su configure toma --cc/--ar/
--ranlib, y hay que darle wrappers de UN SOLO TOKEN (--ar="zig ar" con
espacio se ejecutaría como binario único — mismo gotcha que el AR de
los crates 'cc'). **--disable-autodetect** = determinismo: sin él
configure escanea el sandbox y activa libs a discreción.
libva 2.22.0 121cd0de libva.so + libva-drm.so, REQUIRED por kpipewire. Sólo la capa de
despacho; los drivers reales son RUNTIME (dlopen). Sólo backend DRM.
kpipewire 6.7.2 745665f3 KPipeWire + KPipeWireRecord. Deps: pipewire + ffmpeg + libva + gbm/EGL
(mesa) + libepoxy.
kquickimageeditor 0a4f8156 **BUMP 0.5.0 → 0.6.2.1**: spectacle 6.7 incluye
<KQuickImageEditor/AnnotationDocument>, que NO existe en la serie 0.5
(no tiene src/annotations). La 0.6.x suma deps propias: KF6Config
REQUIRED + OpenCV 4.7 (stack blur, WITH_OPENCV=TRUE por defecto).
Por qué era cara: TODO en spectacle es REQUIRED y casi nada degradable. En particular **KPipeWire no se
puede esquivar**: find_package va sin REQUIRED y set_package_properties lo marca TYPE REQUIRED (eso sí se
degrada con sed), pero src/CMakeLists.txt:101 linkea K::KPipeWireRecord INCONDICIONALMENTE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pipewire (6d15a22c) → libpipewire-0.3.so.0 + libspa-* + daemon 'pipewire' + **'pipewire-pulse'** (habla
el protocolo PulseAudio, así que los clientes libpulse ya sellados le hablan) + tools (pw-cli/pw-play/
pw-record/pw-mon/pw-dump/...). Cierra el lado SERVIDOR del audio: hasta ahora sólo teníamos el CLIENTE
(libpulse con -Ddaemon=false).
Dos frentes lo necesitaban: (1) audio real de Plasma 6; (2) spectacle → kpipewire → pipewire.
Fuente del gitlab.freedesktop.org (como networkmanager/modemmanager). Recortado al core+SPA: sin bluez5
(arrastraría los códecs aptx/ldac/aac/opus), sin jack/v4l2/libcamera/gstreamer/ffmpeg/vulkan/roc/lv2/
avahi/x11/echo-cancel-webrtc/libusb/flatpak/snap; systemd/logind/selinux/rtkit off (no hay systemd).
alsa+sndfile+libpulse+dbus+udev enabled (las 4 ya selladas).
GOTCHA static-vs-shared (variante nueva del patrón): **sin ncurses a propósito**. Su única consumidora es
la tool pw-top (monitor de terminal), guardada por 'if ncurses_dep.found()'. El ncurses del catálogo es
static-only y su .pc arrastra '-static' al link ⇒ 'error: using shared libraries requires dynamic linking'
al enlazar pw-top contra libpipewire-0.3.so. Quitando la dep, meson saltea pw-top y el resto construye.
(Alternativa futura si se quiere pw-top: variante ncurses-shared.)
Próximo: ffmpeg → kpipewire → spectacle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Selladas:
leptonica 1.85.0 e80a5d36 procesamiento de imagen; dep dura de tesseract. Codecs recortados a
las variantes .so/PIC que ya tenemos (libjpeg-turbo-shared +
libpng-shared + zlib-shared); TIFF/WEBP/OPENJPEG/GIF off.
tesseract 5.5.0 08c05238 OCR. spectacle lo exige DURO: pkg_check_modules(TESSERACT REQUIRED)
en CMakeLists:111, sin option() para apagarlo. Sólo la lib (sin
training tools, que piden pango/cairo/icu).
kquickimageeditor 0.5.0 3d4bd904 editor de imagen QtQuick; spectacle lo marca TYPE REQUIRED.
spectacle.toml se commitea CON su blocker documentado en la cabecera (misma convención que usaba
networkmanager-qt): **KPipeWire**. find_package(KPipeWire) va sin REQUIRED y set_package_properties lo
marca TYPE REQUIRED (eso se degrada con sed), PERO src/CMakeLists.txt:101 linkea
INCONDICIONALMENTE ⇒ no hay forma de compilar sin él. Falta la cadena pipewire → (ffmpeg, para
KPipeWireRecord) → kpipewire.
El resto de su cadena YA quedó resuelto y sellado esta sesión: opencv + prison-scanner + zxing-cpp +
tesseract + leptonica + kquickimageeditor + xcb-util-cursor (sin este último: 'No suitable backend
platform was found ... XCB-CURSOR').
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
zxing-cpp 2.3.0 (c98afd82) → libZXing.so: decodificador/codificador de códigos de barras y QR.
prison-scanner 6.27.0 (cbfd6dcc) → libKF6Prison.so.6 + **libKF6PrisonScanner.so.6**.
Por qué la variante: el prison canónico de esta cola se selló con WITH_ZXING=OFF/WITH_MULTIMEDIA=OFF ⇒ no
provee KF6::PrisonScanner, y spectacle corta el build en seco:
if (NOT TARGET KF6::PrisonScanner) message(SEND_ERROR "Prison built without required PrisonScanner")
prison-scanner activa WITH_ZXING=ON + WITH_MULTIMEDIA=ON (qtmultimedia ya sellado). Nombre distinto para
NO re-hashear el prison que ya consumen plasma-workspace/plasma-nm — mismo patrón que kcoreaddons-QML /
kwindowsystem-QML / qcoro-shared.
GOTCHA zxing: hay que compilar **READERS *y* WRITERS**. Con ZXING_WRITERS=OFF el header ZXing/BitMatrix.h
ni se instala, y prison NO sólo escanea — también GENERA códigos (zxingutil.cpp/pdf417barcode.cpp lo
incluyen) ⇒ 'fatal error: ZXing/BitMatrix.h file not found'.
spectacle sigue sin construir: además exige (sin flag para apagarlos) pkg_check_modules(TESSERACT REQUIRED
tesseract) — OCR, arrastra leptonica — y kquickimageeditor TYPE REQUIRED.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
opencv (79526f4b) → libopencv_core.so.4.10 + libopencv_imgproc.so.4.10 + opencv4.pc. spectacle (la
herramienta de captura de pantalla) lo exige: find_package(OpenCV 4.7 REQUIRED core imgproc), lo usa en
src/QtCV.h.
OpenCV es enorme (~95MB de fuente, decenas de módulos): se recorta con **BUILD_LIST=core,imgproc** (CMake
ni configura el resto) + todo OFF (apps/tests/docs/python/java/protobuf/DNN/GUI/video/ffmpeg/IPP/TBB/CUDA/
OpenCL/codecs). Los codecs WITH_* van OFF porque viven en imgcodecs, que no construimos.
**GOTCHA zig-wrapper DURABLE (2da vez que aparece, ahora documentado):** el wrapper clásico de esta cola
arma la lista de args en $a SIN comillas ⇒ SPLITEA cualquier arg con espacios. OpenCV pasa
-DOPENCV_ALLOCATOR_STATS_COUNTER_TYPE="long long" ⇒ zig recibe 'long' suelto y falla con
'error: long: unrecognized file extension'. Fix acá: wrapper PASS-THROUGH con "$@" (OpenCV no es
ECM/KDE, no necesita el filtro de -Wl,--fatal-warnings). Es el mismo bug que ya había pegado en
libkscreen (moc_predefs con -I"dir1 dir2").
spectacle NO construye todavía: además de OpenCV exige KF6::PrisonScanner, y el prison sellado se hizo
con WITH_MULTIMEDIA=OFF (sin el componente Scanner, que pide ZXing + Qt Multimedia).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
kinfocenter 6.7.2 (9dee4f29): bin kinfocenter + KCMs de hardware (kcm_cpu/interrupts/kwinsupportinfo/
xserver/opencl/energyinfo/...). kmenuedit 6.7.2 (d13a64f4): editor del menú de aplicaciones. Ambos sin
recetas nuevas.
DEUDA REGISTRADA — qtbase sin Vulkan: el KCM about-distro ('Acerca de este sistema') incluye
<QVulkanInstance>/<QVulkanFunctions> tanto en sus helpers (opengl-helper/vulkan-helper) como en
GPUEntry.h. Esos headers SÓLO los genera Qt6Gui si qtbase se configuró con Vulkan, y el qtbase sellado
NO lo tiene (vulkan-headers/vulkan-loader están sellados, pero los usa kwin directo, no Qt). Se podó el
KCM entero: activar vulkan en qtbase cascadearía un rebuild de TODO el stack Qt/KF6/Plasma (horas), como
ya se vio con el fix de zlib-shared → kwin(1825 obj)+plasma-workspace(1962 obj). Pendiente para una
próxima cascada planificada de qtbase (junto con otras features que falten).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
systemsettings (4c304dd8) sella a la PRIMERA: bin /usr/bin/systemsettings, NEEDED libKF6KCMUtils.so.6 +
libKF6KCMUtilsCore.so.6 + libPlasmaActivities.so.7. Cero recetas nuevas (todas las deps ya selladas) y
sin pelear DocTools: systemsettings lo declara en OPTIONAL_COMPONENTS.
Era el hueco más grande del escritorio: los KCMs se habían apagado para poder compilar plasma-desktop, y
sin systemsettings no hay shell donde alojarlos. Ahora existe el contenedor + ya hay KCMs reales
construidos por otras piezas de esta sesión: kcm_networkmanagement (plasma-nm), kcm_pulseaudio
(plasma-pa), kcm_powerdevilprofilesconfig (powerdevil), kcm_kscreen (kscreen).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
plasma-pa 6.7.2 (51d7a25b): applet QML org.kde.plasma.volume + kcm_pulseaudio + audioshortcutsservice.
Cierra el frente de audio: hasta ahora el lab sólo tenía libcanberra con backend NULL (sin reproducción).
Cadena completa nueva:
alsa-lib/speexdsp/libsndfile → pulseaudio (cliente) → pulseaudio-qt → plasma-pa
pulseaudio 17.0 (108cac6e) → libpulse.so.0 + libpulse-mainloop-glib.so.0 + libpulse-simple.so.0 (+ .pc).
- **-Ddaemon=false -Dclient=true**: se construye SÓLO la biblioteca cliente. Es lo correcto acá — el
servidor de audio previsto es PipeWire (habla el protocolo PulseAudio) pero los clientes igual
linkean el libpulse de PulseAudio. Evita todo el árbol de módulos del daemon (alsa/bluez/jack/
avahi/orc/fftw/webrtc-aec).
- -Dglib=enabled es OBLIGATORIO (sin él no hay libpulse-mainloop-glib, que plasma-pa exige);
lo habilita glib-shared.
- **GOTCHA lld durable**: PulseAudio comparte UN map-file (version script) entre libpulse,
libpulse-simple y libpulse-mainloop-glib ⇒ al enlazar cada una, el script asigna PULSE_0 a símbolos
que viven en las OTRAS: 'version script assignment of PULSE_0 to symbol pa_glib_mainloop_new failed:
symbol not defined'. GNU ld sólo avisa; ld.lld (el de zig) lo trata como ERROR desde lld 17. Fix:
-Dc_link_args=-Wl,--undefined-version.
pulseaudio-qt 1.8.1 (c6849b9e) → libKF6PulseAudioQt.so.5, selló a la primera. OJO: NO está en el path de
plasma/ sino en download.kde.org/stable/pulseaudio-qt/, y la versión viva es 1.8.x (1.6.0 del
find_package es sólo el mínimo).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
powerdevil 6.7.2 (299af51a): daemon de energía + acciones (suspendsession/dimdisplay/dpms/
brightnesscontrol) + KCMs. kscreen 6.7.2 (ef6218e6): kcm_kscreen + kscreen KDED + kscreen-console +
hdrcalibrator.
Base de audio REAL (el lab sólo tenía libcanberra con backend NULL, sin reproducción):
alsa-lib 1.2.14 c6896caf libasound.so.2 (--disable-python)
speexdsp 1.2.1 dc235b15 libspeexdsp.so.1 (resampler de PulseAudio)
libsndfile 1.2.2 c44d0142 libsndfile.so.1 (--disable-external-libs: sin FLAC/Opus, que no están
sellados; WAV/AIFF/AU basta para sonidos del sistema). Necesita python3.
Siguiente eslabón: pulseaudio → pulseaudio-qt → plasma-pa (applet de volumen).
3 lecciones de la depuración de powerdevil (dep-set de 69):
- ** FALTABA** y el síntoma era engañoso: 'Could NOT find KF6 (missing: KIO KCMUtils
XmlGui)'. La cadena real es KF6ConfigWidgets→find_dependency(KF6WidgetsAddons) ausente ⇒ ConfigWidgets
NOT_FOUND ⇒ cascadea a XmlGui/KIO/KCMUtils. Para diagnosticar hay que seguir los 'could not be found
because dependency X' hacia ARRIBA, no fiarse de los componentes que CMake lista.
- **PlasmaWaylandProtocols/WaylandProtocols/qtwayland** son deps directas de todo lo que toca Wayland
(powerdevil, kscreen). Idem los QML modules (kscreen: org.kde.kitemmodels + org.kde.plasma.plasma5support
→ kitemmodels/plasma5support directos; ambos YA traen su qml/, no hacía falta variante QML=ON).
- **timeout del build**: mató powerdevil a mitad del rebuild de plasma-workspace (mi
cambio de zlib-shared cascadeó kwin[1825 obj]+plasma-workspace[1962 obj]). Usar timeouts largos
(9000) para recetas con dep-sets grandes. Además: 24778 AUTO-MATCHEA el propio
comando de chequeo → usar .
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
plasma-nm (6080f27f) construido: applet QML org.kde.plasma.networkmanagement + kcm_networkmanagement +
plugins VPN (openvpn/openswan/ssh/sstp/strongswan/iodine/fortisslvpn). Cierre verificado:
libKF6NetworkManagerQt.so.6 + libKF6ModemManagerQt.so.6 + libqt6keychain.so.1.
Cierra el frente que plasma-workspace esquivó parcheando NetworkManagerQt a opcional. Cadena entera:
glib-shared → networkmanager (libnm) → networkmanager-qt ─┐
glib-shared → modemmanager (libmm-glib) → modemmanager-qt ───┴→ plasma-nm
Deps nuevas de esta tanda:
modemmanager 1.22.0 9e4ce41d libmm-glib. -Dqmi/mbim/qrtr=false evita libqmi/libmbim/libqrtr;
polkit=no, udev=false (+udevdir explícito, lo exige el assert).
modemmanager-qt 6.27 37e954bb KF6ModemManagerQt (REQUIRED en el find_package(KF6) de plasma-nm
aunque no haya módem).
qtkeychain 0.16.0 6a1319b0 claves WiFi. **LIBSECRET_SUPPORT=OFF**: por defecto ON y exige
libsecret-1 (llavero de GNOME); apagado queda KEYCHAIN_DBUS =
kwallet vía Qt6DBus, que es el backend que KDE usa ⇒ cero deps nuevas.
OJO: el tag de git es '0.16.0' SIN prefijo 'v'.
libxslt 1.1.43 b41cb0fc xsltproc. A diferencia de NM (donde sólo servía a man/ y se degradó),
ModemManager lo USA de verdad: include/meson.build:25 genera
ModemManager-names.h desde header-generator.xsl.
qcoro-shared 0.12.0 b8ed5b49 el qcoro canónico sella sólo .a sin PIC (su CMake no fija
BUILD_SHARED_LIBS) ⇒ 'R_X86_64_32S QCoro::detail::TaskBase<QDBusMessage>
recompile with -fPIC' al linkear plasma-nm. Variante con
BUILD_SHARED_LIBS=ON; nombre distinto para no re-hashear el qcoro que
ya consumen plasma-workspace/powerdevil.
BUILD_OPENCONNECT=OFF es importante: con ON, plasma-nm hace find_package(Qt6WebEngineWidgets REQUIRED)
⇒ arrastraría qtwebengine (Chromium), diferido por el ADR.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>