diff --git a/docs/16-harkaq-jaula.md b/docs/16-harkaq-jaula.md index bce7e86c..5b7bb304 100644 --- a/docs/16-harkaq-jaula.md +++ b/docs/16-harkaq-jaula.md @@ -719,6 +719,64 @@ qué usó sin declarar, con path/dev/ino. > `/etc/passwd`. Una receta real no de-Alpinizada es *mejor demo* (es el problema que de verdad > tenemos) y de paso le entrega valor inmediato a una campaña en curso. +### 4.5 Deuda *declarable* vs *irreducible*: la métrica del <5% 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`. O sea que a zlib **le falta una línea en `[deps]`**, no una excepción. + +Esa distinción rehace la métrica del §7, que hablaba de "recetas que necesitan excepción": + +| | Qué | Cuenta contra el <5% | +|---|---|---| +| **declarable** | el store ya provee el path ⇒ una línea en `[deps]`, arreglo mecánico | **no** | +| **irreducible** | nadie lo provee ⇒ escribir receta, o aceptar como runtime base | **sí** | + +`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) y lo dice: + +``` +DEUDA DECLARABLE — el store ya lo provee; falta la línea en [deps]: + /usr/bin/make → declarar dep: make +DEUDA IRREDUCIBLE — nadie la provee: + /usr/lib/libz.so.1.3.2 +``` + +Las dos son correctas y la segunda es sutil: la receta de zlib de hammer produce `libz.a` +estático, no un `.so`, así que un consumidor que quiera el shared **no** se arregla declarando. +El diagnóstico deja de ser "algo pasó" y pasa a ser "hacé esto". + +### 4.6 Primer barrido de Fase 2 (6 recetas) + +`scripts/harkaq/fase2-barrido.sh`. Muestra chica y deliberadamente fácil (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) | + +**El resultado que importa:** la deuda de 5 de 6 recetas es **un solo path**, y se arregla con una +línea. No hay una 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 la frontera que la campaña *matar gcc* ya tenía identificada +("gcc retenido sólo para {kernel, cmake}", "lo que queda: los `*-sys` con C++"). **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 sobre la muestra: 6 recetas no son 750, y son las fáciles. 1/6 irreducible es 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, no de diseño. + +**Bug del barrido, encontrado por el propio barrido:** copiar la receta a otro directorio rompe la +resolución de `deps.build` (son relativas al directorio de la receta) ⇒ brotli fallaba con "no +pude cargar la dep 'cmake'" y se descartaba en silencio. Se descartaban **justo las recetas con +deps, que son las interesantes**. La copia va al lado del original. + +--- + **Fase 2 — `harkaq-policy` completo + medir la brecha.** Clausura real, `/src`, `/out`, `/tmp`, `/cache`, `/proc` y `/dev` mínimos, y el **runtime base** (sin `/bin/sh` ni la libc del rootfs no arranca nada — ver Q3). Correr contra N recetas del catálogo y medir la tasa de falsos positivos. diff --git a/scripts/harkaq/README.md b/scripts/harkaq/README.md index 09c6c3ab..f7b46cdf 100644 --- a/scripts/harkaq/README.md +++ b/scripts/harkaq/README.md @@ -266,3 +266,24 @@ DEUDA (34) en 8 paths: Sin clasificar son 205 líneas planas; clasificado, 8 paths accionables. **Regla que salió de acá: todo directorio concedido necesita `list` en sus ancestros**, o el escaneo del padre es un falso positivo. + +## Fase 2 — el barrido + +`fase2-barrido.sh ...` + `harkaq-suggest.py`. La métrica del SDD (<5% de recetas +necesitan excepción) medía otra cosa: una deuda que el store YA PROVEE no es una excepción, es +una línea en `[deps]`. Sólo la deuda **irreducible** cuenta. + +```sh +scripts/harkaq/harkaq-base-closure.py .dev-fs/alpine /bin/sh /bin/busybox /bin/bash /bin/coreutils /usr/bin/env > ~/.cache/harkaq/base.policy +printf "ro /usr/lib/os-release\n# expect /usr/bin/gcc\n# expect /usr/bin/ldd\n" >> ~/.cache/harkaq/base.policy +scripts/harkaq/fase2-barrido.sh recipes/zlib.toml recipes/brotli.toml ... +``` + +Primer barrido (6 recetas C, fuentes cacheadas): **5 con sólo deuda declarable —y las 5 por el +MISMO path, `/usr/bin/make`—** y 1 irreducible (brotli: `libstdc++`/`libgcc_s`, el runtime C++ de +Alpine = la frontera que *matar gcc* ya tenía identificada). No hay cola larga de excepciones, +que era el riesgo de muerte del §7. + +**Gotcha del barrido:** copiar la receta a otro directorio rompe la resolución de `deps.build` +(son relativas al dir de la receta) ⇒ se descartaban en silencio justo las recetas CON deps. La +copia va al lado del original. diff --git a/scripts/harkaq/fase2-barrido.sh b/scripts/harkaq/fase2-barrido.sh new file mode 100755 index 00000000..58ed6086 --- /dev/null +++ b/scripts/harkaq/fase2-barrido.sh @@ -0,0 +1,78 @@ +#!/bin/sh +# Fase 2 — barrer el catálogo y medir la tasa de excepciones (SDD 16 §6). +# +# fase2-barrido.sh [...] +# +# El criterio de viabilidad del SDD (§7): "si el 30% de los builds necesita excepción manual, +# harkaq se desactiva y queda como decoración". Métrica: <5%. +# +# Pero "excepción" no es "denegación" (§4.5). Una deuda que el store YA PROVEE no es una +# excepción: es una línea en `[deps]` que falta, y el arreglo es mecánico. Sólo la deuda +# IRREDUCIBLE —nadie la provee— cuenta contra el <5%. Sin esa distinción la métrica mide +# ansiedad, no viabilidad. +# +# `hammer build` pega en la caché si el artefacto ya está sellado (no correría nada y el +# veredicto saldría vacío sin que sea un fallo). Se fuerza un build real copiando la receta con +# otro `name` — cambia hash_inputs sin tocar el store real ni la receta del repo. +set -eu +cd "$(dirname "$0")/../.." + +BIN="${HARKAQ_BIN:-$HOME/.cache/harkaq}" +BASE="${HARKAQ_BASE:-$BIN/base.policy}" +OUT="${OUT:-work/harkaq-fase2}" +mkdir -p "$OUT" + +export HARKAQ=1 HARKAQ_BIN="$BIN" HARKAQ_BASE="$BASE" +export HARKAQ_TIMEOUT="${HARKAQ_TIMEOUT:-120}" + +total=0; limpias=0; solo_declarable=0; con_irreducible=0 +: > "$OUT/deuda-cruda.txt" + +for receta in "$@"; do + n=$(basename "$receta" .toml) + total=$((total + 1)) + # La copia va AL LADO del original: las `deps.build` se resuelven relativas al directorio + # de la receta, así que copiarla a otro sitio rompe `cmake.toml` y compañía — y descartaba + # en silencio justo las recetas CON deps, que son las interesantes. Se limpia al salir. + tmp="$(dirname "$receta")/.harkaq-$n.toml" + sed "s/^name *= *\"$n\"/name = \"$n-hk\"/" "$receta" > "$tmp" + trap 'rm -f "$tmp"' EXIT + + log="$OUT/$n.log" + timeout "${POR_RECETA:-300}" ./target/debug/hammer build "$tmp" --store "$PWD/store" \ + > "$log" 2>&1 || true + + # Un veredicto por fase (configure/compile/install son bwraps distintos ⇒ dominios distintos). + rm -f "$tmp" + grep '^\[harkaq\] {' "$log" | sed 's/^\[harkaq\] //' > "$OUT/$n.verdicts" || true + if [ ! -s "$OUT/$n.verdicts" ]; then + echo "?? $n — sin veredicto (¿caché? ¿fetch falló? ver $log)" + continue + fi + + irr=0; dec=0 + while IFS= read -r v; do + echo "$v" > "$OUT/.v.json" + s=$(scripts/harkaq/harkaq-suggest.py "$OUT/.v.json" "$BASE" "$PWD/store" 2>/dev/null || true) + echo "$s" | grep -q 'IRREDUCIBLE' && irr=1 + echo "$s" | grep -q 'DECLARABLE' && dec=1 + echo "$s" | sed -n 's/^ \(\/[^ ]*\)$/\1/p' >> "$OUT/deuda-cruda.txt" + done < "$OUT/$n.verdicts" + + if [ "$irr" = 1 ]; then + con_irreducible=$((con_irreducible + 1)); echo "!! $n — deuda IRREDUCIBLE" + elif [ "$dec" = 1 ]; then + solo_declarable=$((solo_declarable + 1)); echo "~~ $n — sólo deuda declarable (falta [deps])" + else + limpias=$((limpias + 1)); echo "ok $n — Hermetico" + fi +done + +echo +echo "════ FASE 2 — $total receta(s)" +echo " Hermetico ................... $limpias" +echo " sólo deuda DECLARABLE ....... $solo_declarable (arreglo mecánico: una línea en [deps])" +echo " con deuda IRREDUCIBLE ....... $con_irreducible ← lo que cuenta contra el <5%" +echo +echo " paths de deuda más frecuentes:" +sort "$OUT/deuda-cruda.txt" | uniq -c | sort -rn | head -12 | sed 's/^/ /' diff --git a/scripts/harkaq/harkaq-suggest.py b/scripts/harkaq/harkaq-suggest.py new file mode 100755 index 00000000..f95d8459 --- /dev/null +++ b/scripts/harkaq/harkaq-suggest.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""harkaq-suggest — convierte la deuda en una acción concreta (SDD 16 §4.5). + + harkaq-suggest.py + +Un veredicto `Impuro` dice *qué* usó el build sin declarar. Esto dice *qué hacer*: por cada path +de deuda busca en el store un artefacto sellado que provea ese mismo path relativo. Si lo hay, la +deuda es **declarable** — la receta sólo tiene que añadir la dep, y hammer ya sabe construirla. + +La distinción importa para la métrica de la Fase 2 (<5% de recetas necesitan excepción): + + declarable el store ya lo provee ⇒ NO es una excepción, es una línea en `[deps]`. El + diagnóstico es accionable y el arreglo, mecánico. + irreducible nadie lo provee ⇒ o se escribe una receta, o se acepta como runtime base. Ésta + es la que cuenta contra el <5%. + +Ejemplo real: zlib salió `Impuro` por `/usr/bin/make`. Parecía runtime base — pero +`recipes/make.toml` existe y `store/…-make` está sellado, y `SWAP_MAKE` es uno de los swaps del +selfhost-verify. O sea: no es base, es deuda declarable. La diferencia entre "aceptamos que +hammer usa el make de Alpine" y "a zlib le falta una línea" la decide este cruce, no una opinión. +""" +import json +import os +import sys + + +def indice_del_store(store): + """path-dentro-del-sandbox → [artefactos que lo proveen]. + + `--overlay-src ` funde `/usr/...` en `/usr/...`, así que el path relativo dentro + del artefacto ES el path del sandbox. El mismo mapeo que usa harkaq-policy, al revés. + """ + idx = {} + for art in sorted(os.listdir(store)): + raiz = os.path.join(store, art) + if not os.path.isdir(raiz): + continue + for dirpath, _, files in os.walk(raiz): + if "/.hammer" in dirpath: + continue + for f in files: + completo = os.path.join(dirpath, f) + dentro = "/" + os.path.relpath(completo, raiz) + idx.setdefault(dentro, []).append(art) + return idx + + +def nombre_de(art): + """`<64hex>-` → ``.""" + return art.split("-", 1)[1] if "-" in art else art + + +def main(): + if len(sys.argv) < 4: + print(__doc__.split("\n")[2].strip(), file=sys.stderr) + return 2 + crudo = json.load(open(sys.argv[1])) + politica, store = sys.argv[2], sys.argv[3] + + esperadas = set() + with open(politica) as f: + for l in f: + l = l.strip() + if l.startswith("# expect "): + esperadas.add(l[len("# expect "):].strip()) + + deuda = sorted({d["path"] for d in crudo.get("denials", []) if d["path"] not in esperadas}) + if not deuda: + print("sin deuda ✓") + return 0 + + idx = indice_del_store(store) + declarable, irreducible = {}, [] + for p in deuda: + provs = idx.get(p) + if provs: + declarable[p] = sorted({nombre_de(a) for a in provs}) + else: + irreducible.append(p) + + if declarable: + print("DEUDA DECLARABLE — el store ya lo provee; falta la línea en [deps]:") + for p, quien in declarable.items(): + print(f" {p}\n → declarar dep: {', '.join(quien)}") + if irreducible: + print("DEUDA IRREDUCIBLE — nadie la provee (escribir receta, o aceptar como runtime base):") + for p in irreducible: + print(f" {p}") + # El exit code separa "arreglable con una línea" de "hace falta trabajo de verdad": es lo que + # un barrido de la Fase 2 necesita para contar contra el <5% sin leer texto. + return 0 if not irreducible else 1 + + +if __name__ == "__main__": + sys.exit(main())