Cablea el Stage 0 del bootstrap a la CLI: subcomando `bootstrap stage0 --url --sha256 --version [--seed zig|musl-cross-make]` que abre el store, arma el SeedSpec y llama a hammer_bootstrap::stage0, imprimiendo el b3 del artefacto sellado. - stage0 ahora limpia el staging `.bootstrap-tmp` pase lo que pase (extraído stage0_into): un sha256 erróneo ya no deja el tarball a medio descargar bajo el store. Test reforzado para afirmarlo. - e2e de CLL `bootstrap_cli.rs`: tarball falso vía file:// → sella + imprime hash, idempotente, sha erróneo falla sin sellar. Sin red. Docs SDD 11 §5 y roadmap marcan Stage 0 (lib+CLI) hecho; Stage 1/2 pendientes. Workspace 219 verde. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.9 KiB
SDD 11 — Bootstrap from-scratch (track posterior)
Estado: diseño. Abre el track posterior del SDD 10: bajar de "hammer sobre Alpine" a "hammer sobre sí mismo". No bloquea ninguna Fase 0–6 (ya cerradas); reusa el laboratorio de build (SDD 02) sin retrabajo.
1. El problema: el cordón umbilical
Hoy el userland que hammer muta es el de Alpine (musl + busybox ya cocidos). Eso fue
deliberado (ADR 0002): validar la capa AI-nativa antes de gastar
meses en un bootstrap. Validada esa capa, el track posterior responde una sola pregunta:
¿Puede
hammercompilar el sistema completo que ejecuta, sin pedirle nada al host más que un toolchain semilla fijado por hash?
Si la respuesta es sí y el resultado es bit-reproducible, hammer deja de heredar la confianza de Alpine y la deriva de su propio log de transparencia.
2. Principios (heredados, no nuevos)
- Todo es una receta sellada. Cada pieza del bootstrap es un
Recipe(SDD 02 §1) cuyo artefacto se direcciona por hash en elStore. El bootstrap es un subgrafo del grafo de dependencias que ya construye el lab, no un mecanismo aparte. - Nada del host entra al hash. El único insumo externo es el toolchain semilla, y entra como un artefacto importado con su sha256 fijado (ADR 0006), igual que un tarball upstream. El host aporta un kernel para correr procesos y nada más.
zig cces la semilla primaria (ADR 0003): un binario hermético = compilador C/C++ + musl + headers, cross al target. Escotillamusl-cross-make(gcc+musl) detrás de la misma interfaz de receta (compiler = "gcc") para los paquetes con gcc-ismos.
3. Las tres etapas
host (sólo kernel + seed pinned)
│
┌─────────────▼─────────────┐
│ Stage 0 — toolchain semilla│ zig (o musl-cross-make) ingerido al store,
│ sellado por hash │ fijado por sha256. Cross → x86_64-linux-musl.
└─────────────┬─────────────┘
│ artifact_hash(seed) ← dependencia de build de todo lo demás
┌─────────────▼─────────────┐
│ Stage 1 — userland mínimo │ musl, busybox/toybox, init arje, hammerd,
│ cross-compilado │ cross-compilados CON Stage 0. Sale un rootfs sellado.
└─────────────┬─────────────┘
│ rootfs_hash(stage1)
┌─────────────▼─────────────┐
│ Stage 2 — rebuild nativo │ DENTRO del rootfs Stage 1 (bwrap/chroot/VM),
│ y verificación │ recompila toolchain+userland con las herramientas
└────────────────────────────┘ de Stage 1, NO las del host. Compara hashes.
Stage 0 — Toolchain semilla
Modelamos el toolchain semilla como una fuente fijada, no como un build: un Source de
tipo tarball con sha256 pinned (zig-<ver>-linux-x86_64.tar.xz, o el tarball de
musl-cross-make). hammer-bootstrap lo descarga (fuera del sandbox, como hammer_build::download
ya hace para patch_url/content_url), verifica el sha256 antes de sellarlo, y lo deposita
en el store con un artifact_hash derivado de ese sha256. A partir de ahí el resto del
bootstrap depende del hash del store, no de la ruta del host.
Salida: seed_hash (artefacto del toolchain en el store).
Stage 1 — Userland mínimo
Un conjunto de recetas cuyo [deps].build incluye seed_hash. El conjunto mínimo que arranca
y se administra solo:
| Pieza | Por qué | Compilador |
|---|---|---|
musl |
libc del target | zig cc (estático) |
busybox o toybox |
coreutils + shell | zig cc (estático) |
arje |
init: PID 1 + supervisión (ADR 0007) | toolchain Rust |
hammerd |
diario + bus de agente + watcher, servicio supervisado por arje | toolchain Rust |
Todo se enlaza estático o con rpath controlado (sin depender del loader del host). El resultado
se ensambla en un rootfs (árbol FHS) que a su vez se sella como un artefacto del store
(stage1_hash). El init no se escribe de cero: se adopta arje
(ADR 0007), cuyo PID 1 con supervisión real entrega el
CRASHED real que la Fase 5 dejó diferido — hammerd corre como servicio bajo arje.
El kernel no es el foco de este hito: se importa pinned (como la semilla) para poder correr el rootfs. Construirlo desde fuente es un sub-ítem posterior, ortogonal al auto-alojamiento del userland.
Stage 2 — Rebuild nativo y verificación
El corte del cordón. Dentro del rootfs Stage 1 (primero vía bwrap/chroot, luego en la VM
destino), se reconstruyen Stage 0' y Stage 1' usando sólo las herramientas de Stage 1.
Se compara:
hash(stage1) vs hash(stage1')
- Iguales ⇒ el sistema se compila a sí mismo bit a bit: reproducible y auto-alojado. Hito cumplido.
- Distintos ⇒ hay no-determinismo (timestamps, paths embebidos, orden de enlace). Se caza y se elimina; es exactamente el trabajo que SDD 09 §2 exige.
4. Manifiesto de bootstrap (embrión del log de transparencia)
Cada etapa anota una línea (stage, recipe_hash, artifact_hash, seed_hash, ts) en un
bootstrap.json versionado. Ese manifiesto es la semilla del log de transparencia
(SDD 09 §4): un tercero reproduce el bootstrap y verifica que sus hashes
coinciden con los publicados, sin confiar en el binario del autor.
5. Interfaz
Nuevo crate hammer-bootstrap (orquestador) sobre hammer-build + hammer-core::Store.
No introduce mecanismo nuevo de build: encadena recetas y persiste el manifiesto.
// hammer-bootstrap
pub fn stage0(seed: &SeedSpec, store: &Store) -> Result<ArtifactHash>; // ✅ toolchain semilla
pub fn stage1(seed: &ArtifactHash, store: &Store) -> Result<RootfsHash>; // ☐ userland mínimo
pub fn stage2(stage1: &RootfsHash, store: &Store) -> Result<VerifyReport>; // ☐ rebuild + diff
pub fn all(seed: &SeedSpec, store: &Store) -> Result<BootstrapManifest>; // ☐
SeedSpec { kind, version, url, sha256 } es la identidad pinned de la semilla; seed_hash()
deriva el ArtifactHash de (kind, version, sha256) — no del url ni del host, así que
cualquier espejo del mismo tarball produce el mismo artefacto.
CLI (implementado lo de Stage 0; el resto pendiente):
hammer bootstrap stage0 --url URL --sha256 HEX --version V [--seed zig|musl-cross-make] # ✅
hammer bootstrap stage1 [--target x86_64-linux-musl] # ☐
hammer bootstrap stage2 --verify # ☐
hammer bootstrap --all # las tres + reporte de reproducibilidad # ☐
6. Decisiones abiertas (fijadas en ADR 0008)
- Semilla primaria:
zigvsmusl-cross-make. Inclinación:zigpor hermeticidad (ADR 0003), conmusl-cross-makecomo escotilla por receta. - busybox vs toybox para el userland mínimo (licencia vs cobertura de comandos).
- Particionado/montaje de
/storey/var/lib/hammeren la imagen destino (track posterior del roadmap). - Kernel: importado pinned ahora; from-source después.
7. Hecho cuando
hammer bootstrap --all produce un rootfs que, ejecutado en la VM destino, reconstruye su
propio toolchain y userland con hashes idénticos a los publicados, sin que ninguna
herramienta del host entre al resultado. Ese día Alpine deja de ser una dependencia y pasa a
ser, a lo sumo, una conveniencia de desarrollo.