> 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…
util-linux entra al núcleo: el stage1 pasa de 4 a 5 y la consola la abre agetty
Decisión del usuario. Con util-linux en STAGE1_COMPONENTS, la card `console-getty`
—que vive en la seed del NÚCLEO— ya puede apuntar a un binario que existe en ESE
rootfs, que era exactamente lo que faltaba: util-linux estaba en los 7 perfiles de
targets.toml y eso no lo ponía en el rootfs, porque el producto se ensambla de
estas listas y no del catálogo.
Verificado en los artefactos, no en el código:
stage1-rootfs b3:b9eb52a3
/sbin/agetty 1.292.208 bytes
card console-getty → /sbin/agetty argv=[agetty, -n, -l, /bin/sh, 115200, console]
product-attested-rootfs b3:f3e1ce16
/sbin/agetty presente · card console-getty → /sbin/agetty
takana attest → ✓ 7 binarios críticos casan su BLAKE3, agetty entre ellos
`sbin/agetty` entra también en ATTEST_PATHS, y la regla que eso obedece queda
escrita: lo que un card EJECUTA tiene que estar atestado. Cambiar un `exec` sin
tocar esa lista deja el binario de arranque fuera de la raíz de confianza — un
hueco que no rompe nada y que ninguna prueba ve, porque el producto bootea igual.
El argv no cambia y eso no es casualidad: agetty documenta las dos formas de orden
—`<line> [<baud>]` y `<baud>,... <line>`— y acepta -n (--skip-login) y -l
(--login-program) con la misma semántica que el applet. Comprobado ejecutando el
binario sellado, no leyendo su manual.
⚠ LO QUE ESTO CUESTA, y va escrito en la constante para que no se pierda:
· el of_tree del stage1 cambia ⇒ el baseline del selfhost-verify
(9adefb82/7fa6cb4e) es de cuando el núcleo eran CUATRO y hay que re-verificar;
· el núcleo que la VM reconstruye bit a bit pasa a incluir un paquete C grande;
· la raíz de confianza crece ~119 binarios, de los que el producto usa UNO. Si
algún día molesta, lo que hace falta es agetty, no util-linux entero.
⚠ Y falta la prueba que de verdad decide: arrancar. Un getty no se valida con
pruebas unitarias —las 42 del crate pasaban también con la card rota apuntando a
un binario inexistente—; la prueba es product-boot-test.sh en QEMU, que esta
jaula no tiene.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
diff --git a/crates/takana-bootstrap/src/lib.rs b/crates/takana-bootstrap/src/lib.rs
index d2f65fd5..5e48cf76 100644
--- a/crates/takana-bootstrap/src/lib.rs
+++ b/crates/takana-bootstrap/src/lib.rs
@@ -166,10 +166,26 @@ pub fn seed_build_config(
pub type RootfsHash = ArtifactHash;
/// Los componentes del userland mínimo, en orden de ensamblado: `musl` (libc), `busybox`
-/// (coreutils + sh), `hammerd` (daemon, receta Cargo) y `arje-zero` (el init PID 1, ADR 0007).
-/// arje-zero es PID 1 vía el symlink `/sbin/init`; supervisa a `hammerd` y a la getty por la
-/// seed card ([SDD 12](../../docs/12-init-real.md)) — su muerte es el `CRASHED` real.
-const STAGE1_COMPONENTS: &[&str] = &["musl", "busybox", "hammerd", "arje-zero"];
+/// (coreutils + sh), `util-linux` (la consola), `hammerd` (daemon, receta Cargo) y `arje-zero`
+/// (el init PID 1, ADR 0007). arje-zero es PID 1 vía el symlink `/sbin/init`; supervisa a `hammerd`
+/// y a la getty por la seed card ([SDD 12](../../docs/12-init-real.md)) — su muerte es el `CRASHED`
+/// real.
+///
+/// ⚠ **`util-linux` entró el 2026-09-22 y el núcleo pasó de 4 a 5** (decisión del usuario, frente
+/// de botar busybox). Entra por UNA razón concreta y conviene que se lea antes de añadir nada más
+/// aquí: **la card `console-getty` de abajo vive en esta seed, o sea en el núcleo, y su `exec` tiene
+/// que existir en ESTE rootfs.** Mientras el núcleo tuvo cuatro componentes, el único getty posible
+/// era el applet de busybox: `util-linux` está en los 7 perfiles de `targets.toml` y eso **no lo
+/// pone en el rootfs** — el producto se ensambla de estas listas, no del catálogo
+/// ([[catalogo-no-es-imagen]]). Apuntar la card a `/sbin/agetty` sin esto deja el arranque MUDO,
+/// con `supervision: Restart` reintentando un binario inexistente para siempre.
+///
+/// ⚠ **Lo que esto cuesta, dicho entero:** el `of_tree` del stage1 cambia, así que el baseline del
+/// selfhost-verify (`9adefb82/7fa6cb4e`) deja de valer y hay que re-verificar; el núcleo que se
+/// reconstruye bit a bit dentro de la VM pasa a incluir un paquete C grande; y la raíz de confianza
+/// crece en ~119 binarios, de los cuales el producto usa uno. Ese último punto es el que hay que
+/// revisar si algún día molesta: lo que hace falta es `agetty`, no util-linux entero.
+const STAGE1_COMPONENTS: &[&str] = &["musl", "busybox", "util-linux", "hammerd", "arje-zero"];
/// La **seed card** que arje-zero lee en `/ente/seed.card.json` al boot ([SDD 12 §4](../../docs/12-init-real.md)):
/// un `Card` `Virtual` cuyos `genesis` son `hammerd` (servicio `Native` con `Restart` — el
@@ -177,6 +193,14 @@ const STAGE1_COMPONENTS: &[&str] = &["musl", "busybox", "hammerd", "arje-zero"];
/// (`seeds/arje-qemu.card.json`) para que `Card::validate()` la acepte; los ULID son fijos para que
/// el `RootfsHash` sea reproducible. Template autocontenido (opción A del SDD): takana no depende de
/// `card-core` como librería; el pin a arje-zero + el boot en VM cubren el drift de schema.
+/// ⚠ **La consola la abre `agetty` de util-linux, no el applet `getty`** (2026-09-22). El argv es
+/// EL MISMO y eso no es casualidad: agetty documenta las dos formas —`agetty [opts] <line>
+/// [<baud>]` y `agetty [opts] <baud>,... <line>`— y acepta `-n` (--skip-login) y `-l`
+/// (--login-program) con la misma semántica que el applet. Verificado ejecutando el binario
+/// sellado, no leyendo su manual.
+///
+/// ⚠ Este literal es **JSON**: los comentarios van ACÁ, no dentro. Meter `//` en la cadena la deja
+/// inválida y caen siete pruebas de golpe, ninguna con un mensaje que hable de comentarios.
const STAGE1_SEED_CARD: &str = r#"{
"schema_version": 1,
"id": "01HQAR53D4M2NBV8KZTYXFQA00",
@@ -237,8 +261,8 @@ const STAGE1_SEED_CARD: &str = r#"{
"cpu_affinity": null
},
"payload": { "Native": {
- "exec": "/bin/busybox",
- "argv": ["getty", "-n", "-l", "/bin/sh", "115200", "console"],
+ "exec": "/sbin/agetty",
+ "argv": ["agetty", "-n", "-l", "/bin/sh", "115200", "console"],
"envp": []
}},
"supervision": { "Restart": { "initial": 200, "max": 10000 } },
@@ -389,20 +413,22 @@ fn assemble_rootfs(
// ── Capa de servicios: el PRODUCTO (Separación Mecanismo/Política) ──────────────────────────────
//
// `STAGE1_COMPONENTS` + `STAGE1_SEED_CARD` son el **mecanismo base**: el núcleo atómico que el
-// selfhost-verify reconstruye bit a bit (`of_tree` 9adefb82/7fa6cb4e). Inmutable y blindado. Los
+// selfhost-verify reconstruye bit a bit. ⚠ El baseline `of_tree` 9adefb82/7fa6cb4e es de cuando
+// el núcleo eran CUATRO componentes: `util-linux` entró el 2026-09-22 y hay que re-verificar. Los
// **servicios** (sshd, …) son **política de producto**: NO entran al núcleo (no contaminan el
// `of_tree` ni obligan a un rebuild pesado de openssl/zlib dentro de la VM). Se inyectan por
// **hidratación tardía** sobre el rootfs base ya sellado, produciendo un `product-rootfs` APARTE.
// Así, meter más servicios mañana no toca jamás el núcleo verificado.
-/// Componentes de servicio inyectados sobre el 4/4 verificado: `netup` (red) + `openssh` (sshd/ssh).
+/// Componentes de servicio inyectados sobre el 5/5 verificado (era 4/4 hasta que
+/// `util-linux` entró al núcleo el 2026-09-22): `netup` (red) + `openssh` (sshd/ssh).
/// Daemons ⇒ llevan card en la seed de producto. Crecer esta lista NO toca `STAGE1_COMPONENTS`.
const SERVICE_COMPONENTS: &[&str] = &["netup", "openssh"];
/// Userland Rust-nativo ADOPTADO (Etapa C): multicall `uutils`/coreutils + `find`/`xargs` + `diff`/`cmp`
/// + `rg`. Se hidrata sobre la base DESPUÉS de busybox, así que sus symlinks en `/usr/bin` ENSOMBRECEN
/// los applets de busybox (con `PATH` `/usr/bin:/bin` gana la tool Rust). Es el "adelgazar busybox" del
-/// producto SIN tocar el núcleo: busybox sigue en el 4/4 (self-host) y queda de fallback para lo aún no
+/// producto SIN tocar el núcleo: busybox sigue en el núcleo (self-host) y queda de fallback para lo aún no
/// reemplazado (`sh`/ash, `tar`, …). NO son daemons ⇒ no llevan card en la seed (sólo ficheros).
const USERLAND_COMPONENTS: &[&str] =
&["uutils", "findutils", "findutils-xargs", "diffutils", "ripgrep"];
@@ -462,7 +488,8 @@ const SSHD_CONFIG: &str = "Port 22\nListenAddress 0.0.0.0\nPermitRootLogin prohi
const ATTEST_PATHS: &[&str] = &[
"usr/bin/arje-zero", // PID 1 — la raíz
"usr/bin/hammerd", // diario + bus
- "bin/busybox", // shell/getty + exec de varios cards
+ "bin/busybox", // shell (`/bin/sh`) + exec del card de sshd
+ "sbin/agetty", // la consola: lo ejecuta el card `console-getty`
"usr/sbin/sshd", // daemon SSH (superficie de red)
"usr/bin/netup", // configurador de red
"usr/bin/coreutils", // userland Rust multicall
@@ -2117,7 +2144,11 @@ mod tests {
assert_eq!(hammerd["payload"]["Native"]["exec"], "/usr/bin/hammerd");
assert!(hammerd["supervision"]["Restart"]["initial"].is_number(), "hammerd es Restart");
let getty = &v["genesis"][1];
- assert_eq!(getty["payload"]["Native"]["exec"], "/bin/busybox");
+ assert_eq!(getty["payload"]["Native"]["exec"], "/sbin/agetty");
+ // El argv es el mismo que tenía con el applet: agetty acepta `<baud> <line>` y las dos
+ // opciones. Si alguien lo cambia, que sea a sabiendas.
+ assert_eq!(getty["payload"]["Native"]["argv"][0], "agetty");
+ assert_eq!(getty["payload"]["Native"]["argv"][3], "/bin/sh");
}
// --- Capa de servicios (producto): composición de seed + hidratación tardía ---
@@ -2460,8 +2491,10 @@ open(os.path.join(os.path.dirname(out),"..","..","bins-seen.txt"),"w").write("\n
let by_label = |l: &str| bins.iter().find(|(x, _)| x == l).map(|(_, p)| p.clone()).unwrap();
assert_eq!(by_label("arje-zero"), staging.join("usr/bin/arje-zero"));
assert_eq!(by_label("hammerd"), staging.join("usr/bin/hammerd"));
- // console-getty y sshd ejecutan ambos /bin/busybox ⇒ misma ruta, labels distintos.
- assert_eq!(by_label("console-getty"), staging.join("bin/busybox"));
+ // ⚠ Desde 2026-09-22 la consola la abre `agetty` y sshd sigue en busybox: dos rutas
+ // distintas donde antes había una. Este test existe para eso — que el derivado siga al
+ // `exec` de cada card y no a una lista escrita a mano.
+ assert_eq!(by_label("console-getty"), staging.join("sbin/agetty"));
assert_eq!(by_label("sshd"), staging.join("bin/busybox"));
}