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>