Files
takana/docs/state/kernel-contract.toml
T
SergioandClaude Opus 5 abda7d3cc1 contrato de kernel: alta de pidfd, y los consumidores dejan de describir un bug ya arreglado
Handoff de vuelta desde tawasuyu (SDD 25 §8): las seis tareas del otro lado
estan cerradas, y eso dejo este contrato mintiendo de dos formas.

- Alta de `proceso-por-descriptor` (pidfd_open / pidfd_send_signal). Se usa
  desde W1 y no estaba declarada. `symbols` vacio a proposito: no depende de
  ningun CONFIG_*, es interfaz del core desde Linux 5.3. Al no estar en ningun
  perfil no se comprueba contra un .config; entra porque el contrato es la
  lista de lo que se USA, y si manana el minimo de kernel baja de 5.3, esta
  linea es la que lo dice.
- Los cinco consumidores de cgroup llevaban numero de linea y quedaron viejos
  con W1-W4. Van por nombre de funcion, y la regla de escritura queda arriba.
- Los `silent` de esos cinco describian el bug que W4 arreglo ("solo emite un
  warn!", "silencio total"). Un guardian que dice que hay un punto ciego donde
  ya no lo hay es peor que no tenerlo.
- `contabilidad-por-tarea` decia que sandokan sondea /proc para medir una
  unidad. Desde W2 eso sale del cgroup en O(1); el que sigue sondeando /proc es
  el monitor de procesos del SISTEMA, que es otro consumidor.

Y en SDD 25 §8, el estado real: las seis cerradas, mas W4.bis, donde el hallazgo
no fue el que este documento suponia. En arje el limite no se descartaba: NO SE
PEDIA. El camino `plain` -el de casi todas las Cards- no creaba cgroup, y
`apply_rlimits_to_cgroup` la llamaba solo shuma.

Verificado con el binario, no de memoria: `hammer kernel contract --list` carga
las 14 capacidades, y `hammer kernel contract --profile anfitrion-cards` sigue
en verde (11 exigidas presentes, 13 miradas).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiYsdSwF1nxjBTTsa1empe
2026-09-03 20:27:05 +00:00

260 lines
12 KiB
TOML

# Contrato de capacidades del kernel (SDD 25 §4, §7-H1 y §8-W6).
#
# QUÉ ES. La lista de pedazos de interfaz de kernel que el userland de hammer/tawasuyu usa POR
# NOMBRE, con su consumidor y con **cómo falla hoy si no están**. Se comprueba con
# `hammer kernel contract` contra un `.config` YA PRODUCIDO (el que la receta instala en
# `/out/boot/config-<versión>`), nunca contra la receta: entre el `scripts/config -e X` y el
# `.config` hay un `olddefconfig` que puede tragarse el símbolo en silencio.
#
# POR QUÉ EXISTE. SDD 25 encontró que los kernels de hammer se construyen sin `CONFIG_MEMCG`, y
# que el escritor del otro lado descarta el error: una Card que pide un tope de memoria arranca
# SIN tope y la única huella es un `warn!`. La máquina de desarrollo no lo ve porque su kernel
# (Artix) sí trae MEMCG. Este fichero es el guardián de ese punto ciego.
#
# POR QUÉ PERFILES. Misma lección que el gate de hardware: `recipes/linux.toml` es el kernel de
# QEMU con consola serie del selfhost-verify y no hospeda Cards. Un contrato global rechazaría
# una receta sana.
#
# REGLA DE ALTA. Una capacidad entra acá sólo si hay un consumidor con ruta y símbolo. Lo que
# «estaría bueno tener» no es un contrato.
#
# REGLA DE ESCRITURA. Los `consumer` van por **nombre de función, sin número de línea**. Los
# números envejecen con cada commit del otro repo y ya lo hicieron: entre W1 y W4 (2026-09-02)
# los cinco `consumer` de cgroups quedaron apuntando a líneas movidas, y —peor— sus `silent`
# describían un bug ya arreglado («sólo emite un warn!», «silencio total»), que es la forma de
# mentir más cara que tiene un guardián: decir que hay un punto ciego donde ya no lo hay.
version = 1
reviewed_against = "7.1.2"
# ---------------------------------------------------------------------------------------------
# Capacidades
# ---------------------------------------------------------------------------------------------
[[capability]]
id = "limite-memoria"
title = "Tope de memoria por unidad"
symbols = ["MEMCG"]
interface = ["memory.max", "memory.high", "memory.current"]
consumer = """
arje-incarnate::cgroup::{preparar, apply_rlimits_to_cgroup, set_memory_max_at, set_memory_high_at} \
← sandokan-core::Engine::{set_memory_max,set_memory_high} ← sandokan-monitor-llimphi (govierna \
memory.high) · sandokan-local::medir_entidad lee `memory.current`"""
silent = """
YA NO ES SILENCIOSO (tawasuyu, W4 el 2026-09-02 y W4.bis el 2026-09-03). Sin MEMCG los ficheros \
NO EXISTEN, y ahora eso se dice: `apply_rlimits_to_cgroup` devuelve `Limites{aplicados, faltantes}` \
con el símbolo que falta —y sólo acusa al kernel si pudo leer `/sys/fs/cgroup/cgroup.controllers` \
y el controller no estaba—; arje lo marca por unidad (`Degradation::CgroupLimitNotApplied`, visible \
en `arje-ctl status` como `corriendo⚠`) y shuma lo devuelve en las `warnings` de `create`. \
Lo que este contrato SÍ sigue sin poder desmentir: nadie vio todavía el OOM-kill del kernel sobre \
un Ente de arje en una máquina con delegación de cgroups."""
notes = """
PAGADO 2026-08-30 (SDD 25 §7-H1): `-e MEMCG` en la fase configure de linux-generic, linux-metal, \
linux-metal-dual y la derivada linux-gioser, en una sola tanda porque re-hashea el kernel \
(SDD 22 §1). Costó +80/84/88/80 KiB de bzImage. `linux.toml` queda fuera a propósito: perfil \
qemu-serial, sin Cards, y su hash es el baseline del selfhost-verify."""
[[capability]]
id = "peso-cpu"
title = "Reparto proporcional de CPU por unidad"
symbols = ["CGROUPS", "CGROUP_SCHED", "FAIR_GROUP_SCHED"]
interface = ["cpu.weight", "cpu.stat"]
consumer = "arje-incarnate::cgroup::{preparar, ensure_cgroup, set_cpu_weight_at}"
silent = """
YA NO ES SILENCIOSO (W4, 2026-09-02): el `let _ = write(cpu.weight)` murió. El peso que no se \
pudo escribir vuelve al caller nombrando `CGROUP_SCHED`, y el reweight en caliente de `pacha` \
también (W4.bis: `set_cpu_weight_at` pasa por `escribir_control`, que exige nombrar el símbolo)."""
[[capability]]
id = "tope-procesos"
title = "Tope de procesos por unidad"
symbols = ["CGROUPS", "CGROUP_PIDS"]
interface = ["pids.max", "pids.current"]
consumer = """
arje-incarnate::cgroup::{preparar, apply_rlimits_to_cgroup} · sandokan-local::medir_entidad lee \
`pids.current`"""
silent = """
Mismo patrón que memory.max, y arreglado con él: el tope que no se pudo escribir vuelve al caller \
y marca la unidad. ⚠️ Hasta W4.bis (2026-09-03) había algo peor que un `warn!`: en arje el tope \
NO SE PEDÍA. El camino `plain` —el de casi todas las Cards, con los namespaces en `false`— no \
creaba cgroup, y `apply_rlimits_to_cgroup` la llamaba sólo shuma."""
[[capability]]
id = "peso-io"
title = "Reparto de E/S por unidad"
symbols = ["BLK_CGROUP", "BLK_CGROUP_IOCOST"]
interface = ["io.weight", "io.cost.qos", "io.cost.model"]
consumer = "arje-incarnate::cgroup::{preparar, ensure_cgroup, set_io_weight_at}"
silent = """
YA NO ES SILENCIOSO (W4, 2026-09-02): el `let _ = write(io.weight)` murió, igual que el de \
`cpu.weight`. El peso que no se pudo escribir vuelve al caller nombrando `BLK_CGROUP`."""
notes = """
`io.weight` con sintaxis «default N» lo publica iocost, no BFQ — por eso el contrato pide \
BLK_CGROUP_IOCOST y no IOSCHED_BFQ (que está apagado en las recetas y no hace falta)."""
[[capability]]
id = "cpus-por-unidad"
title = "Pinneo de CPUs por unidad"
symbols = ["CPUSETS"]
interface = ["cpuset.cpus", "cpuset.mems"]
consumer = "arje-incarnate::cgroup::set_cpuset_at"
[[capability]]
id = "jaula-de-rutas"
title = "Landlock: la jaula de rutas de harkaq"
symbols = ["SECURITY", "SECURITY_LANDLOCK"]
interface = ["landlock_create_ruleset(2)", "landlock_restrict_self(2)"]
consumer = "hammer-build::harkaq (SDD 16) y hammer-build::sandbox"
silent = """
Landlock ausente no rompe el build: la jaula simplemente no se aplica y el veredicto por fase \
sale igual de verde. Evidencia negativa que no prueba nada."""
[[capability]]
id = "jaula-de-syscalls"
title = "seccomp: el filtro de syscalls de harkaq"
symbols = ["SECCOMP", "SECCOMP_FILTER"]
interface = ["seccomp(2) SECCOMP_SET_MODE_FILTER"]
consumer = "hammer-build::harkaq (fase 1 cerrada, barrido en granja)"
[[capability]]
id = "namespaces-de-build"
title = "Namespaces para el sandbox de build"
symbols = ["NAMESPACES", "USER_NS", "PID_NS", "UTS_NS", "IPC_NS"]
interface = ["unshare(2)", "bwrap"]
consumer = "hammer-build::sandbox (bwrap sin pre_exec — ver SDD 25 §7-H4)"
silent = "Sin USER_NS bwrap falla ruidosamente; entra al contrato porque es la base de todo build."
[[capability]]
id = "hidratacion-overlay"
title = "Overlay con metacopy para hidratar árboles"
symbols = ["OVERLAY_FS", "OVERLAY_FS_REDIRECT_DIR", "OVERLAY_FS_INDEX", "OVERLAY_FS_XINO_AUTO", "OVERLAY_FS_METACOPY"]
interface = ["mount -t overlay"]
consumer = "hammer-overlay / `hammer hydrate` (docs/04-overlay.md)"
[[capability]]
id = "journal-fanotify"
title = "Journal de accesos por fanotify"
symbols = ["FANOTIFY", "FANOTIFY_ACCESS_PERMISSIONS"]
interface = ["fanotify_init(2) FAN_CLASS_CONTENT"]
consumer = "hammerd::watcher / hammer-journal (handoff arje→hammer, frente 2)"
[[capability]]
id = "io-uring"
title = "io_uring"
symbols = ["IO_URING"]
interface = ["io_uring_setup(2)"]
consumer = "kikin J4 (PLAN-KIKIN §4.bis) — el sustrato se declara explícito, no «al azar del defconfig»"
[[capability]]
id = "presion-de-recursos"
title = "PSI: presión de CPU/memoria/E/S"
symbols = ["PSI"]
interface = ["/proc/pressure/{cpu,memory,io}", "cpu.pressure", "memory.pressure"]
consumer = """
sandokan-monitor-llimphi — PREVISTO, no actual: hoy gobierna por muestreo. Se declara como \
`wants` para que el día que se use, el kernel ya lo traiga."""
silent = "Los ficheros no existen y el lector cae al muestreo sin decir que degradó."
[[capability]]
id = "contabilidad-por-tarea"
title = "taskstats + proc connector (el reemplazo de sondear /proc)"
symbols = ["TASKSTATS", "TASK_DELAY_ACCT", "CONNECTOR", "PROC_EVENTS"]
interface = ["netlink NETLINK_CONNECTOR", "TASKSTATS_CMD_GET"]
consumer = """
sandokan-local — PREVISTO (SDD 25 T3/W2). ⚠️ Ya no es cierto que sandokan sondee `/proc` para \
medir una unidad: desde W2 (2026-09-02) la telemetría POR UNIDAD sale del cgroup en O(1) \
(`arje-incarnate::cgroup::medir` → `memory.current`/`pids.current`/`cpu.stat`). Lo que sigue \
sondeando `/proc` es el monitor de procesos del SISTEMA (`sysmon-core::scan`), que necesita \
enumerar todo y es otro consumidor. Se declara `wants` porque el kernel ya lo trae por defconfig \
y perderlo sería una regresión silenciosa."""
[[capability]]
id = "proceso-por-descriptor"
title = "pidfd: agarrar un proceso por descriptor y no por su número"
symbols = []
interface = ["pidfd_open(2)", "pidfd_send_signal(2)"]
consumer = "sandokan-local::pidfd (backend opcional, `disponible()`) ← LocalEngine::{stop, reap}"
silent = """
NINGUNO, y por eso `symbols` va vacío: pidfd no depende de ningún `CONFIG_*` — es interfaz del \
core desde Linux 5.3, así que en un kernel de hammer va a estar siempre. Entra al contrato \
porque el contrato es la lista de lo que se USA, no la de lo que puede faltar; si mañana el \
mínimo de kernel baja de 5.3, esta línea es la que lo dice. `sandokan kernel` la mide con una \
syscall de prueba (no hay `.config` que leer en el Ubuntu ajeno) y tiene tercer estado \
`NoSeSabe`: un «no pude preguntar» informado como «no está» manda a recompilar un kernel sano."""
notes = """
ALTA PEDIDA DESDE TAWASUYU (2026-09-03, handoff de tasas del kernel §3, «dos cosas que le tocan \
a hammer»). Se usa desde W1 (2026-09-02): `pidfd_open` al encarnar —único momento sin carrera, \
porque un hijo sin cosechar no cede su número—, el descriptor vive en la `Entity`, `stop` señala \
por ahí y la rama `ECHILD` de `reap` le pregunta al descriptor. `/proc` queda entero detrás (W5)."""
# ---------------------------------------------------------------------------------------------
# Perfiles: PARA QUÉ es cada kernel
# ---------------------------------------------------------------------------------------------
[[profile]]
id = "anfitrion-cards"
title = "Hospeda Cards de sandokan/arje con límites"
help = """
El kernel de un sistema hammer real. Acá vive el contrato entero: si una Card pide un tope, el \
tope tiene que existir."""
requires = [
"limite-memoria",
"peso-cpu",
"tope-procesos",
"peso-io",
"cpus-por-unidad",
"jaula-de-rutas",
"jaula-de-syscalls",
"namespaces-de-build",
"hidratacion-overlay",
"journal-fanotify",
"io-uring",
]
wants = ["presion-de-recursos", "contabilidad-por-tarea"]
[[profile]]
id = "qemu-serial"
title = "Kernel de QEMU con consola serie (selfhost-verify)"
help = """
`recipes/linux.toml`. No hospeda Cards: construye hammer dentro de una VM y su ArtifactHash es \
LOAD-BEARING del baseline `of_tree 9adefb82` del selfhost-verify. Por eso los límites por unidad \
son `wants` y no `requires`: subirlos acá cuesta rehacer el baseline, y el consumidor no existe."""
requires = [
"namespaces-de-build",
"hidratacion-overlay",
"jaula-de-rutas",
"jaula-de-syscalls",
"io-uring",
]
wants = ["limite-memoria", "peso-cpu", "tope-procesos", "journal-fanotify"]
# ---------------------------------------------------------------------------------------------
# Qué perfil le toca a cada artefacto. Un kernel sin fila acá NO se comprueba y se informa:
# un portón que calla lo que no pudo mirar no es un portón.
# ---------------------------------------------------------------------------------------------
[[target]]
artifact = "linux"
profile = "qemu-serial"
notes = "6.16.12, QEMU-tuned, INTOCADO (baseline del selfhost-verify)."
[[target]]
artifact = "linux-generic"
profile = "anfitrion-cards"
notes = "7.1.2, hardware variado de testers."
[[target]]
artifact = "linux-metal"
profile = "anfitrion-cards"
notes = "6.16.12, clavado al TigerLake/Iris-Xe/AX201."
[[target]]
artifact = "linux-metal-dual"
profile = "anfitrion-cards"
notes = "7.1.2, USB de escritorio dual."
[[target]]
artifact = "linux-gioser"
profile = "anfitrion-cards"
notes = "Derivado de `hammer kernel plan` para gioser (SDD 22, bzImage 11,1 MB)."