diff --git a/scripts/vigia-subcomandos.py b/scripts/vigia-subcomandos.py new file mode 100755 index 00000000..6e52d868 --- /dev/null +++ b/scripts/vigia-subcomandos.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +# vigia-subcomandos.py — ¿hay herramientas SELLADAS que no sirven porque falta su DRIVER? +# +# ══ EL PUNTO CIEGO QUE ESTE VIGÍA CUBRE ════════════════════════════════════════════════════════ +# `build-state.py` contesta «¿está sellado?», `vigia-sonames.py` contesta «¿algún `.so` que alguien +# pide no lo provee nadie?» y `vigia-imagen.py` contesta «¿la imagen se puede USAR?». Ninguno +# contesta ésta: **¿este binario sellado sirve para algo por sí solo?** +# +# Hay una familia entera de herramientas que NO son programas: son SUBCOMANDOS. `protoc-gen-go` es un +# plugin que `protoc` ejecuta; `cargo-audit` existe para escribirse `cargo audit`; `kubectl-tree` lo +# despacha `kubectl`. Sellados, no vacíos, sin NEEDED colgante, reproducibles — y **inertes**, porque +# el driver que los invoca no está en el catálogo. +# +# ⚠ **Ningún indicador existente puede verlo.** El artefacto tiene contenido (regla 3 ✓), resuelve +# sus sonames (vigia-sonames ✓), el grafo lo cuenta como `sealed` (✓) y `verificar-repro` dice que +# REPRODUCE (✓). Cuatro verdes sobre algo que no se puede usar. La arista «me ejecuta aquél» no es +# una dep de build, así que no existe en el grafo: se descubre sólo cuando alguien va a usarlo. +# +# ══ CÓMO LO ENCONTRÉ (2026-09-11), que es la razón de que este fichero exista ═══════════════════ +# `recipes/protoc-gen-go.toml` llevaba meses sellado y el catálogo NO tenía `protoc`. Un plugin de +# protoc sin protoc no hace absolutamente nada. No lo delató ninguna métrica: lo delató ir a escribir +# la receta de un servicio gRPC y preguntarse con qué se generan los stubs. +# +# ══ QUÉ MIDE, Y POR QUÉ ASÍ ════════════════════════════════════════════════════════════════════ +# Para cada prefijo de la tabla DRIVERS, junta las recetas que empiezan con él y comprueba que el +# **BINARIO** del driver exista dentro de algún artefacto sellado. +# +# · Se comprueba el BINARIO y no el nombre de la receta a propósito: el driver de `protoc-gen-*` es +# `protoc`, que lo publica la receta `protobuf`. Preguntar «¿existe recipes/protoc.toml?» habría +# contestado «no» incluso después de cerrarlo, y un vigía que grita cuando ya está arreglado se +# empieza a ignorar — que es cómo se muere un guardián ([[guardianes-el-ruido-los-mata]]). +# · La tabla es EXPLÍCITA y no una heurística sobre guiones. `git-cliff` es subcomando de git, pero +# `gettext-tiny` no es subcomando de `gettext`. Adivinar por el guión da falsos positivos, y un +# vigía con falsos positivos no se lee. +# +# ══ AUTOPRUEBA ═════════════════════════════════════════════════════════════════════════════════ +# `--autoprueba` corre los dos controles que hacen falta para creerle a un guardián: uno NEGATIVO +# (esconder un driver que sí está ⇒ TIENE que saltar) y uno POSITIVO (sin tocar nada, los drivers +# presentes NO deben aparecer). Un guardián que nunca falló no se sabe si sirve. +# +# Uso: scripts/vigia-subcomandos.py [--autoprueba] +# Sale: 0 si no hay inertes · 1 si los hay · 2 si la autoprueba falla +import glob +import os +import re +import sys + +RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# prefijo de receta → (binario del driver, quién se supone que lo publica, para el mensaje) +DRIVERS = { + "cargo-": ("cargo", "el toolchain de Rust"), + "kubectl-": ("kubectl", "una receta kubectl"), + "git-": ("git", "recipes/git.toml"), + "helm-": ("helm", "recipes/helm.toml"), + "protoc-gen-": ("protoc", "recipes/protobuf.toml"), + "docker-": ("docker", "una receta docker"), +} + + +def artefactos(): + """Todos los directorios de artefacto del store, por nombre de receta. + + ⚠ El nombre se saca con una EXPRESIÓN ANCLADA, no con `glob('store/*-')`: ese glob hace + coincidencia por sufijo, así que `*-go` casa con `…-protoc-gen-go` y contesta que `go` está + sellado cuando no lo está. Me pasó midiendo esto mismo.""" + out = {} + for d in glob.glob(os.path.join(RAIZ, "store", "*")): + m = re.match(r"^[0-9a-f]{64}-(.+)$", os.path.basename(d)) + if m and os.path.isdir(d): + out.setdefault(m.group(1), []).append(d) + return out + + +def provee_binario(arts, nombre_bin, ocultar=()): + """¿Algún artefacto sellado trae un ejecutable llamado `nombre_bin`?""" + for receta, dirs in arts.items(): + if receta in ocultar: + continue + for d in dirs: + for sub in ("bin", "sbin", "usr/bin", "usr/sbin", "usr/local/bin"): + p = os.path.join(d, sub, nombre_bin) + if os.path.exists(p): + return receta + return None + + +def barrido(arts, ocultar=()): + """→ lista de (prefijo, binario, quien_deberia, [recetas inertes]).""" + recetas = set() + for patron in ("recipes/*.toml", "recipes/incoming*/*.toml"): + for f in glob.glob(os.path.join(RAIZ, patron)): + recetas.add(os.path.basename(f)[:-5]) + hallazgos = [] + for prefijo, (binario, quien) in sorted(DRIVERS.items()): + subs = sorted(r for r in recetas if r.startswith(prefijo) and r != prefijo.rstrip("-")) + if not subs: + continue + if provee_binario(arts, binario, ocultar) is None: + hallazgos.append((prefijo, binario, quien, subs)) + return hallazgos + + +def autoprueba(): + """Control NEGATIVO + control POSITIVO. Sin los dos, no se sabe si el vigía mide algo.""" + arts = artefactos() + ok = True + + # NEGATIVO: escondemos `git`, que SÍ está y tiene subcomandos ⇒ el vigía tiene que verlo. + quien_git = provee_binario(arts, "git") + if quien_git is None: + print(" ⚠ control negativo NO APLICABLE: `git` no está sellado en esta máquina") + else: + roto = [p for p, _b, _q, _s in barrido(arts, ocultar={quien_git})] + if "git-" in roto: + print(f" ✓ control NEGATIVO: escondiendo `{quien_git}`, el vigía canta `git-` ") + else: + print(f" ✗ control NEGATIVO FALLÓ: escondí `{quien_git}` y el vigía no dijo nada") + ok = False + + # POSITIVO: sin tocar nada, los drivers que SÍ están no deben aparecer. Un vigía que grita + # sobre todo es igual de inútil que uno que nunca grita. + presentes = [p for p, (b, _q) in DRIVERS.items() if provee_binario(arts, b)] + ruidosos = [p for p, _b, _q, _s in barrido(arts) if p in presentes] + if ruidosos: + print(f" ✗ control POSITIVO FALLÓ: canta sobre drivers presentes: {ruidosos}") + ok = False + else: + print(f" ✓ control POSITIVO: no canta sobre los {len(presentes)} drivers presentes " + f"({', '.join(p.rstrip('-') for p in presentes)})") + return ok + + +def main(): + if "--autoprueba" in sys.argv: + print("══ AUTOPRUEBA DEL VIGÍA ══") + sys.exit(0 if autoprueba() else 2) + + arts = artefactos() + hallazgos = barrido(arts) + print("══ SUBCOMANDOS SIN DRIVER ══") + if not hallazgos: + print(" ✓ 0 herramientas inertes: todos los drivers de la tabla están en el catálogo") + sys.exit(0) + total = 0 + for prefijo, binario, quien, subs in hallazgos: + total += len(subs) + print(f"\n ✗ falta el binario `{binario}` ({quien})") + print(f" ⇒ {len(subs)} recetas selladas quedan INERTES:") + for i in range(0, len(subs), 6): + print(" " + " ".join(subs[i:i + 6])) + print(f"\n TOTAL: {total} herramientas selladas que no se pueden invocar.") + print(" No es deuda de BUILD (construyen y reproducen): es deuda de CATÁLOGO.") + sys.exit(1) + + +if __name__ == "__main__": + main()