gioser

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 1534Shell 199Rust 109Markdown 993713 Definiciones

vigía --uso: de los 64 applets que sólo da busybox, 12 tienen llamador

Es lo que el plan dejó sin medir tres veces («no se midió qué applets usa el
producto de verdad; eso abarataría el paso 1»). El modo lee del TSV los applets
con proveedor_medido = SOLO-BUSYBOX y los busca en el repo con las reglas
anti-ruido que el vigía ya tenía.

12 invocados: sh (28 sitios, lo cierra simi), ifconfig/route/udhcpc/netstat
(todos con ip/ss como primera opción y el applet como respaldo), setuidgid (3
cards: gitea, pacha-secretos, tejido), getty (la card de la consola), adduser,
nc, nslookup, poweroff. Y uno que es falso positivo del grep y conviene tener
escrito: los `insmod` de iso-image.sh/install-image.sh son de GRUB —`insmod
part_gpt` dentro de un grub.cfg—, no del busybox.

Los otros 52 no los llama nadie EN EL REPO, y la salida lo dice sin dejar que se
lea como una lista de borrado: un grep ve a quien invoca por nombre, y hay dos
consumidores que no aparecen nunca — el KERNEL (llama a /sbin/modprobe por
call_usermodehelper y al gestor de /dev por hotplug, así que modprobe/lsmod/
depmod/rmmod/modinfo/mdev/uevent/makedevs salen «libres» y no lo son) y el
USUARIO de la máquina, que espera clear/reset/vi/traceroute al entrar por ssh.

La primera versión del modo decía 10 y estaba mal en dos casos, los dos del lado
peligroso — dan permiso para retirar algo que se usa:

  · setuidgid se invoca como `exec /usr/bin/setuidgid pacha`, con RUTA ABSOLUTA,
    y el patrón de comando pide el nombre pegado al exec.
  · getty vive como `"argv": ["getty", …]`, elemento de un array JSON, y la regla
    que filtra cadenas por «parece una línea de comandos» —la que bajó el ruido
    de 91 hits a 0— descarta un nombre suelto.

Las reglas que quitan ruido son las que crean ceguera; lo que decide es hacia
dónde se equivoca cada una. Ahora busca tres formas: nombre desnudo, ruta
absoluta y primer elemento de argv. El vigía sigue verde en su modo original
(123 applets retirados, nadie los invoca) y se salta su propio fichero, que
nombra applets en la documentación y se denunciaba a sí mismo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

8a109400b50787d8fccf9571e38d9b9b4b66f70f · por Sergio · (hace 1 mes) · padre 4357b29cb · ver el árbol en este commit

Delta semántico · 4

scripts/busybox-vigia.py

2 archivos · +171 −0

diff --git a/docs/plan-botar-busybox.md b/docs/plan-botar-busybox.md
index de2f8d3c..57234d36 100644
--- a/docs/plan-botar-busybox.md
+++ b/docs/plan-botar-busybox.md
@@ -1252,3 +1252,48 @@ ncurses— moría en ejecución con `Error relocating /lib/libncursesw.so.6`, to
 anfitrión. Un `file` sobre los binarios lo dice en un segundo; ninguna prueba del build lo iba a
 decir. Es [[cgo-sin-baseline-sigill]] otra vez: **redefinir una variable que el harness ya puebla
 pierde en silencio lo que el harness había puesto**, y el síntoma aparece lejos.
+
+## Quién usa lo que queda — `busybox-vigia.py --uso` (2026-09-21)
+
+Esto es **lo que el documento dejó sin medir tres veces**: «no se midió qué applets usa el producto
+de verdad; eso abarataría el paso 1». Ahora se mide. El modo nuevo lee del TSV los applets con
+`proveedor_medido = SOLO-BUSYBOX` y los busca en el repo con las reglas anti-ruido del vigía.
+
+De los **64**, sólo **12 tienen un llamador dentro del producto**:
+
+| applet | quién lo llama |
+|---|---|
+| `sh` | 28 sitios — el `/bin/sh` de todo; lo cierra `simi` |
+| `ifconfig` `route` `udhcpc` `netstat` | `metal-iso.sh`, `cosmic/metal-desktop-image.sh`, `test-foco-egress.sh`, `mudanza/censar.py` — **todos con `ip`/`ss` como primera opción y el applet como respaldo** (`ip link set … \|\| ifconfig …`) |
+| `insmod` | `iso-image.sh`, `install-image.sh` — ⚠ **es el `insmod` de GRUB**, no el de busybox: `insmod part_gpt` dentro de un `grub.cfg`. Falso positivo del grep, no migración |
+| `setuidgid` | `gitea`, `pacha-secretos`, `tejido` — tres cards |
+| `getty` | la card de la consola (`ssh-e2e-test.sh`, `takana-bootstrap`) |
+| `adduser` | `servidor/alta-usuario.sh` |
+| `nc` | `test-foco-egress.sh` |
+| `nslookup` | `drive-netup.py` |
+| `poweroff` | `farm/harkaq-campana-vol.sh` |
+
+**Los otros 52 no los invoca nadie en el repo.** Y ahí está la trampa, escrita en la salida del
+propio comando para que no se lea como una lista de borrado:
+
+> **«Candidato» no es «retirable».** Un grep ve a quien invoca POR NOMBRE. Hay dos clases de
+> consumidor que no aparecen nunca: **el kernel** —llama a `/sbin/modprobe` por
+> `call_usermodehelper` y al gestor de `/dev` por hotplug, así que `modprobe lsmod depmod rmmod
+> modinfo mdev uevent makedevs` salen «libres» y no lo son— y **el usuario de la máquina**, que
+> espera encontrar `clear`, `reset`, `vi` o `traceroute` al entrar por ssh.
+
+### Y los dos falsos negativos que tuvo el modo, que son la parte que enseña
+
+La primera versión decía **10 invocados** y estaba mal en dos casos, los dos peligrosos porque
+llevan a retirar un applet que el producto usa:
+
+1. **`setuidgid`** — se invoca como `exec /usr/bin/setuidgid pacha`, con **ruta absoluta**. El
+   patrón de comando pide el nombre pegado al `exec`, y ahí hay un `/usr/bin/` en medio.
+2. **`getty`** — vive como `"argv": ["getty", "-n", …]`, **elemento de un array JSON**. La regla que
+   filtra cadenas de Rust/TOML por «parece una línea de comandos» —la que en su día bajó el ruido de
+   91 hits a 0— descarta una cadena que es sólo un nombre.
+
+Las dos reglas que quitan ruido son las mismas que crean ceguera, y la diferencia es hacia dónde se
+equivoca cada una. En el modo original —«¿alguien llama a algo que ya retiré?»— un falso positivo
+cuesta un minuto y un falso negativo cuesta un boot roto. En este modo pasa igual, así que ahora
+busca **tres formas**: nombre desnudo, ruta absoluta y primer elemento de `argv`.
diff --git a/scripts/busybox-vigia.py b/scripts/busybox-vigia.py
index f978322e..4291dff5 100755
--- a/scripts/busybox-vigia.py
+++ b/scripts/busybox-vigia.py
@@ -7,6 +7,17 @@ el fallo aparece en boot, en otra máquina, semanas después.
 
     scripts/busybox-vigia.py            # exit 1 si hay invocaciones de applets retirados
     scripts/busybox-vigia.py --list     # imprime los applets retirados y sale 0
+    scripts/busybox-vigia.py --uso      # quién invoca los applets que SÓLO provee busybox
+
+`--uso` responde la pregunta que el plan dejó abierta tres veces: **de lo que todavía sólo da
+busybox, ¿qué usa el producto de verdad?** Lee los applets con `proveedor_medido = SOLO-BUSYBOX`
+del TSV y los busca en el repo con las mismas cuatro reglas anti-ruido del vigía. Lo que no invoca
+nadie es candidato a salir del defconfig sin escribir ni migrar nada; lo que sí, es una migración
+con nombre y número de línea.
+
+⚠ Mira ESTE repo, que es donde vive el producto (cards, guiones de imagen, instalador). NO ve los
+`configure` de terceros que corren dentro del sandbox de build: para ésos el consumidor es el
+frente A, y el criterio es otro.
 
 La lista de retirados se LEE de recipes/busybox.toml (los `# CONFIG_X is not set` que el
 recorte inyecta), cruzada con el mapa applet->símbolo de docs/state/busybox-applets.tsv.
@@ -79,7 +90,122 @@ def funciones_definidas(txt):
            set(re.findall(r'(?m)^\s*function\s+([A-Za-z_][A-Za-z0-9_-]*)\s*\{', txt))
 
 
+def solo_busybox():
+    """Applets vivos cuyo `proveedor_medido` (del censo) dice que no los provee nadie más."""
+    tsv = (RAIZ / 'docs' / 'state' / 'busybox-applets.tsv').read_text().splitlines()
+    cab = tsv[0].split('\t')
+    i_ap, i_prov = cab.index('applet'), cab.index('proveedor_medido')
+    i_rec = cab.index('recorte_2026_09_21')
+    out = set()
+    for ln in tsv[1:]:
+        c = ln.split('\t')
+        if len(c) > max(i_ap, i_prov, i_rec) and c[i_rec] != 'RETIRADO' and c[i_prov] == 'SOLO-BUSYBOX':
+            out.add(c[i_ap])
+    return out
+
+
+def escanear(applets):
+    """[(fichero, línea, applet, texto)] de cada invocación en el repo.
+
+    Busca TRES formas, y las tres hacen falta — con sólo la primera este modo dio dos falsos
+    negativos MEDIDOS, y un falso negativo acá significa retirar un applet que el producto usa:
+      1. nombre desnudo en posición de comando   `killall foo`
+      2. RUTA ABSOLUTA                           `exec /usr/bin/setuidgid pacha` — 3 recetas
+      3. primer elemento de un `argv` JSON       `"argv": ["getty", "-n", ...]` — la card de la
+                                                 consola, takana-bootstrap/src/lib.rs:241
+    La 2 se escapaba porque el patrón de comando pide el nombre pegado al `exec` y ahí hay un
+    `/usr/bin/` en medio; la 3 porque una cadena JSON suelta no parece una línea de comandos.
+    """
+    alt = '|'.join(re.escape(a) for a in sorted(applets, key=len, reverse=True))
+    rx = re.compile(PRE + '(' + alt + ')' + POST, re.M)
+    rx_ruta = re.compile(r'(?:/usr/bin/|/bin/|/usr/sbin/|/sbin/)(' + alt + r')\b')
+    rx_argv = re.compile(r'(?:argv|exec)["\s:=]+\[?\s*"(' + alt + r')"')
+    hits = []
+    for f in RAIZ.rglob('*'):
+        if not f.is_file() or f.suffix not in EXT:
+            continue
+        rel = str(f.relative_to(RAIZ))
+        if any(rel.startswith(s) or ('/' + s) in rel for s in SALTAR):
+            continue
+        # El propio vigía nombra applets en su documentación y en sus regex: se salta a sí mismo,
+        # o se denuncia en cada corrida y el ruido lo vuelve inservible.
+        if f.resolve() == pathlib.Path(__file__).resolve():
+            continue
+        try:
+            crudo = f.read_text(errors='ignore')
+        except OSError:
+            continue
+        if len(crudo) > 900_000:
+            continue
+        propias = funciones_definidas(crudo)
+        txt = sin_comentarios(crudo, f.suffix)
+        lineas = None
+        # Las formas 2 y 3 se buscan sobre el texto sin el filtro de «parece una línea de comandos»:
+        # una ruta absoluta y un `argv` ya son invocaciones por sí mismas.
+        if f.suffix == '.rs':
+            crudo_sc = re.sub(r'(?m)//.*$', '', crudo)
+        elif f.suffix in ('.sh', '.toml', '.py'):
+            crudo_sc = re.sub(r'(?m)(?<!\\)#.*$', '', crudo)
+        else:
+            crudo_sc = crudo
+        sc_lineas = crudo_sc.splitlines()
+        for m2 in list(rx_ruta.finditer(crudo_sc)) + list(rx_argv.finditer(crudo_sc)):
+            n = crudo_sc[:m2.start()].count('\n') + 1
+            hits.append((rel, n, m2.group(1), sc_lineas[n - 1].strip()[:100] if n <= len(sc_lineas) else ''))
+        for m in rx.finditer(txt):
+            ap = m.group(1)
+            if ap in propias:
+                continue
+            antes, despues = txt[:m.start(1)].rstrip(), txt[m.end(1):m.end(1) + 1]
+            if antes.endswith('|') and despues == ')':
+                continue
+            ini = txt.rfind('\n', 0, m.start(1)) + 1
+            fin = txt.find('\n', m.end(1))
+            if re.search(r'command\s+-v\s+' + re.escape(ap) + r'\b', txt[ini:fin if fin > 0 else len(txt)]):
+                continue
+            if lineas is None:
+                lineas = txt.splitlines()
+            n = txt[:m.start()].count('\n') + 1
+            hits.append((rel, n, ap, lineas[n - 1].strip()[:100]))
+    return hits
+
+
+def modo_uso():
+    applets = solo_busybox() - NO_VIGILAR
+    if not applets:
+        print('busybox-vigia --uso: el censo no marca ningún applet como SOLO-BUSYBOX')
+        return 0
+    hits = escanear(applets)
+    usados = {}
+    for rel, n, ap, linea in hits:
+        usados.setdefault(ap, []).append((rel, n, linea))
+    libres = sorted(applets - set(usados))
+    print(f'busybox-vigia --uso: {len(applets)} applets que hoy SÓLO provee busybox\n')
+    print(f'── {len(usados)} INVOCADOS en el repo — cada uno es una migración, no un borrado ──')
+    for ap in sorted(usados):
+        print(f'  {ap}')
+        for rel, n, linea in usados[ap][:4]:
+            print(f'      {rel}:{n}  {linea}')
+        if len(usados[ap]) > 4:
+            print(f'      … y {len(usados[ap]) - 4} más')
+    print(f'\n── {len(libres)} SIN NINGUNA invocación — candidatos al defconfig ──')
+    print('  ' + ' '.join(libres))
+    print()
+    print('\u26a0 \u00abCandidato\u00bb NO es \u00abretirable\u00bb. Un grep ve a quien INVOCA por nombre, y hay dos clases de')
+    print('   consumidor que no aparecen NUNCA y que rompen el producto si se les quita el applet:')
+    print('     \u00b7 el KERNEL — llama a /sbin/modprobe por call_usermodehelper, y al gestor de /dev por')
+    print('       hotplug. modprobe lsmod depmod rmmod modinfo mdev uevent makedevs salen \u00ablibres\u00bb acá')
+    print('       y no lo son: es el frente kmod, que es escritura pendiente, no un borrado.')
+    print('     \u00b7 el USUARIO de la máquina — clear, reset, vi, traceroute no los invoca ningún guión del')
+    print('       repo y son la CLI que alguien espera encontrar al entrar por ssh.')
+    print('   Lo que esta lista sí dice es cuáles no tienen consumidor DENTRO del producto: es el primer')
+    print('   filtro, no el último.')
+    return 0
+
+
 def main():
+    if '--uso' in sys.argv:
+        return modo_uso()
     retirados = applets_retirados() - NO_VIGILAR
     if '--list' in sys.argv:
         print('\n'.join(sorted(retirados)) or '(la receta no retira ningún applet)')