From f783f0107dce3245c4168e3b3d21621df3cb5c83 Mon Sep 17 00:00:00 2001 From: sergio Date: Wed, 15 Jul 2026 22:53:57 -0400 Subject: [PATCH] =?UTF-8?q?harkaq:=20campa=C3=B1a=20desatendida=20?= =?UTF-8?q?=E2=80=94=20medir=E2=86=92declarar=E2=86=92verificar,=20sin=20r?= =?UTF-8?q?egex=20y=20sin=20preguntas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- scripts/farm/harkaq-add-deps.py | 80 +++++++++++++++++++++++++++ scripts/farm/harkaq-campana.sh | 72 ++++++++++++++++++++++++ scripts/farm/harvest-harkaq.sh | 97 +++++++++++++++++++++++++++++++++ 3 files changed, 249 insertions(+) create mode 100755 scripts/farm/harkaq-add-deps.py create mode 100755 scripts/farm/harkaq-campana.sh create mode 100755 scripts/farm/harvest-harkaq.sh diff --git a/scripts/farm/harkaq-add-deps.py b/scripts/farm/harkaq-add-deps.py new file mode 100755 index 00000000..96ce52e2 --- /dev/null +++ b/scripts/farm/harkaq-add-deps.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""harkaq-add-deps — añade deps de build a una receta, sin romperla. (Cosecha desatendida.) + + harkaq-add-deps.py recipes/zlib.toml make binutils + +Idempotente: si la dep ya está, no toca nada y sale 1 (nada que commitear). + +Corre DESATENDIDO y de noche, editando ficheros que son la fuente de verdad del catálogo. Por eso +la regla es: **ante la duda, no tocar**. Un fichero mal editado a las 3am no da un error, da una +receta corrupta que nadie mira hasta el lunes. Cada caso que no se entiende sale sin cambios y con +motivo, que es la misma disciplina de `SinEvidencia`: no afirmar lo que no se sabe. +""" +import re +import sys + + +def anadir(path, nuevas): + with open(path) as f: + s = f.read() + orig = s + + m = re.search(r'^\[deps\]\s*$(.*?)(?=^\[|\Z)', s, re.M | re.S) + if m: + bloque = m.group(1) + mb = re.search(r'^(\s*build\s*=\s*)\[([^\]]*)\]', bloque, re.M) + if mb: + ya = re.findall(r'"([^"]+)"', mb.group(2)) + falta = [d for d in nuevas if d not in ya] + if not falta: + return False, "ya estaban declaradas" + lista = ya + falta + nuevo = mb.group(1) + "[" + ", ".join(f'"{d}"' for d in lista) + "]" + s = s[:m.start(1)] + bloque[:mb.start()] + nuevo + bloque[mb.end():] + s[m.end(1):] + else: + # Hay [deps] pero sin `build = [...]` (p.ej. sólo `runtime`). Insertar la línea. + linea = "build = [" + ", ".join(f'"{d}"' for d in nuevas) + "]\n" + s = s[:m.start(1)] + "\n" + linea + bloque.lstrip("\n") + s[m.end(1):] + else: + # Sin [deps]: se añade al final. `[build.phases]` suele ser la última sección y sus strings + # multilínea (''' … ''') se comerían un bloque puesto después ⇒ va ANTES de la primera + # sección [build...] si existe, y si no, al final. + bloque = "\n[deps]\nbuild = [" + ", ".join(f'"{d}"' for d in nuevas) + "]\n" + mfase = re.search(r'^\[build\.phases\]', s, re.M) + if mfase: + s = s[:mfase.start()] + bloque.lstrip("\n") + "\n" + s[mfase.start():] + else: + s = s.rstrip("\n") + "\n" + bloque + + if s == orig: + return False, "sin cambios" + + # Guarda: sólo escribir si el TOML sigue pareciendo un TOML y no perdimos nada. Un parser de + # verdad sería mejor, pero tomllib no ESCRIBE y re-emitir con un writer reordenaría el fichero + # entero (y con él, comentarios que valen oro en este repo). + try: + import tomllib + tomllib.loads(s) + except Exception as e: + return False, f"el resultado no parsea como TOML ({e}) — NO se toca" + if len(s) < len(orig): + return False, "el resultado es más corto que el original — NO se toca" + + with open(path, "w") as f: + f.write(s) + return True, "ok" + + +def main(): + if len(sys.argv) < 3: + print(__doc__.split("\n")[2].strip(), file=sys.stderr) + return 2 + path, nuevas = sys.argv[1], sys.argv[2:] + ok, motivo = anadir(path, nuevas) + if not ok: + print(f" ·· {path}: {motivo}", file=sys.stderr) + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/farm/harkaq-campana.sh b/scripts/farm/harkaq-campana.sh new file mode 100755 index 00000000..07d3d8b3 --- /dev/null +++ b/scripts/farm/harkaq-campana.sh @@ -0,0 +1,72 @@ +#!/bin/sh +# harkaq-campana.sh — bucle AUTÓNOMO del worker para la campaña harkaq. Hermano de +# farm-worker-loop.sh: muele sin esperar a nadie, 24/7, y no pregunta NADA. +# +# scripts/farm/harkaq-campana.sh [lista-de-recetas.txt] +# +# Mide, no decide: por cada receta la construye bajo la jaula y guarda los veredictos crudos en +# work/harkaq-campana/.verdicts. La clasificación y la edición de recetas las hace el HUB +# (harvest-harkaq.sh, por cron), porque el store del worker es parcial y marcaría como irreducible +# lo que sí es declarable. El worker MIDE, el hub CLASIFICA. +# +# Idempotente: lo ya medido se saltea; re-correrlo sólo mide lo nuevo. Cuando no queda nada, +# duerme y re-escanea — igual que farm-worker-loop.sh. +# +# Diseñado para la NOCHE: nunca bloquea esperando una decisión. Un build que falla NO detiene la +# cola; su veredicto se guarda igual (un build que falla es justo cuando más interesa saber qué +# tocó fuera de su clausura) y se sigue. Ocho horas de idle por una pregunta es el fallo que este +# script existe para no tener. +set -u + +HAMMER_DIR="${HAMMER_DIR:-/opt/hammer}" +cd "$HAMMER_DIR" +LISTA="${1:-work/harkaq-cola.txt}" +OUT="work/harkaq-campana" +BIN="${HARKAQ_BIN:-/root/.cache/harkaq}" +IDLE_SLEEP="${IDLE_SLEEP:-300}" +POR_RECETA="${POR_RECETA:-900}" +mkdir -p "$OUT" + +export HARKAQ=1 HARKAQ_BIN="$BIN" HARKAQ_BASE="${HARKAQ_BASE:-$BIN/base.policy}" +export HARKAQ_TIMEOUT="${HARKAQ_TIMEOUT:-300}" + +# Gate: sin ABI>=7 el kernel no audita y TODOS los veredictos saldrían SinEvidencia. Moler la +# noche entera para producir "no sabemos" es peor que no moler. +abi=$(python3 -c 'import ctypes;print(ctypes.CDLL("libc.so.6").syscall(444,None,0,1))' 2>/dev/null) +if [ "${abi:-0}" -lt 7 ]; then + echo "ABORTO: Landlock ABI ${abi:-?} < 7 ⇒ sin audit de denegaciones. ¿La golden es la 6.17?" + exit 4 +fi +echo "$(date -u +%FT%TZ) campaña harkaq: ABI $abi, cola $LISTA" + +while :; do + hechas=0; nuevas=0 + while IFS= read -r n; do + [ -n "$n" ] || continue + [ -f "recipes/$n.toml" ] || continue + if [ -s "$OUT/$n.verdicts" ]; then hechas=$((hechas + 1)); continue; fi + + # Forzar build real: `hammer build` pega en la caché si el artefacto ya está sellado y no + # correría nada (veredicto vacío que NO es un fallo). La copia va AL LADO del original o se + # rompe la resolución de deps.build, que son relativas al dir de la receta. + tmp="recipes/.hk-$n.toml" + sed "s/^name *= *\"$n\"/name = \"$n-hkm\"/" "recipes/$n.toml" > "$tmp" + timeout "$POR_RECETA" ./target/release/hammer build "$tmp" --store "$PWD/store" \ + > "$OUT/$n.log" 2>&1 + rm -f "$tmp" + grep '^\[harkaq\] {' "$OUT/$n.log" | sed 's/^\[harkaq\] //' > "$OUT/$n.verdicts" + if [ -s "$OUT/$n.verdicts" ]; then + nuevas=$((nuevas + 1)) + echo "$(date -u +%FT%TZ) ++ $n ($(wc -l < "$OUT/$n.verdicts") fase/s medidas)" + else + # Sin veredicto: el build ni arrancó (fetch, dep rota…). Se marca para no reintentar + # en bucle y se SIGUE — la cola no se detiene por una receta. + echo "sin-veredicto" > "$OUT/$n.skip" + rm -f "$OUT/$n.verdicts" + echo "$(date -u +%FT%TZ) ?? $n — sin veredicto, salteada (ver $OUT/$n.log)" + fi + done < "$LISTA" + + echo "$(date -u +%FT%TZ) ciclo: $nuevas nuevas, $hechas ya medidas. durmiendo ${IDLE_SLEEP}s" + sleep "$IDLE_SLEEP" +done diff --git a/scripts/farm/harvest-harkaq.sh b/scripts/farm/harvest-harkaq.sh new file mode 100755 index 00000000..1e25b7ff --- /dev/null +++ b/scripts/farm/harvest-harkaq.sh @@ -0,0 +1,97 @@ +#!/bin/sh +# harvest-harkaq.sh — COSECHA-ONLY de la campaña harkaq (modo desatendido, para cron en el hub). +# Determinista, sin IA. Hermano de harvest-go.sh y con la misma disciplina: no elige, no diagnostica, +# no compila en el hub; sólo convierte en commit lo que el worker YA midió. +# +# Uso (cron): cd /home/sergio/hammer && scripts/farm/harvest-harkaq.sh root@ >>work/harvest-harkaq.log 2>&1 +# +# POR QUÉ EXISTE ASÍ: la lista de "recetas a las que les falta declarar make" NO se puede sacar de +# un regex. Se intentó y falló dos veces en la misma tarde: `\bmake\b` colaba `cargo-make` (el guión +# es límite de palabra) y 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. La fuente de verdad es la MEDICIÓN, no la conjetura — que es exactamente para lo +# que existe harkaq. Este script sólo aplica lo que el kernel denunció. +# +# Ciclo (idempotente, seguro de re-correr): +# 1. git pull --rebase (otro agente pushea al mismo main) +# 2. baja los veredictos del worker +# 3. por cada veredicto: harkaq-suggest contra el store COMPLETO del hub (el del worker es parcial +# y marcaría como irreducible lo que sí es declarable — el worker MIDE, el hub CLASIFICA) +# 4. si la deuda es DECLARABLE ⇒ añade la dep a la receta. Si es IRREDUCIBLE ⇒ NO la toca: va a +# tandas/needs-review-harkaq/ con el motivo. Declarar algo que el store no provee rompería el +# build en vez de arreglarlo. +# 5. commit + push con `git add` EXPLÍCITO (nunca -A: respeta al otro agente) +set -u + +HUB="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$HUB" +WORKER="${1:?uso: harvest-harkaq.sh root@}" +SSH_KEY="${SSH_KEY:-$HOME/.ssh/github5}" +REMOTE="${REMOTE:-/opt/hammer}" +SSH="ssh -i $SSH_KEY -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes" +REVIEW="tandas/needs-review-harkaq" +mkdir -p "$REVIEW" work + +echo "═══ $(date -u +%FT%TZ) cosecha harkaq desde $WORKER" +git pull --rebase -q 2>/dev/null || echo " (pull falló; sigo con lo local)" + +VERD=work/harkaq-verdicts +rm -rf "$VERD"; mkdir -p "$VERD" +rsync -az -e "$SSH" "$WORKER:$REMOTE/work/harkaq-campana/" "$VERD/" 2>/dev/null || { + echo " sin veredictos que cosechar (¿el worker sigue midiendo?)"; exit 0; } + +tocadas="" +for v in "$VERD"/*.verdicts; do + [ -f "$v" ] || continue + n=$(basename "$v" .verdicts) + [ -f "recipes/$n.toml" ] || continue + + deps=""; irr=0 + while IFS= read -r j; do + [ -n "$j" ] || continue + echo "$j" > /tmp/.hkv.json + s=$(scripts/harkaq/harkaq-suggest.py /tmp/.hkv.json "$HOME/.cache/harkaq/base.policy" \ + "$HUB/store" "$HUB/recipes" 2>/dev/null) || true + echo "$s" | grep -q IRREDUCIBLE && irr=1 + # "→ declarar dep: X" (el store lo provee y sabe cuál artefacto es). + d=$(echo "$s" | sed -n 's/.*→ declarar dep: \([a-zA-Z0-9._-]*\).*/\1/p') + deps="$deps $d" + done < "$v" + + deps=$(echo "$deps" | tr ' ' '\n' | grep -v '^$' | sort -u | tr '\n' ' ') + [ -n "$deps" ] || continue + + if [ "$irr" = 1 ]; then + # NO se toca: declarar algo que el store no provee rompe el build en vez de arreglarlo. + cp "$v" "$REVIEW/$n.verdicts" + echo " ?? $n — deuda irreducible ⇒ a needs-review (deps declarables vistas:$deps)" + continue + fi + + if python3 scripts/farm/harkaq-add-deps.py "recipes/$n.toml" $deps; then + echo " ++ $n — declaradas:$deps" + tocadas="$tocadas recipes/$n.toml" + fi +done + +if [ -n "$tocadas" ]; then + # `git add` EXPLÍCITO por rutas: el repo lo comparte otro agente (cola gráfica/Go). + # shellcheck disable=SC2086 + git add $tocadas + git commit -q -m "harkaq: declarar deps medidas por el kernel (cosecha automática) + +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:$(echo "$tocadas" | tr ' ' '\n' | grep -v '^$' | sed 's|recipes/||;s|\.toml||' | tr '\n' ' ') + +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 $REVIEW. + +Co-Authored-By: Claude Opus 4.8 " && git push -q 2>/dev/null || { + git pull --rebase -q && git push -q; } + echo " ✓ commit+push de $(echo "$tocadas" | wc -w) receta(s)" +else + echo " nada que declarar en este ciclo" +fi