# 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-`), 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)."