harkaq: campaña desatendida — medir→declarar→verificar, sin regex y sin preguntas
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>
This commit is contained in:
Executable
+80
@@ -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())
|
||||
Executable
+72
@@ -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/<n>.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
|
||||
Executable
+97
@@ -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@<ip> >>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@<ip>}"
|
||||
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 <noreply@anthropic.com>" && 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
|
||||
Reference in New Issue
Block a user