tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1574Shell 204Rust 111Markdown 993763 Definiciones

harkaq: primer barrido en la granja — la deuda irreducible de la muestra es perl (§4.10)

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>

13233ee4e9334e0aa3785f0dc35e97ef50148476 · por sergio · (hace 3 meses) · padre ea28a3dbc · ver el árbol en este commit

Delta semántico · 3

crates/hammer-build/src/sandbox.rs

scripts/harkaq/harkaq-suggest.py

4 archivos · +153 −2

diff --git a/crates/hammer-build/src/sandbox.rs b/crates/hammer-build/src/sandbox.rs
index f9d69187..a33e1f9b 100644
--- a/crates/hammer-build/src/sandbox.rs
+++ b/crates/hammer-build/src/sandbox.rs
@@ -257,7 +257,21 @@ impl Sandbox {
             // del rootfs Alpine y no ve al host ⇒ contrato, no deuda. Lo destapó brotli, cuyo
             // configure lee /proc/cpuinfo y /proc/meminfo para dimensionar el paralelismo.
             "ro /proc",
-            "list /", "list /opt",
+            // LISTAR la estructura del árbol no es deuda: Landlock separa READ_DIR de READ_FILE,
+            // así que esto deja hacer `ls /usr/lib` sin poder LEER un solo fichero no declarado.
+            // La deuda de de-Alpinización es leer/ejecutar lo ajeno, no saber que existe.
+            //
+            // Sin esto, una receta SIN deps no genera ni un `list` (los `list` salían sólo de los
+            // ancestros de la clausura) ⇒ todo escaneo del árbol caía como denegación. El barrido
+            // en la granja marcaba a bash como deuda IRREDUCIBLE por `/usr/lib`, que es un gap de
+            // política, no una dependencia de Alpine — un falso positivo de los que matan el
+            // proyecto (§7).
+            "list /", "list /opt", "list /usr", "list /usr/bin", "list /usr/lib",
+            "list /usr/include", "list /usr/share", "list /usr/libexec", "list /usr/local",
+            "list /lib", "list /bin", "list /sbin", "list /usr/sbin", "list /etc", "list /var",
+            // `--tmp-overlay /` hace la raíz escribible con una capa tmpfs descartable: escribir
+            // en /var/tmp no toca el rootfs Alpine ni sobrevive al build. Es scratch, como /tmp.
+            "rw /var/tmp",
         ]
         .iter()
         .map(|s| s.to_string())
diff --git a/docs/16-harkaq-jaula.md b/docs/16-harkaq-jaula.md
index f981f9fd..01af248c 100644
--- a/docs/16-harkaq-jaula.md
+++ b/docs/16-harkaq-jaula.md
@@ -808,6 +808,39 @@ el silencio de un lector roto.
 
 ---
 
+### 4.10 Barrido en la granja: la deuda irreducible de la muestra es **perl**
+
+Primer barrido sobre la granja con ABI 7 (`harkaq-farm-setup.sh` + `fase2-barrido.sh`, 24 recetas,
+14 con veredicto — las 10 restantes son Go/Rust y cayeron en caché o fetch). El primer número
+crudo fue **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`, así que **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**, porque conoce la lista de ficheros de cada artefacto — y el worker sólo
+   tiene 103 artefactos contra los cientos del hub.
+
+⇒ **El worker MIDE, el hub CLASIFICA.** Es la misma separación que lector/clasificador (§4.2): el
+que tiene el privilegio —o los datos— hace lo mínimo. Reclasificado en el hub:
+
+| receta | crudo en el worker | real |
+|---|---|---|
+| bash | `ranlib`, `/usr/lib`, `/var/tmp` | — (gap de política) |
+| doas | `/usr/bin/diff` | — (`diffutils` lo provee) |
+| ca-certificates, 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 en el store: es 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".
+
 ### 4.9 Matar gcc: la última milla, cerrada
 
 El primer barrido (§4.6) dejó **un solo** caso irreducible: brotli pidiendo
diff --git a/scripts/harkaq/harkaq-farm-setup.sh b/scripts/harkaq/harkaq-farm-setup.sh
new file mode 100755
index 00000000..993eedce
--- /dev/null
+++ b/scripts/harkaq/harkaq-farm-setup.sh
@@ -0,0 +1,68 @@
+#!/bin/sh
+# harkaq-farm-setup.sh <ip> — deja un worker de la granja listo para barrer con harkaq.
+#
+# La granja es el compilador continuo (SDD 16 §4.8), así que esto tiene que ser reproducible y
+# no un copy-paste. Idempotente: re-correrlo sobre un worker ya provisto no rompe nada.
+#
+# Precondición dura: la golden 408909310 (kernel 6.17 ⇒ Landlock ABI 7). Con la vieja (6.8, ABI 4)
+# el barrido correría y daría `SinEvidencia` en TODO — que es lo que D7 manda y lo que no sirve
+# para medir. Por eso lo primero que hace es verificar el ABI y abortar si no llega.
+set -eu
+
+IP="${1:?uso: harkaq-farm-setup.sh <ip>}"
+KEY="${SSH_KEY:-$HOME/.ssh/github5}"
+REMOTE="${REMOTE:-/opt/hammer}"
+SSH="ssh -i $KEY -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes"
+HERE="$(cd "$(dirname "$0")" && pwd)"
+
+echo "== 1/4 verificando el ABI (gate: >=7 o no hay evidencia)"
+abi=$($SSH "root@$IP" 'python3 -c "
+import ctypes; libc=ctypes.CDLL(\"libc.so.6\",use_errno=True)
+print(libc.syscall(444,None,0,1))"' 2>/dev/null | tr -d '\r')
+echo "   kernel: $($SSH "root@$IP" uname -r 2>/dev/null)  →  Landlock ABI = $abi"
+if [ "${abi:-0}" -lt 7 ]; then
+    echo "   ABORTO: ABI $abi < 7 ⇒ el kernel no audita denegaciones. Todos los veredictos"
+    echo "           saldrían SinEvidencia. ¿El worker salió de la golden 408909310?"
+    exit 4
+fi
+
+echo "== 2/4 copiando y compilando las piezas"
+$SSH "root@$IP" "mkdir -p /root/.cache/harkaq $REMOTE/scripts/harkaq"
+scp -q -i "$KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
+    "$HERE"/harkaq-uapi.h "$HERE"/harkaq-audit.c "$HERE"/harkaq-exec.c \
+    "$HERE"/harkaq-policy.sh "$HERE"/harkaq-verdict.py "$HERE"/harkaq-suggest.py \
+    "$HERE"/harkaq-base-closure.py "$HERE"/fase2-barrido.sh \
+    "root@$IP:$REMOTE/scripts/harkaq/"
+# harkaq-uapi.h no es una comodidad: el worker tiene headers 6.8 sobre kernel 6.17 y NO define
+# ni AUDIT_LANDLOCK_ACCESS. Sin él el lector filtraría por un número que no conoce y vería cero
+# denegaciones — el falso `Hermetico` de D9, por headers viejos (§4.8).
+$SSH "root@$IP" "cd $REMOTE/scripts/harkaq && chmod +x *.sh *.py &&
+    gcc -O1 -Wall -o /root/.cache/harkaq/harkaq-audit harkaq-audit.c &&
+    gcc -O1 -Wall -static -o /root/.cache/harkaq/harkaq-exec harkaq-exec.c &&
+    echo '   compiladas ✓'"
+
+echo "== 3/4 derivando el runtime base DEL ROOTFS DEL WORKER"
+# El runtime base es POR ROOTFS (§4.3) — derivarlo del rootfs del worker, no copiar el del
+# laptop: los sonames de las libs (libz.so.1.3.2, …) dependen de la versión de Alpine que tenga.
+$SSH "root@$IP" "cd $REMOTE &&
+    python3 scripts/harkaq/harkaq-base-closure.py .dev-fs/alpine \
+        /bin/sh /bin/busybox /bin/bash /bin/coreutils /usr/bin/env 2>/dev/null \
+        > /root/.cache/harkaq/base.policy
+    printf 'ro /usr/lib/os-release\n' >> /root/.cache/harkaq/base.policy
+    # Sondas de compilador de Alpine: se DENIEGAN y se clasifican (D3). Concederlas dejaría a
+    # zig/configure usar el gcc de Alpine por detrás — justo lo que la campaña matar-gcc caza.
+    for p in /usr/bin/gcc /usr/bin/ldd /usr/bin/c89 /usr/bin/c99; do
+        printf '# expect %s\n' \"\$p\" >> /root/.cache/harkaq/base.policy
+    done
+    echo \"   runtime base: \$(grep -c '^ro ' /root/.cache/harkaq/base.policy) reglas + \$(grep -c '^# expect' /root/.cache/harkaq/base.policy) esperadas\""
+
+echo "== 4/4 comprobando la cadena de evidencia de punta a punta"
+$SSH "root@$IP" "cd $REMOTE && cargo build -q -p hammer-cli 2>/dev/null || true; ls target/debug/hammer >/dev/null 2>&1 && echo '   hammer-cli listo ✓' || echo '   OJO: falta target/debug/hammer (cargo build -p hammer-cli)'"
+
+cat <<EOF
+
+listo. En el worker:
+  cd $REMOTE
+  export HARKAQ_BIN=/root/.cache/harkaq HARKAQ_BASE=/root/.cache/harkaq/base.policy
+  scripts/harkaq/fase2-barrido.sh recipes/zlib.toml recipes/bzip2.toml ...
+EOF
diff --git a/scripts/harkaq/harkaq-suggest.py b/scripts/harkaq/harkaq-suggest.py
index f95d8459..b9580449 100755
--- a/scripts/harkaq/harkaq-suggest.py
+++ b/scripts/harkaq/harkaq-suggest.py
@@ -1,7 +1,7 @@
 #!/usr/bin/env python3
 """harkaq-suggest — convierte la deuda en una acción concreta (SDD 16 §4.5).
 
-    harkaq-suggest.py <verdict-crudo.json> <politica> <store>
+    harkaq-suggest.py <verdict-crudo.json> <politica> <store> [recipes-dir]
 
 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
@@ -50,12 +50,31 @@ def nombre_de(art):
     return art.split("-", 1)[1] if "-" in art else art
 
 
+def catalogo(recipes_dir):
+    """Nombres de receta que hammer SABE construir, tenga o no el artefacto a mano.
+
+    Sin esto, "declarable" sería relativo a la máquina: el store del worker de la granja trae 103
+    artefactos y el del laptop cientos, así que el MISMO path (`/usr/bin/make`) salía declarable
+    en uno e irreducible en el otro. Eso no es una propiedad de la receta: es del disco donde
+    corrés el barrido, y habría inflado la métrica de la Fase 2 en la granja — que es justo donde
+    se va a medir en serio.
+
+    El catálogo es la respuesta correcta a "¿esto lo sabemos construir?". El store sólo dice
+    "¿lo tenemos construido AHORA?", que es otra pregunta.
+    """
+    if not recipes_dir or not os.path.isdir(recipes_dir):
+        return set()
+    return {f[:-5] for f in os.listdir(recipes_dir) if f.endswith(".toml")}
+
+
 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]
+    recipes_dir = sys.argv[4] if len(sys.argv) > 4 else os.path.join(
+        os.path.dirname(os.path.abspath(store)), "recipes")
 
     esperadas = set()
     with open(politica) as f:
@@ -70,11 +89,28 @@ def main():
         return 0
 
     idx = indice_del_store(store)
+    recetas = catalogo(recipes_dir)
     declarable, irreducible = {}, []
     for p in deuda:
         provs = idx.get(p)
         if provs:
+            # El store es autoritativo: sabe qué ficheros aporta cada artefacto.
             declarable[p] = sorted({nombre_de(a) for a in provs})
+        elif os.path.basename(p) in recetas:
+            # Respaldo por catálogo: el artefacto no está construido AQUÍ, pero hammer sabe
+            # construirlo. Es declarable igual — la deuda es de la receta, no del disco.
+            #
+            # OJO, heurística por NOMBRE y por eso sólo un respaldo: acierta con `make`, pero
+            # falla con `/usr/bin/ranlib` (lo trae `binutils`) y `/usr/bin/diff` (lo trae
+            # `diffutils`). Un fichero no se llama como su paquete. El único que sabe la verdad
+            # es el store, porque conoce la lista de ficheros de cada artefacto.
+            #
+            # ⇒ CLASIFICAR EN EL HUB, no en el worker. El worker de la granja mide (produce los
+            # veredictos crudos) y el hub clasifica, porque es quien tiene el store COMPLETO. Con
+            # el store parcial del worker (103 artefactos) esto marcaba bash y doas como deuda
+            # IRREDUCIBLE y habría dado una métrica de Fase 2 inflada. Misma separación que
+            # lector/clasificador: el que tiene el privilegio (o los datos) hace lo mínimo.
+            declarable[p] = [os.path.basename(p) + " (receta en el catálogo, sin construir aquí)"]
         else:
             irreducible.append(p)