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 1389Shell 137Markdown 80Rust 802602 Definitions

⎇ 77317a72911ad2fe7895b2e2da5a8d9ac812fbad scripts/harkaq/README.md Markdown · 235 lines · 11.5 KB History raw

harkaq: Verdict clasificado (base|esperada|deuda) + el runtime base es por forma de build 77317a729 · sergio ·

1# harkaq — banco de pruebas y piezas
2
3## Las piezas (Fase 1)
4
5| Fichero | Qué es | Dónde corre |
6|---|---|---|
7| `harkaq-audit.c` | el lector de evidencia; emite el `Verdict` JSON | **host**, junto a hammerd; necesita `CAP_AUDIT_READ` |
8| `harkaq-exec.c` | aplica la política y ejecuta el builder | **dentro** de bwrap, estático (rootfs musl) |
9| `harkaq-run.sh` | encadena lector + bwrap + exec + canario | host |
10
11```sh
12mkdir -p ~/.cache/harkaq
13gcc -O1 -Wall -o ~/.cache/harkaq/harkaq-audit harkaq-audit.c
14gcc -O1 -Wall -static -o ~/.cache/harkaq/harkaq-exec harkaq-exec.c
15sudo setcap cap_audit_read,cap_audit_control+ep ~/.cache/harkaq/harkaq-audit   # 1 vez
16
17printf 'ro /usr\nro /bin\nro /lib\nro /lib64\nro /etc\n' > /tmp/p.txt
18./harkaq-run.sh /tmp/p.txt '/bin/echo hola'
19```
20
21**`setcap` se pierde al recompilar** el lector: hay que repetirlo. Es a propósito que el
22privilegio viva en un helper chico y no en hammerd (§8 Q1c): `CAP_AUDIT_*` es superficie de más
23para el daemon entero.
24
25### El hito de Fase 1 (2026-07-15)
26
27```json
28limpio  {"estado":"Hermetico","canario_visto":true,"contador_kernel":1,"denials":[]}
29impuro  {"estado":"Impuro","canario_visto":true,"contador_kernel":2,
30         "denials":[{"blockers":"fs.read_file","path":"/home/sergio/hammer/README.md",…}]}
31```
32
33Canario visto en ambos (la evidencia está *ganada*, no supuesta), contador del kernel coherente
34en ambos, y el impuro nombra el path exacto. Falta el acto real: derivar la política de la
35clausura de una receta de verdad (`harkaq-policy`) y contrastar una receta de-Alpinizada contra
36una que no lo está.
37
38### Sutileza: lo que DAC ya bloquea es invisible
39
40Un acceso que los permisos Unix rechazan **no genera registro de Landlock** — el hook del LSM ni
41llega a correr. Descubierto probando con `/root/.bashrc` (drwx------): el escenario "impuro" dio
42`Hermetico` porque DAC lo bloqueó antes.
43
44No rompe la afirmación de D2 (si DAC lo bloqueó, el build tampoco lo usó ⇒ `Hermetico` sigue
45siendo cierto), pero acota el **diagnóstico**: harkaq no te dice que el build lo *intentó*. Para
46la de-Alpinización da igual — el rootfs Alpine es todo world-readable — pero al escribir tests
47hay que elegir paths que DAC permita, o el control no controla nada.
48
49---
50
51## Banco de pruebas
52
53Ver `docs/16-harkaq-jaula.md`. Acá viven los experimentos que sostienen las afirmaciones del SDD:
54si el doc dice "medido", el que lo midió está en este directorio.
55
56## `q1-audit.c` — el gate del proyecto (Q1, ✅ cerrado 2026-07-15)
57
58Responde: *¿llegan los registros `AUDIT_LANDLOCK_*` a un lector?* Todo harkaq cuelga de eso — su
59producto es la evidencia, no la jaula.
60
61```sh
62gcc -O1 -Wall -o q1-audit q1-audit.c
63for s in same-exec new-exec new-exec-logon; do sudo ./q1-audit $s; done
64```
65
66Necesita root (`CAP_AUDIT_READ` para leer el multicast, `CAP_AUDIT_CONTROL` para encender el
67audit). **Enciende `audit_enabled=1` globalmente y no lo restaura** — reversible, no persiste al
68reboot (no toca el cmdline), pero deja `dmesg` más ruidoso mientras tanto.
69
70### Resultados en el laptop (ABI 10, kernel 7.2.0-rc3)
71
72| Escenario | Registros `ACCESS` | Qué prueba |
73|---|---|---|
74| `same-exec` | 2 | el kernel loguea por defecto sin `execve` |
75| `new-exec` | **0** | **flags por defecto ⇒ ciego tras `execve`** — el caso real de harkaq |
76| `new-exec-logon` | 4 | `LOG_NEW_EXEC_ON` lo arregla |
77
78Los registros traen `blockers=fs.read_file path="/etc/passwd" dev="nvme1n1p6" ino=4458369` y el
79`exe=` que lo intentó. Análisis completo en `docs/16-harkaq-jaula.md` §3.4.
80
81### Por qué aborta con código 4
82
83Si no puede **confirmar** `audit_enabled=1`, aborta en vez de reportar 0 registros. No es
84paranoia: la primera corrida dio 0 en los tres escenarios y parecía confirmar la hipótesis. Era un
85bug del instrumento (`AUDIT_GET` con `NLM_F_ACK` → el ACK llegaba antes que la respuesta → nunca
86se encendía el audit). Se detectó sólo porque el **control** (`same-exec`) también dio 0, y eso era
87imposible.
88
89Es el modo de falla de harkaq sobre su propio banco de pruebas: un sistema cuyo producto es la
90*ausencia* de algo no falla ruidosamente — dice que todo está bien. De ahí sale D9 (el canario
91deliberado), y de ahí sale la regla de este directorio: **todo experimento lleva un control que
92debe dar positivo.** Un experimento sin control no mide, tranquiliza.
93
94## `q1b-attrib.c` — userns y atribución (Q1b, ✅ cerrado 2026-07-15)
95
96Responde: *¿el lector del host ve las denegaciones de adentro de bwrap, y se puede atribuir cada
97registro a SU build con la granja en paralelo?*
98
99```sh
100gcc -O1 -Wall -o ~/.cache/harkaq/q1b q1b-attrib.c   # NO compilar bajo /tmp: --tmpfs /tmp lo tapa
101sudo ~/.cache/harkaq/q1b
102```
103
104Lanza 2 víctimas concurrentes en bwrap `--unshare-all` (los ns de `Sandbox::bwrap_args`), cada una
105con un canario de nonce distinto, y escucha desde el host. Las víctimas bajan a `$SUDO_UID`: el
106despliegue real es lector privilegiado + builds sin privilegio.
107
108Resultado: los registros **cruzan** el userns/pidns/mountns, y el canario con nonce **atribuye**
109sin ambigüedad (`AAAA=1 BBBB=1`, cero cruces). Análisis en `docs/16-harkaq-jaula.md` §3.5.
110
111### Los tres rastrillos de este banco, para el próximo que lo toque
112
1131. **No compilar bajo `/tmp`**: el sandbox hace `--tmpfs /tmp` y tapa el binario.
1142. **El canario necesita un punto de montaje escribible.** Con `--ro-bind / /`, bwrap no puede
115   crear `/harkaq-canary-X` en la raíz de sólo lectura (el sandbox real usa `--tmp-overlay /` y no
116   tendría el problema). Va bajo `/tmp`, y por eso `/tmp` **no** entra en la clausura de la víctima.
1173. **Root + `--unshare-all` no atraviesa un home `drwx------`.** bwrap mapea sólo uid 0→0; los
118   ficheros de uid 1000 quedan sin mapear y `CAP_DAC_OVERRIDE` en un userns sólo vale sobre uids
119   **mapeados**. De ahí `drop_privs()`.
120
121### Por qué también aborta con código 4
122
123Mismo gate que `q1-audit`, más uno: **si alguna víctima no sale con status 0, aborta** en vez de
124contar registros. La corrida que lo motivó imprimió *"NADA cruzó ⇒ harkaq-audit no puede vivir en
125el host: replantear"* cuando bwrap ni siquiera había podido ejecutar el binario. Una conclusión
126arquitectónica falsa, a partir de cero estímulo.
127
128Van tres veces en una sola sesión (`NLM_F_ACK`, el redirect a `/dev/null`, el exec) que un cero
129resultó ser el instrumento y no el kernel. Un veredicto que no verifica su propio estímulo es el
130falso `Hermetico` de D9, pero del lado del banco.
131
132## `harkaq-policy.sh` + `q2-runtime-base.sh` — la clausura y el runtime base
133
134`harkaq-policy.sh <dir-dep>...` deriva la política de la clausura (D1: no se escribe, se deriva).
135Enumera **fichero a fichero** —no por directorio— porque el sandbox funde las deps y Alpine en el
136mismo `/usr` (§4.1). Emite `list` de los directorios para que pkgconf/configure puedan escanear
137sin poder leer lo no declarado.
138
139`q2-runtime-base.sh` responde Q2 midiendo, no adivinando: corre un build real en el sandbox real
140con `política = clausura` y deja que las denegaciones nombren el runtime base. Se itera con
141`BASE_EXTRA` (una línea por regla), y **cada línea que se añade ahí debe venir de una denegación
142que el kernel denunció, nunca de una corazonada** — si se define de más, la política concede
143Alpine entero y la evidencia deja de valer.
144
145```sh
146export BASE_EXTRA='ro /bin/sh
147ro /bin/busybox
148ro /lib/ld-musl-x86_64.so.1'
149HARKAQ_TIMEOUT=25 scripts/harkaq/q2-runtime-base.sh
150```
151
152Resultado (2026-07-15, contra la dep `zlib` del store):
153
154```
155estado: Impuro | canario: True | contador kernel: 3
156    fs.read_file /usr/bin/env
157    fs.read_file /usr/lib/libz.so.1.3.2      ← ¡la zlib de ALPINE, no la dep declarada!
158```
159
160### Dos rastrillos más, aprendidos acá
161
1621. **El canario no puede depender de un binario.** Con `cat $CANARY` moría en `/bin/cat` (applet
163   de busybox fuera de la clausura) y el probe no probaba nada. Ahora usa sólo builtins:
164   `read _ < $CANARY || true`.
1652. **El canario no puede vivir en `/tmp`.** La política concede `rw /tmp` (es el HOME del build),
166   así que un canario ahí cae DENTRO de la clausura, se lee sin problema y no deniega nada ⇒
167   `SinEvidencia` eterno. Va en la raíz, donde la política no alcanza por construcción.
168
169El segundo lo cazó **D9 mismo**: el lector se negó a certificar (`SinEvidencia`) en vez de decir
170`Hermetico`. El canario funcionando como se diseñó, sobre un bug de quien lo diseñó.
171
172### Q2 resuelta: el runtime base son 5 paths (2026-07-15)
173
174`MODE=base` corre **sin ninguna dep declarada**: sin clausura que pueda explicarlas, toda
175denegación es runtime base por definición. Eso aísla la respuesta sin juicio de valor.
176
177```sh
178export BASE_EXTRA='ro /bin/sh
179ro /bin/busybox
180ro /lib/ld-musl-x86_64.so.1
181ro /usr/bin/env
182ro /usr/lib/os-release
183rw /dev/null
184ro /dev/urandom'
185MODE=base scripts/harkaq/q2-runtime-base.sh   # rc=0, sólo queda /usr/bin/gcc (esperada)
186MODE=zlib scripts/harkaq/q2-runtime-base.sh   # Impuro: [/usr/lib/libz.so.1.3.2] — deuda pura
187```
188
189Tres categorías, todas medidas (detalle en `docs/16-harkaq-jaula.md` §4.2):
190
191- **Contrato**: `/src /out /tmp /dev/null(rw) /dev/urandom /proc /opt/zig` — los monta bwrap, no
192  son Alpine.
193- **Runtime base Alpine**: `/bin/sh /bin/busybox /lib/ld-musl /usr/bin/env /usr/lib/os-release`.
194  Cinco. El resto de Alpine es deuda genuina.
195- **Denegación esperada**: `/usr/bin/gcc` — `zig cc` lo sondea y el build sale `rc=0` igual.
196  Concederla dejaría a zig usar el gcc de Alpine por detrás. **Se deniega y se clasifica** (D3).
197
198Ojo con dos: `/dev/null` es destino de **escritura** (`rw`, no `ro` — con `ro` quedan
199`fs.write_file` colgando), y el piso duro es `/bin/sh`→`busybox`→`ld-musl`: sin eso ni el canario
200corre y no hay diagnóstico, sólo `SinEvidencia`.
201
202### ¿Aguantan los 5 paths en otra forma de build? No — pero converge (§4.3)
203
204`MODE=configure` corre un `configure` de autotools **real** (`work/sources/libgpg-error-*`,
205override con `Q2_SRC`). Iterando con el mismo método:
206
207| Ronda | Deuda restante (paths únicos) |
208|---|---|
209| 1 | `/bin/bash`, `/bin/coreutils` |
210| 2 | `libacl`, `libattr`, `libcrypto`, `libreadline`, `libutmps` |
211| 3 | `libncursesw`, `libskarnet` |
212| 4 | `/usr/bin/c89`, `/usr/bin/c99`, `/usr/bin/ldd`, `/usr/bin/make` |
213
214El runtime base es **por forma de build** (~16 entradas para autotools, no 5), pero **converge en
2153 rondas y se queda chico** ⇒ la métrica de <5% de la Fase 2 sigue siendo plausible.
216
217Lo importante: **el runtime base no es una lista de binarios, es su cierre de `.so`** (bash
218arrastra libreadline+libncursesw; coreutils arrastra libacl+libattr). Es computable con `ldd`, no
219adivinable — `harkaq-policy` debería derivarlo igual que deriva la clausura de las deps.
220
221Y el residuo es todo señal: `c89`/`c99`/`ldd` son sondas de compilador de Alpine (→ esperadas,
222como `gcc`), y `/usr/bin/make` es una decisión de diseño real — hoy los builds usan el `make` de
223Alpine sin declararlo. **Es la misma lista que reemplazan los swaps del `selfhost-verify`**,
224descubierta por otro camino.
225
226### El clasificador va FUERA del lector
227
228`harkaq-verdict.py <verdict-crudo> <politica> [--human]`. El lector tiene `CAP_AUDIT_READ`;
229clasificar es política, no privilegio. Mantenerlo tonto es el argumento de Q1c, y de yapa se
230itera la clasificación sin recompilar el binario capabilitado (sin perder el `setcap`).
231
232Las expectativas viajan en la política como `# expect <path>` — una sola fuente de verdad, y
233`harkaq-exec` las ignora como comentario. `SinEvidencia` manda sobre todo: si el lector no es
234confiable no se clasifica nada, porque reinterpretarlo sería el falso `Hermetico` que el canario
235existe para impedir.

README.md

harkaq — banco de pruebas y piezas

Las piezas (Fase 1)

FicheroQué esDónde corre
harkaq-audit.cel lector de evidencia; emite el Verdict JSONhost, junto a hammerd; necesita CAP_AUDIT_READ
harkaq-exec.caplica la política y ejecuta el builderdentro de bwrap, estático (rootfs musl)
harkaq-run.shencadena lector + bwrap + exec + canariohost
mkdir -p ~/.cache/harkaq
gcc -O1 -Wall -o ~/.cache/harkaq/harkaq-audit harkaq-audit.c
gcc -O1 -Wall -static -o ~/.cache/harkaq/harkaq-exec harkaq-exec.c
sudo setcap cap_audit_read,cap_audit_control+ep ~/.cache/harkaq/harkaq-audit   # 1 vez

printf 'ro /usr\nro /bin\nro /lib\nro /lib64\nro /etc\n' > /tmp/p.txt
./harkaq-run.sh /tmp/p.txt '/bin/echo hola'

setcap se pierde al recompilar el lector: hay que repetirlo. Es a propósito que el privilegio viva en un helper chico y no en hammerd (§8 Q1c): CAP_AUDIT_* es superficie de más para el daemon entero.

El hito de Fase 1 (2026-07-15)

limpio  {"estado":"Hermetico","canario_visto":true,"contador_kernel":1,"denials":[]}
impuro  {"estado":"Impuro","canario_visto":true,"contador_kernel":2,
         "denials":[{"blockers":"fs.read_file","path":"/home/sergio/hammer/README.md",…}]}

Canario visto en ambos (la evidencia está ganada, no supuesta), contador del kernel coherente en ambos, y el impuro nombra el path exacto. Falta el acto real: derivar la política de la clausura de una receta de verdad (harkaq-policy) y contrastar una receta de-Alpinizada contra una que no lo está.

Sutileza: lo que DAC ya bloquea es invisible

Un acceso que los permisos Unix rechazan no genera registro de Landlock — el hook del LSM ni llega a correr. Descubierto probando con /root/.bashrc (drwx------): el escenario "impuro" dio Hermetico porque DAC lo bloqueó antes.

No rompe la afirmación de D2 (si DAC lo bloqueó, el build tampoco lo usó ⇒ Hermetico sigue siendo cierto), pero acota el diagnóstico: harkaq no te dice que el build lo intentó. Para la de-Alpinización da igual — el rootfs Alpine es todo world-readable — pero al escribir tests hay que elegir paths que DAC permita, o el control no controla nada.


Banco de pruebas

Ver docs/16-harkaq-jaula.md. Acá viven los experimentos que sostienen las afirmaciones del SDD: si el doc dice "medido", el que lo midió está en este directorio.

q1-audit.c — el gate del proyecto (Q1, ✅ cerrado 2026-07-15)

Responde: ¿llegan los registros AUDIT_LANDLOCK_* a un lector? Todo harkaq cuelga de eso — su producto es la evidencia, no la jaula.

gcc -O1 -Wall -o q1-audit q1-audit.c
for s in same-exec new-exec new-exec-logon; do sudo ./q1-audit $s; done

Necesita root (CAP_AUDIT_READ para leer el multicast, CAP_AUDIT_CONTROL para encender el audit). Enciende audit_enabled=1 globalmente y no lo restaura — reversible, no persiste al reboot (no toca el cmdline), pero deja dmesg más ruidoso mientras tanto.

Resultados en el laptop (ABI 10, kernel 7.2.0-rc3)

EscenarioRegistros ACCESSQué prueba
same-exec2el kernel loguea por defecto sin execve
new-exec0flags por defecto ⇒ ciego tras execve — el caso real de harkaq
new-exec-logon4LOG_NEW_EXEC_ON lo arregla

Los registros traen blockers=fs.read_file path="/etc/passwd" dev="nvme1n1p6" ino=4458369 y el exe= que lo intentó. Análisis completo en docs/16-harkaq-jaula.md §3.4.

Por qué aborta con código 4

Si no puede confirmar audit_enabled=1, aborta en vez de reportar 0 registros. No es paranoia: la primera corrida dio 0 en los tres escenarios y parecía confirmar la hipótesis. Era un bug del instrumento (AUDIT_GET con NLM_F_ACK → el ACK llegaba antes que la respuesta → nunca se encendía el audit). Se detectó sólo porque el control (same-exec) también dio 0, y eso era imposible.

Es el modo de falla de harkaq sobre su propio banco de pruebas: un sistema cuyo producto es la ausencia de algo no falla ruidosamente — dice que todo está bien. De ahí sale D9 (el canario deliberado), y de ahí sale la regla de este directorio: todo experimento lleva un control que debe dar positivo. Un experimento sin control no mide, tranquiliza.

q1b-attrib.c — userns y atribución (Q1b, ✅ cerrado 2026-07-15)

Responde: ¿el lector del host ve las denegaciones de adentro de bwrap, y se puede atribuir cada registro a SU build con la granja en paralelo?

gcc -O1 -Wall -o ~/.cache/harkaq/q1b q1b-attrib.c   # NO compilar bajo /tmp: --tmpfs /tmp lo tapa
sudo ~/.cache/harkaq/q1b

Lanza 2 víctimas concurrentes en bwrap --unshare-all (los ns de Sandbox::bwrap_args), cada una con un canario de nonce distinto, y escucha desde el host. Las víctimas bajan a $SUDO_UID: el despliegue real es lector privilegiado + builds sin privilegio.

Resultado: los registros cruzan el userns/pidns/mountns, y el canario con nonce atribuye sin ambigüedad (AAAA=1 BBBB=1, cero cruces). Análisis en docs/16-harkaq-jaula.md §3.5.

Los tres rastrillos de este banco, para el próximo que lo toque

  1. No compilar bajo /tmp: el sandbox hace --tmpfs /tmp y tapa el binario.
  2. El canario necesita un punto de montaje escribible. Con --ro-bind / /, bwrap no puede crear /harkaq-canary-X en la raíz de sólo lectura (el sandbox real usa --tmp-overlay / y no tendría el problema). Va bajo /tmp, y por eso /tmp no entra en la clausura de la víctima.
  3. Root + --unshare-all no atraviesa un home drwx------. bwrap mapea sólo uid 0→0; los ficheros de uid 1000 quedan sin mapear y CAP_DAC_OVERRIDE en un userns sólo vale sobre uids mapeados. De ahí drop_privs().

Por qué también aborta con código 4

Mismo gate que q1-audit, más uno: si alguna víctima no sale con status 0, aborta en vez de contar registros. La corrida que lo motivó imprimió "NADA cruzó ⇒ harkaq-audit no puede vivir en el host: replantear" cuando bwrap ni siquiera había podido ejecutar el binario. Una conclusión arquitectónica falsa, a partir de cero estímulo.

Van tres veces en una sola sesión (NLM_F_ACK, el redirect a /dev/null, el exec) que un cero resultó ser el instrumento y no el kernel. Un veredicto que no verifica su propio estímulo es el falso Hermetico de D9, pero del lado del banco.

harkaq-policy.sh + q2-runtime-base.sh — la clausura y el runtime base

harkaq-policy.sh <dir-dep>... deriva la política de la clausura (D1: no se escribe, se deriva). Enumera fichero a fichero —no por directorio— porque el sandbox funde las deps y Alpine en el mismo /usr (§4.1). Emite list de los directorios para que pkgconf/configure puedan escanear sin poder leer lo no declarado.

q2-runtime-base.sh responde Q2 midiendo, no adivinando: corre un build real en el sandbox real con política = clausura y deja que las denegaciones nombren el runtime base. Se itera con BASE_EXTRA (una línea por regla), y cada línea que se añade ahí debe venir de una denegación que el kernel denunció, nunca de una corazonada — si se define de más, la política concede Alpine entero y la evidencia deja de valer.

export BASE_EXTRA='ro /bin/sh
ro /bin/busybox
ro /lib/ld-musl-x86_64.so.1'
HARKAQ_TIMEOUT=25 scripts/harkaq/q2-runtime-base.sh

Resultado (2026-07-15, contra la dep zlib del store):

estado: Impuro | canario: True | contador kernel: 3
    fs.read_file /usr/bin/env
    fs.read_file /usr/lib/libz.so.1.3.2      ← ¡la zlib de ALPINE, no la dep declarada!

Dos rastrillos más, aprendidos acá

  1. El canario no puede depender de un binario. Con cat $CANARY moría en /bin/cat (applet de busybox fuera de la clausura) y el probe no probaba nada. Ahora usa sólo builtins: read _ < $CANARY || true.
  2. El canario no puede vivir en /tmp. La política concede rw /tmp (es el HOME del build), así que un canario ahí cae DENTRO de la clausura, se lee sin problema y no deniega nada ⇒ SinEvidencia eterno. Va en la raíz, donde la política no alcanza por construcción.

El segundo lo cazó D9 mismo: el lector se negó a certificar (SinEvidencia) en vez de decir Hermetico. El canario funcionando como se diseñó, sobre un bug de quien lo diseñó.

Q2 resuelta: el runtime base son 5 paths (2026-07-15)

MODE=base corre sin ninguna dep declarada: sin clausura que pueda explicarlas, toda denegación es runtime base por definición. Eso aísla la respuesta sin juicio de valor.

export BASE_EXTRA='ro /bin/sh
ro /bin/busybox
ro /lib/ld-musl-x86_64.so.1
ro /usr/bin/env
ro /usr/lib/os-release
rw /dev/null
ro /dev/urandom'
MODE=base scripts/harkaq/q2-runtime-base.sh   # rc=0, sólo queda /usr/bin/gcc (esperada)
MODE=zlib scripts/harkaq/q2-runtime-base.sh   # Impuro: [/usr/lib/libz.so.1.3.2] — deuda pura

Tres categorías, todas medidas (detalle en docs/16-harkaq-jaula.md §4.2):

Ojo con dos: /dev/null es destino de escritura (rw, no ro — con ro quedan fs.write_file colgando), y el piso duro es /bin/sh→busybox→ld-musl: sin eso ni el canario corre y no hay diagnóstico, sólo SinEvidencia.

¿Aguantan los 5 paths en otra forma de build? No — pero converge (§4.3)

MODE=configure corre un configure de autotools real (work/sources/libgpg-error-*, override con Q2_SRC). Iterando con el mismo método:

RondaDeuda restante (paths únicos)
1/bin/bash, /bin/coreutils
2libacl, libattr, libcrypto, libreadline, libutmps
3libncursesw, libskarnet
4/usr/bin/c89, /usr/bin/c99, /usr/bin/ldd, /usr/bin/make

El runtime base es por forma de build (~16 entradas para autotools, no 5), pero converge en 3 rondas y se queda chico ⇒ la métrica de <5% de la Fase 2 sigue siendo plausible.

Lo importante: el runtime base no es una lista de binarios, es su cierre de .so (bash arrastra libreadline+libncursesw; coreutils arrastra libacl+libattr). Es computable con ldd, no adivinable — harkaq-policy debería derivarlo igual que deriva la clausura de las deps.

Y el residuo es todo señal: c89/c99/ldd son sondas de compilador de Alpine (→ esperadas, como gcc), y /usr/bin/make es una decisión de diseño real — hoy los builds usan el make de Alpine sin declararlo. Es la misma lista que reemplazan los swaps del selfhost-verify, descubierta por otro camino.

El clasificador va FUERA del lector

harkaq-verdict.py <verdict-crudo> <politica> [--human]. El lector tiene CAP_AUDIT_READ; clasificar es política, no privilegio. Mantenerlo tonto es el argumento de Q1c, y de yapa se itera la clasificación sin recompilar el binario capabilitado (sin perder el setcap).

Las expectativas viajan en la política como # expect <path> — una sola fuente de verdad, y harkaq-exec las ignora como comentario. SinEvidencia manda sobre todo: si el lector no es confiable no se clasifica nada, porque reinterpretarlo sería el falso Hermetico que el canario existe para impedir.