bootstrap: arje-zero como PID 1 de Stage 1 (I2 del SDD 12)
Reemplaza el init provisional de busybox por arje-zero como PID 1 — el paso "init real" que entrega el CRASHED real (Fase 5 diferida, ADR 0007). - STAGE1_COMPONENTS += arje-zero (la receta puente ya lo construía) - assemble_rootfs: genera /ente/seed.card.json (template autocontenido), crea /sbin/init -> /usr/bin/arje-zero, y los mount points que arje monta (incl. /ente, /var/lib/hammer, /sys/fs/cgroup, /dev/pts, /dev/shm). Se retira el inittab. - la seed declara hammerd como Payload::Native Restart (su on_death = el CRASHED real) + console-getty supervisada; ULIDs fijos ⇒ RootfsHash reproducible (v2) - la seed se VERIFICÓ contra el tipo real card_core::Card (from_json + validate pasan), no sólo como JSON — evidencia, no aserción - +2 tests (ensamblado con arje init + seed válida); 21 verdes en el crate Falta para cerrar Stage 1: bus único (B.2) y atestación (A1/A2). Boot real en VM. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4a1d30db92
commit
14e10d9292
@@ -165,26 +165,91 @@ pub fn seed_build_config(
|
||||
/// mismos artefactos producen el mismo hash aunque difieran en inodes o timestamps.
|
||||
pub type RootfsHash = ArtifactHash;
|
||||
|
||||
/// Los componentes del userland mínimo, en orden de ensamblado. `musl` primero (libc), `busybox`
|
||||
/// (coreutils + sh + init), `hammerd` (daemon, receta Cargo). El init `arje` (ADR 0007) reemplaza
|
||||
/// al de busybox en un lote posterior; hasta entonces el PID 1 provisional es el `init` de busybox
|
||||
/// que arranca a `hammerd` por `/etc/inittab`.
|
||||
const STAGE1_COMPONENTS: &[&str] = &["musl", "busybox", "hammerd"];
|
||||
/// 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"];
|
||||
|
||||
/// `inittab` del init provisional (busybox). Monta los pseudo-FS, arranca `hammerd` como servicio
|
||||
/// respawn y abre una shell. Cuando entre `arje` como PID 1 (ADR 0007) este archivo se reemplaza
|
||||
/// por su supervisión real (que entrega el `CRASHED` que la Fase 5 dejó diferido).
|
||||
const PROVISIONAL_INITTAB: &str = "\
|
||||
# Stage 1 — init provisional (busybox). Reemplazado por arje (ADR 0007) más adelante.
|
||||
::sysinit:/bin/mount -t proc proc /proc
|
||||
::sysinit:/bin/mount -t sysfs sysfs /sys
|
||||
::sysinit:/bin/mount -t devtmpfs dev /dev
|
||||
::sysinit:/bin/mount -o remount,rw /
|
||||
::respawn:/usr/bin/hammerd
|
||||
::respawn:/bin/sh
|
||||
::ctrlaltdel:/bin/umount -a -r
|
||||
::shutdown:/bin/umount -a -r
|
||||
";
|
||||
/// 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
|
||||
/// `CRASHED` real) y una `console-getty` supervisada. La forma replica una seed real de arje
|
||||
/// (`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): hammer no depende de
|
||||
/// `card-core` como librería; el pin a arje-zero + el boot en VM cubren el drift de schema.
|
||||
const STAGE1_SEED_CARD: &str = r#"{
|
||||
"schema_version": 1,
|
||||
"id": "01HQAR53D4M2NBV8KZTYXFQA00",
|
||||
"lineage": null,
|
||||
"label": "hammer-stage1",
|
||||
"provides": ["Spawn", "Journal"],
|
||||
"requires": [],
|
||||
"permissions": { "networking": "full", "filesystem": "read-write", "ipc": { "allow": [] }, "processes": true },
|
||||
"soma": {
|
||||
"namespaces": { "mount": false, "pid": false, "net": false, "uts": false, "ipc": false, "user": false, "cgroup": false },
|
||||
"rlimits": { "mem_bytes": null, "nproc": null, "nofile": null },
|
||||
"cgroup": { "path": "arje.slice/hammer", "cpu_weight": null, "io_weight": null },
|
||||
"cpu_affinity": null
|
||||
},
|
||||
"payload": "Virtual",
|
||||
"supervision": "OneShot",
|
||||
"lifecycle": "daemon",
|
||||
"priority": "normal",
|
||||
"flow": { "input": [], "output": [] },
|
||||
"genesis": [
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "01HQAR53D4M2NBV8KZTYXFQA01",
|
||||
"lineage": null,
|
||||
"label": "hammerd",
|
||||
"provides": [],
|
||||
"requires": [],
|
||||
"permissions": { "networking": "full", "filesystem": "read-write", "ipc": { "allow": [] }, "processes": true },
|
||||
"soma": {
|
||||
"namespaces": { "mount": false, "pid": false, "net": false, "uts": false, "ipc": false, "user": false, "cgroup": false },
|
||||
"rlimits": { "mem_bytes": null, "nproc": null, "nofile": null },
|
||||
"cgroup": { "path": "arje.slice/hammerd", "cpu_weight": null, "io_weight": null },
|
||||
"cpu_affinity": null
|
||||
},
|
||||
"payload": { "Native": {
|
||||
"exec": "/usr/bin/hammerd",
|
||||
"argv": ["--store", "/store", "--journal", "/var/lib/hammer/journal"],
|
||||
"envp": [["RUST_LOG", "info"], ["ENTE_BUS_SOCK", "/run/ente-bus.sock"]]
|
||||
}},
|
||||
"supervision": { "Restart": { "initial": 200, "max": 10000 } },
|
||||
"lifecycle": "daemon",
|
||||
"priority": "normal",
|
||||
"flow": { "input": [], "output": [] },
|
||||
"genesis": []
|
||||
},
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "01HQAR53D4M2NBV8KZTYXFQA02",
|
||||
"lineage": null,
|
||||
"label": "console-getty",
|
||||
"provides": [],
|
||||
"requires": [],
|
||||
"permissions": { "networking": "none", "filesystem": "read-write", "ipc": { "allow": [] }, "processes": true },
|
||||
"soma": {
|
||||
"namespaces": { "mount": false, "pid": false, "net": false, "uts": false, "ipc": false, "user": false, "cgroup": false },
|
||||
"rlimits": { "mem_bytes": null, "nproc": null, "nofile": null },
|
||||
"cgroup": { "path": "arje.slice/getty", "cpu_weight": null, "io_weight": null },
|
||||
"cpu_affinity": null
|
||||
},
|
||||
"payload": { "Native": {
|
||||
"exec": "/bin/busybox",
|
||||
"argv": ["getty", "-n", "-l", "/bin/sh", "115200", "console"],
|
||||
"envp": []
|
||||
}},
|
||||
"supervision": { "Restart": { "initial": 200, "max": 10000 } },
|
||||
"lifecycle": "daemon",
|
||||
"priority": "normal",
|
||||
"flow": { "input": [], "output": [] },
|
||||
"genesis": []
|
||||
}
|
||||
]
|
||||
}
|
||||
"#;
|
||||
|
||||
/// Qué construir para Stage 1: la semilla ya sellada (por `(seed_hash, seed_kind)`) y el directorio
|
||||
/// de recetas donde viven `musl.toml` / `busybox.toml`.
|
||||
@@ -255,15 +320,16 @@ pub fn stage1(
|
||||
Ok(rootfs_hash)
|
||||
}
|
||||
|
||||
/// Hash de contenido del rootfs: tag de versión + `(name, hash)` de cada componente en orden + el
|
||||
/// init provisional. Cambiar un componente, el orden o el inittab re-hashea el rootfs.
|
||||
/// Hash de contenido del rootfs: tag de versión + `(name, hash)` de cada componente en orden + la
|
||||
/// seed card del init. Cambiar un componente, el orden o la seed re-hashea el rootfs. El tag `v2`
|
||||
/// marca el corte respecto al rootfs con init provisional (`v1`).
|
||||
fn rootfs_hash(components: &[(String, ArtifactHash)]) -> RootfsHash {
|
||||
let mut inputs: Vec<Vec<u8>> = vec![b"hammer-stage1-rootfs-v1".to_vec()];
|
||||
let mut inputs: Vec<Vec<u8>> = vec![b"hammer-stage1-rootfs-v2".to_vec()];
|
||||
for (name, h) in components {
|
||||
inputs.push(name.as_bytes().to_vec());
|
||||
inputs.push(h.as_str().as_bytes().to_vec());
|
||||
}
|
||||
inputs.push(PROVISIONAL_INITTAB.as_bytes().to_vec());
|
||||
inputs.push(STAGE1_SEED_CARD.as_bytes().to_vec());
|
||||
let refs: Vec<&[u8]> = inputs.iter().map(|v| v.as_slice()).collect();
|
||||
ArtifactHash::of_inputs(&refs)
|
||||
}
|
||||
@@ -282,14 +348,19 @@ fn assemble_and_seal(
|
||||
}
|
||||
|
||||
/// Ensambla el árbol FHS del rootfs en `staging`: esqueleto de directorios, hidratación de cada
|
||||
/// componente sellado (hardlinks) y el init provisional. Es la pieza testeable sin build real.
|
||||
/// componente sellado (hardlinks), la seed card de arje y `/sbin/init`→arje-zero. Es la pieza
|
||||
/// testeable sin build real ([SDD 12](../../docs/12-init-real.md)).
|
||||
fn assemble_rootfs(
|
||||
store: &Store,
|
||||
components: &[(String, ArtifactHash)],
|
||||
staging: &Path,
|
||||
) -> Result<()> {
|
||||
// Esqueleto FHS mínimo. Los pseudo-FS quedan como puntos de montaje vacíos.
|
||||
for d in ["proc", "sys", "dev", "tmp", "run", "etc", "root", "var", "usr/bin", "bin", "sbin"] {
|
||||
// Esqueleto FHS. arje-zero monta él mismo los pseudo-FS (arje-kernel), así que sólo deben
|
||||
// existir como puntos de montaje vacíos; `/ente` lleva la seed, `/var/lib/hammer` el diario.
|
||||
for d in [
|
||||
"proc", "sys", "sys/fs/cgroup", "dev", "dev/pts", "dev/shm", "run", "tmp", "etc", "root",
|
||||
"var", "var/lib/hammer", "ente", "usr/bin", "bin", "sbin",
|
||||
] {
|
||||
std::fs::create_dir_all(staging.join(d))?;
|
||||
}
|
||||
|
||||
@@ -304,8 +375,14 @@ fn assemble_rootfs(
|
||||
// componente lo escribió último.
|
||||
let _ = std::fs::remove_dir_all(staging.join(".hammer"));
|
||||
|
||||
// Init provisional (busybox lo lee de /etc/inittab).
|
||||
std::fs::write(staging.join("etc/inittab"), PROVISIONAL_INITTAB)?;
|
||||
// La seed card que arje-zero lee al boot.
|
||||
std::fs::write(staging.join("ente/seed.card.json"), STAGE1_SEED_CARD)?;
|
||||
|
||||
// arje-zero es PID 1 vía `/sbin/init`. Symlink absoluto: el kernel lo sigue sea cual sea el
|
||||
// cmdline, sin exigir `init=`. Idempotente: si ya existe (re-ensamblado), lo reemplazamos.
|
||||
let init_link = staging.join("sbin/init");
|
||||
let _ = std::fs::remove_file(&init_link);
|
||||
std::os::unix::fs::symlink("/usr/bin/arje-zero", &init_link)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -642,7 +719,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assemble_rootfs_hydrates_and_writes_provisional_init() {
|
||||
fn assemble_rootfs_hydrates_and_installs_arje_init() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let store = Store::open(tmp.path().join("store")).unwrap();
|
||||
|
||||
@@ -658,24 +735,55 @@ mod tests {
|
||||
std::fs::create_dir_all(w.join(".hammer")).unwrap();
|
||||
std::fs::write(w.join(".hammer/recipe.toml"), b"name = 'busybox'\n").unwrap();
|
||||
});
|
||||
let harje = seal_component(&store, "arje-zero", "cc33", |w| {
|
||||
std::fs::create_dir_all(w.join("usr/bin")).unwrap();
|
||||
std::fs::write(w.join("usr/bin/arje-zero"), b"\x7fELFfake").unwrap();
|
||||
});
|
||||
|
||||
let staging = store.root().join("staging");
|
||||
std::fs::create_dir_all(&staging).unwrap();
|
||||
let components = [("musl".to_string(), hmusl), ("busybox".to_string(), hbb)];
|
||||
let components = [
|
||||
("musl".to_string(), hmusl),
|
||||
("busybox".to_string(), hbb),
|
||||
("arje-zero".to_string(), harje),
|
||||
];
|
||||
assemble_rootfs(&store, &components, &staging).unwrap();
|
||||
|
||||
assert!(staging.join("usr/lib/libc.a").is_file(), "musl hidratado");
|
||||
assert!(staging.join("bin/busybox").is_file(), "busybox hidratado");
|
||||
assert!(staging.join("usr/bin/arje-zero").is_file(), "arje-zero hidratado");
|
||||
// arje-zero es PID 1 vía /sbin/init.
|
||||
assert_eq!(
|
||||
std::fs::read_link(staging.join("bin/sh")).unwrap(),
|
||||
std::path::PathBuf::from("busybox"),
|
||||
"el symlink de applet se replica literal"
|
||||
std::fs::read_link(staging.join("sbin/init")).unwrap(),
|
||||
std::path::PathBuf::from("/usr/bin/arje-zero"),
|
||||
"/sbin/init apunta a arje-zero"
|
||||
);
|
||||
assert!(staging.join("proc").is_dir() && staging.join("sbin").is_dir(), "esqueleto FHS");
|
||||
let inittab = std::fs::read_to_string(staging.join("etc/inittab")).unwrap();
|
||||
assert!(inittab.contains("/bin/sh"), "init provisional abre una shell");
|
||||
assert!(inittab.contains("mount -t proc"), "monta los pseudo-FS");
|
||||
// La seed card está en su lugar y es JSON válido con los genesis esperados.
|
||||
let seed = std::fs::read_to_string(staging.join("ente/seed.card.json")).unwrap();
|
||||
let v: serde_json::Value = serde_json::from_str(&seed).unwrap();
|
||||
assert_eq!(v["label"], "hammer-stage1");
|
||||
let labels: Vec<&str> = v["genesis"].as_array().unwrap()
|
||||
.iter().map(|g| g["label"].as_str().unwrap()).collect();
|
||||
assert_eq!(labels, vec!["hammerd", "console-getty"]);
|
||||
// Puntos de montaje que arje monta solos, presentes como dirs vacíos.
|
||||
assert!(staging.join("sys/fs/cgroup").is_dir() && staging.join("dev/pts").is_dir());
|
||||
assert!(!staging.join(".hammer").exists(), "el sidecar no debe quedar en el rootfs");
|
||||
// Ya no hay init provisional de busybox.
|
||||
assert!(!staging.join("etc/inittab").exists(), "el inittab provisional fue retirado");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stage1_seed_card_is_valid_json_with_native_hammerd() {
|
||||
// La seed embebida debe ser JSON válido y declarar hammerd como servicio Native Restart
|
||||
// (el CRASHED real) — un template roto rompería el boot de arje-zero.
|
||||
let v: serde_json::Value = serde_json::from_str(STAGE1_SEED_CARD).unwrap();
|
||||
assert_eq!(v["payload"], "Virtual", "la semilla es Virtual");
|
||||
let hammerd = &v["genesis"][0];
|
||||
assert_eq!(hammerd["label"], "hammerd");
|
||||
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");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+7
-5
@@ -185,11 +185,13 @@ Diseño completo en [SDD 11 — Bootstrap from-scratch](11-bootstrap.md). Resume
|
||||
idempotente. Cubierto por 4 unit + 1 e2e de CLI (offline, `file://`).
|
||||
- **Stage 1** ◑ — userland mínimo cross-compilado. Hecho: recetas pinned `musl` 1.2.5 +
|
||||
`busybox` 1.36.1 (estáticas, zig cc) + `hammerd` (receta Cargo: repo pinned + deps
|
||||
vendoreadas en el fetch para build `--offline`); `hammer bootstrap stage1` las construye con
|
||||
la semilla, ensambla el rootfs FHS (hardlink) y lo sella con un `RootfsHash` de contenido +
|
||||
init provisional (busybox/inittab que arranca `hammerd`). Falta sólo sustituir el init por
|
||||
`arje`. El cross-compile real se valida en la VM (los e2e se saltan sin `.dev-fs`); el
|
||||
manifiesto anota la línea de Stage 1.
|
||||
vendoreadas en el fetch para build `--offline`) + `arje-zero` (init PID 1, receta Cargo a
|
||||
tawasuyu); `hammer bootstrap stage1` los construye con la semilla, ensambla el rootfs FHS
|
||||
(hardlink) y lo sella con un `RootfsHash` de contenido. El init es **arje-zero como PID 1**
|
||||
(`/sbin/init`→arje-zero) con su seed card `/ente/seed.card.json` (hammerd + getty supervisados
|
||||
⇒ el `CRASHED` real) — diseño en [SDD 12](12-init-real.md), seed verificada contra
|
||||
`card_core::Card`. Falta: bus único (B.2) y atestación (A1/A2). El cross-compile y el boot
|
||||
reales se validan en la VM; el manifiesto anota la línea de Stage 1.
|
||||
- **Stage 2** ☐ — rebuild nativo dentro del rootfs y diff de hashes ⇒ auto-alojamiento
|
||||
bit-reproducible.
|
||||
- Reemplazar el init de Alpine por **tu init** (bus por pipes nativo) — habilita el `CRASHED`
|
||||
|
||||
@@ -134,11 +134,13 @@ descarta: acoplar los grafos de build de los dos repos es justo lo que C.3 evita
|
||||
## 6. Fases de implementación (riesgo creciente)
|
||||
|
||||
- **I1 — arje-zero construible.** ✅ (`recipes/arje-zero.toml`, receta puente).
|
||||
- **I2 — arje es PID 1 de Stage 1.** `STAGE1_COMPONENTS += arje-zero`; `assemble_rootfs` genera
|
||||
`/ente/seed.card.json` (§4, opción A), provee los mount points + `/sbin/init`→arje-zero, y retira
|
||||
- **I2 — arje es PID 1 de Stage 1.** ✅ `STAGE1_COMPONENTS += arje-zero`; `assemble_rootfs` genera
|
||||
`/ente/seed.card.json` (§4, opción A), provee los mount points (incl. `/ente`, `/var/lib/hammer`,
|
||||
`/sys/fs/cgroup`, `/dev/pts`, `/dev/shm`) + `/sbin/init`→`/usr/bin/arje-zero`, y ya **no** escribe
|
||||
el `/etc/inittab` provisional. arje supervisa hammerd ⇒ **CRASHED real manejado por el init**.
|
||||
Validación: boot en VM (no testeable sin display/kernel; los unit tests cubren la generación de la
|
||||
seed y el ensamblado, como el resto de Stage 1).
|
||||
La seed embebida se **verificó contra el tipo real** `card_core::Card` (`from_json` + `validate()`
|
||||
pasan), no sólo como JSON válido. El boot completo se valida en VM (no testeable sin kernel); los
|
||||
unit tests cubren la seed y el ensamblado, como el resto de Stage 1.
|
||||
- **I3 — bus único (plan B.2).** hammerd `announce` en `arje-bus` (cambio en hammerd) y **expone el
|
||||
`CRASHED` a la capa de IA** de hammer (`agent.sock` emite `{"t":"crashed",…}`). El `agent.sock`
|
||||
(JSON, API de IA) queda **encima**; `arje-bus` (postcard) es el plano de control del init.
|
||||
|
||||
Reference in New Issue
Block a user