gioser

sergio/takana

> 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…

TOML 1471Shell 173Rust 105Markdown 943545 Definitions

⎇ 749ea9e7972faaf48337db8d3abc7806e719eac1 docs/11-bootstrap.md Markdown · 259 lines · 15.9 KB History raw

recipes: GNU make 4.4.1 — pieza 1 del toolchain hammer-from-source (variante b) 749ea9e79 · sergio ·

1# SDD 11 — Bootstrap from-scratch (track posterior)
2
3> **Estado:** diseño. Abre el *track posterior* del [SDD 10](10-roadmap.md): bajar de "hammer
4> sobre Alpine" a "hammer sobre sí mismo". No bloquea ninguna Fase 0–6 (ya cerradas); reusa
5> el laboratorio de build ([SDD 02](02-build-lab.md)) sin retrabajo.
6
7## 1. El problema: el cordón umbilical
8
9Hoy el userland que `hammer` muta es el de Alpine (musl + busybox ya cocidos). Eso fue
10deliberado ([ADR 0002](adr/0002-alpine-first.md)): validar la capa AI-nativa antes de gastar
11meses en un bootstrap. Validada esa capa, el track posterior responde una sola pregunta:
12
13> **¿Puede `hammer` compilar el sistema completo que ejecuta, sin pedirle nada al host más
14> que un toolchain semilla fijado por hash?**
15
16Si la respuesta es sí *y* el resultado es bit-reproducible, hammer deja de heredar la
17confianza de Alpine y la deriva de su propio [log de transparencia](09-trust-model.md).
18
19## 2. Principios (heredados, no nuevos)
20
21- **Todo es una receta sellada.** Cada pieza del bootstrap es un `Recipe` ([SDD 02 §1](02-build-lab.md))
22  cuyo artefacto se direcciona por hash en el `Store`. El bootstrap es un *subgrafo* del grafo
23  de dependencias que ya construye el lab, no un mecanismo aparte.
24- **Nada del host entra al hash.** El único insumo externo es el **toolchain semilla**, y entra
25  como un artefacto importado con su sha256 **fijado** ([ADR 0006](adr/0006-pinned-commits.md)),
26  igual que un tarball upstream. El host aporta un kernel para correr procesos y nada más.
27- **`zig cc` es la semilla primaria** ([ADR 0003](adr/0003-zig-cc-builder.md)): un binario
28  hermético = compilador C/C++ + musl + headers, cross al target. Escotilla `musl-cross-make`
29  (gcc+musl) detrás de la misma interfaz de receta (`compiler = "gcc"`) para los paquetes con
30  gcc-ismos.
31
32## 3. Las tres etapas
33
34```
35        host (sólo kernel + seed pinned)
36                 │
37   ┌─────────────▼─────────────┐
38   │ Stage 0 — toolchain semilla│  zig (o musl-cross-make) ingerido al store,
39   │   sellado por hash         │  fijado por sha256. Cross → x86_64-linux-musl.
40   └─────────────┬─────────────┘
41                 │ artifact_hash(seed)  ← dependencia de build de todo lo demás
42   ┌─────────────▼─────────────┐
43   │ Stage 1 — userland mínimo  │  musl, busybox/toybox, init arje, hammerd,
44   │   cross-compilado          │  cross-compilados CON Stage 0. Sale un rootfs sellado.
45   └─────────────┬─────────────┘
46                 │ rootfs_hash(stage1)
47   ┌─────────────▼─────────────┐
48   │ Stage 2 — rebuild nativo   │  DENTRO del rootfs Stage 1 (bwrap/chroot/VM),
49   │   y verificación           │  recompila toolchain+userland con las herramientas
50   └────────────────────────────┘  de Stage 1, NO las del host. Compara hashes.
51```
52
53### Stage 0 — Toolchain semilla
54
55Modelamos el toolchain semilla como una **fuente fijada**, no como un build: un `Source` de
56tipo tarball con `sha256` pinned (`zig-<ver>-linux-x86_64.tar.xz`, o el tarball de
57`musl-cross-make`). `hammer-bootstrap` lo descarga (fuera del sandbox, como `hammer_build::download`
58ya hace para `patch_url`/`content_url`), verifica el sha256 **antes** de sellarlo, y lo deposita
59en el store con un `artifact_hash` derivado de ese sha256. A partir de ahí el resto del
60bootstrap depende del *hash del store*, no de la ruta del host.
61
62Salida: `seed_hash` (artefacto del toolchain en el store).
63
64### Stage 1 — Userland mínimo
65
66Un conjunto de recetas cuyo `[deps].build` incluye `seed_hash`. El conjunto mínimo que arranca
67y se administra solo:
68
69| Pieza | Por qué | Compilador |
70|---|---|---|
71| `musl` | libc del target | zig cc (estático) |
72| `busybox` o `toybox` | coreutils + shell | zig cc (estático) |
73| `arje` | init: PID 1 + supervisión ([ADR 0007](adr/0007-arje-como-init-propio.md)) | toolchain Rust |
74| `hammerd` | diario + bus de agente + watcher, **servicio supervisado por arje** | toolchain Rust |
75
76Todo se enlaza estático o con rpath controlado (sin depender del loader del host). El resultado
77se ensambla en un **rootfs** (árbol FHS) que a su vez se sella como un artefacto del store
78(`stage1_hash`). El init **no se escribe de cero**: se adopta `arje`
79([ADR 0007](adr/0007-arje-como-init-propio.md)), cuyo PID 1 con supervisión real entrega el
80`CRASHED` real que la Fase 5 dejó diferido — `hammerd` corre como servicio bajo arje.
81
82> El **kernel** no es el foco de este hito: se importa pinned (como la semilla) para poder
83> correr el rootfs. Construirlo desde fuente es un sub-ítem posterior, ortogonal al
84> auto-alojamiento del userland.
85
86### Stage 2 — Rebuild nativo y verificación
87
88El corte del cordón. Dentro del rootfs Stage 1 (primero vía `bwrap`/`chroot`, luego en la VM
89destino), se reconstruyen Stage 0' y Stage 1' usando **sólo** las herramientas de Stage 1.
90Se compara:
91
92```
93hash(stage1)   vs   hash(stage1')
94```
95
96- **Iguales** ⇒ el sistema se compila a sí mismo bit a bit: reproducible y auto-alojado. Hito
97  cumplido.
98- **Distintos** ⇒ hay no-determinismo (timestamps, paths embebidos, orden de enlace). Se
99  caza y se elimina; es exactamente el trabajo que [SDD 09](09-trust-model.md) §2 exige.
100
101## 4. Manifiesto de bootstrap (embrión del log de transparencia) ✅
102
103Cada etapa anota una línea `(stage, recipe_hash, artifact_hash, seed_hash, ts)` en un
104`bootstrap.json` versionado (en la raíz del store). Ese manifiesto **es** la semilla del log de
105transparencia ([SDD 09 §4](09-trust-model.md)): un tercero reproduce el bootstrap y verifica que
106sus hashes coinciden con los publicados, sin confiar en el binario del autor.
107
108Implementado en `hammer-bootstrap::manifest` (`BootstrapManifest` / `StageEntry`): append-only e
109**idempotente** (de-dup por `(stage, artifact_hash)`), escritura atómica, y `ts` que **no** entra
110en ningún hash (dos reproducciones del mismo bootstrap difieren a lo sumo en ese campo). Stage 0
111ya anota su línea: `recipe_hash = None` (la semilla se ingiere, no se compila) y
112`artifact_hash == seed_hash` (la semilla es a la vez insumo y producto).
113
114## 5. Interfaz
115
116Nuevo crate `hammer-bootstrap` (orquestador) sobre `hammer-build` + `hammer-core::Store`.
117No introduce mecanismo nuevo de build: encadena recetas y persiste el manifiesto.
118
119```rust
120// hammer-bootstrap
121pub fn stage0(seed: &SeedSpec, store: &Store) -> Result<ArtifactHash>;            // ✅ toolchain semilla
122pub fn stage1(spec: &Stage1Spec, cfg: &BuildConfig, store: &Store)
123    -> Result<RootfsHash>;                                                       // ◑ musl+busybox+hammerd; init arje ☐
124pub fn stage2(stage1: &RootfsHash, store: &Store) -> Result<VerifyReport>;        // ✅ ancla+verifica; rebuild in-rootfs en VM ✓ REPRODUCIBLE
125pub fn all(seed: &SeedSpec, store: &Store) -> Result<BootstrapManifest>;          // ☐
126```
127
128`stage1` cross-compila `musl` + `busybox` + `hammerd` con la semilla (no el zig del host),
129ensambla un rootfs FHS por hardlink y lo sella; el `RootfsHash` se deriva del **contenido** (hashes
130de componentes + init), no del árbol en disco, así que es reproducible. El PID 1 provisional es el
131`init` de busybox vía `/etc/inittab`, que arranca `hammerd` como servicio respawn. `hammerd` es una
132receta Cargo (repo pinned + deps vendoreadas en el fetch para build `--offline`). `arje`
133([ADR 0007](adr/0007-arje-como-init-propio.md)) ya tiene su **receta puente** `recipes/arje-zero.toml`
134(Cargo, fuente = monorepo tawasuyu pinned al commit de la migración del CAS a BLAKE3, `-p arje-zero`):
135prueba que el lab construye el init, pero todavía **no** es PID 1. El paso "init real" —arje-zero
136como PID 1 del rootfs con su seed card, `arje-bus` y hammerd como Card de servicio supervisada, que
137entrega el `CRASHED` real— está diseñado en el [SDD 12](12-init-real.md) y **validado en QEMU**
138(ver [runbook](runbooks/stage1-vm-boot.md)).
139
140`stage2` ancla el **content-hash** del rootfs de Stage 1 (`ArtifactHash::of_tree` — los bytes
141reales, no el hash input-addressed del store) como la referencia a reproducir, y la anota en el
142manifiesto. `verify_against` la compara con un rebuild `stage1'` producido **dentro** del rootfs (en
143la VM, recompilando con sólo las herramientas de Stage 1): iguales ⇒ auto-alojado bit a bit. El
144determinismo necesario lo dan las rutas fijas (`/src`) + `SOURCE_DATE_EPOCH` del sandbox +
145`CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1` (impuesto por el sandbox tras cazar el no-determinismo del
146codegen paralelo de rustc en `arje-zero`). **El rebuild in-rootfs ya se ejecutó end-to-end**: `KVM=1
147MEM=24576 ./scripts/selfhost-verify.sh` reconstruyó los 4/4 dentro del rootfs y su
148`of_tree(stage1') = b3:0039b2b9…` igualó la referencia ⇒ `✓ REPRODUCIBLE: stage1' == stage1`
149(ver [runbook](runbooks/stage1-vm-boot.md) §8).
150
151`SeedSpec { kind, version, url, sha256 }` es la identidad pinned de la semilla; `seed_hash()`
152deriva el `ArtifactHash` de `(kind, version, sha256)` — no del `url` ni del host, así que
153cualquier espejo del mismo tarball produce el mismo artefacto.
154
155CLI (implementado lo de Stage 0; el resto pendiente):
156
157```
158hammer bootstrap stage0 --url URL --sha256 HEX --version V [--seed zig|musl-cross-make]   # ✅
159hammer bootstrap stage1 --seed-hash HASH [--seed zig] [--recipes DIR]                      # ✅ booteado en QEMU
160hammer bootstrap stage2 --rootfs HASH [--verify CONTENT_HASH]                              # ✅ ancla+verifica; rebuild en VM ✓ REPRODUCIBLE
161hammer bootstrap --all                # las tres + reporte de reproducibilidad             # ☐
162```
163
164## 6. Decisiones (fijadas en [ADR 0008](adr/0008-bootstrap-stages.md))
165
166- **Semilla primaria:** `zig` por hermeticidad (ADR 0003), con `musl-cross-make` como escotilla
167  por receta. ✅
168- **coreutils+shell = `busybox`** (no toybox): el Stage 1 es una balsa desechable hacia el
169  userland nativo en Rust/Zig; se prioriza compatibilidad de flags y velocidad, y su GPLv2 no
170  contamina el sistema final. ✅
171- **init `arje` diferido:** Stage 1 con init mínimo provisional; `arje` (y el `CRASHED` real) entran
172  como receta git pinned en un lote posterior. ✅
173- **Layout de la semilla:** ingesta pura en Stage 0 + resolución del toolchain al usarlo
174  (`SeedSpec::toolchain_dir`, localiza `zig` en raíz o hijo versionado). ✅
175- **Particionado/montaje** de `/store` y `/var/lib/hammer` en la imagen destino: track posterior
176  del roadmap. ☐
177- **Kernel:** importado pinned ahora; from-source después. ☐
178
179## 7. Auto-alojamiento: el builder rootfs (camino a Stage 2 pleno)
180
181La verificación de reproducibilidad de Stage 2 está operativa, probada y **ejecutada end-to-end**:
182`of_tree` (content-hash de bytes) + `verify_against`, los 4 componentes reconstruyen
183**bit-idéntico**, y el **rebuild *dentro* del rootfs** ya corrió en la VM dando `✓ REPRODUCIBLE`
184(variante (a) abajo; ver [runbook §8](runbooks/stage1-vm-boot.md)). El obstáculo que hubo que
185resolver para llegar ahí:
186
187> El Stage 1 que booteamos es un **runtime** (musl+busybox+hammerd+arje-zero): **no trae compilador**
188> (ni `zig`, ni `make`/autotools, ni `cargo`/rust, ni `linux-headers`, ni `bwrap`). No puede
189> reconstruir nada. El rebuild in-rootfs exige un **builder rootfs**: Stage 1 + el toolchain adentro.
190
191### 7.1 Qué necesita el builder rootfs
192
193Para correr `hammer bootstrap stage1` **dentro** de sí mismo: el binario `hammer` (estático), las
194recetas, la **semilla** (`zig`, ya un artefacto sellado), `make`+autotools, `cargo`+rust,
195`linux-headers`, y `bwrap` (el lab anida un sandbox ⇒ userns sin privilegios dentro del chroot/VM).
196
197### 7.2 Dos niveles de pureza (fasable)
198
199- **(a) Builder pragmático** — el toolchain entra al rootfs **desde Alpine** (apk), no construido por
200  hammer. Demuestra el **mecanismo** (rebuild in-rootfs + `of_tree(stage1)==of_tree(stage1')`) y
201  cierra la reproducibilidad *end-to-end*, sin ser aún auto-alojamiento puro.
202- **(b) Auto-alojamiento puro** — el toolchain lo **construye hammer desde fuente** (recetas para
203  make/autotools, y el gran tramo: rust/llvm o un rustc bootstrappeado). Es el end-state; el más
204  caro (construir rust desde cero). Se llega **incrementalmente**, reemplazando una a una las piezas
205  Alpine por componentes hammer, con Stage 2 verificando cada paso.
206  - **Pieza 1 — GNU make `4.4.1`** (`recipes/make.toml`): arranque de (b). Build estático musl con
207    `zig cc` (mismo camino que `grep`: tarball release con `configure` → AutoconfReady), sellado en
208    `b3:fbad44ac…` y **reproducible bit-a-bit** (dos builds en stores distintos ⇒ árbol idéntico).
209    Es la herramienta base de toda receta autotools. Pendiente: swap al `/toolchain` del builder en
210    lugar del `make` de Alpine, con Stage 2 reverificando `of_tree(stage1')`.
211
212### 7.3 Enganche con lo ya hecho
213
214`stage2` ya ancla `of_tree(stage1)`; el builder produce `stage1'` y `verify_against` emite el
215veredicto. El determinismo necesario está cubierto (paths fijos `/src`, `SOURCE_DATE_EPOCH`, locks
216deterministas). El sub-ítem era: **ensamblar el builder** (variante de `assemble_rootfs` que hidrata
217el toolchain) y **correr el rebuild anidado** en la VM.
218
219### 7.4 Estado: builder ensamblado + rebuild in-VM ejecutado ✅ ✓ REPRODUCIBLE
220
221El **ensamblado** del builder está implementado (`hammer_bootstrap::builder_rootfs`, variante a):
222
223```
224hammer bootstrap builder --stage1 <H> --seed-hash <H> \
225    [--toolchain .dev-fs/alpine] [--toolchain-tag alpine-3.23.4] \
226    [--hammer-bin <static-musl-hammer>] [--ref-content of_tree(stage1)] \
227    [--work-cache work/] [--out work/builder-rootfs]
228```
229
230Sobre el rootfs de Stage 1 (hidratado como base) monta: `/toolchain` (el rootfs Alpine, el sandbox
231de build), `/store` con la **semilla replicada** (el `hammer` de adentro resuelve zig por hash),
232`/usr/bin/hammer` + `/etc/hammer/recipes`, y el driver **`/usr/bin/rebuild-stage1`** que lee
233`/etc/hammer/rebuild.env` (`SEED_HASH`/`SEED_KIND`/`REF_CONTENT`), apunta `HAMMER_ROOTFS=/toolchain`,
234corre `hammer bootstrap stage1` y compara `stage1'` contra la referencia con `stage2 --verify`.
235
236El builder **no se sella** (su `/toolchain` Alpine no es content-addressed); se ensambla en un
237`out_dir` para empaquetar como initramfs. Su identidad sí es reproducible: un `ArtifactHash` *lógico*
238de sus insumos (stage1 + semilla + recetas + binario + tag del toolchain + el driver), anotado en el
239manifiesto (línea stage 2). Validado contra el store real (Stage 1 `73d7a9be…` + semilla
240`3ce721ec…` ⇒ builder `81dad3d9…`, 1.2 GB con el toolchain). **El paso operacional ya se ejecutó:**
241`scripts/selfhost-verify.sh` empaqueta el builder, lo bootea con KVM y corre `rebuild-stage1`
242no-interactivo; in-VM reconstruyó los 4/4 (arje-zero cu=1 en ~27 min) y `of_tree(stage1') =
243b3:0039b2b9…` igualó la referencia ⇒ `✓ REPRODUCIBLE: stage1' == stage1`. Lo que queda es la
244variante (b), el auto-alojamiento *puro* (toolchain construido por hammer desde fuente, §7.2).
245**Arrancada:** la pieza 1, GNU make `4.4.1` (`recipes/make.toml`), ya se construye desde fuente
246con el lab — estática, reproducible bit-a-bit (§7.2b). Ver [runbook §8](runbooks/stage1-vm-boot.md).
247
248## 8. Hecho cuando
249
250`hammer bootstrap --all` produce un rootfs que, ejecutado en la VM destino, **reconstruye su
251propio toolchain y userland con hashes idénticos a los publicados**, sin que ninguna
252herramienta del host entre al resultado. Ese día Alpine deja de ser una dependencia y pasa a
253ser, a lo sumo, una conveniencia de desarrollo.
254
255**Estado (2026-06-12):** el **mecanismo** está demostrado end-to-end con la variante (a) — el
256rootfs reconstruye su userland in-VM con `of_tree` idéntico (`✓ REPRODUCIBLE`, §7.4). Falta sólo
257la variante (b): que el **toolchain** dentro del builder lo construya hammer desde fuente (no
258provenga de Alpine), reemplazando una a una las piezas con Stage 2 verificando cada paso. Ese es
259el corte pleno del cordón.

11-bootstrap.md

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 hammer compilar 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)

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:

PiezaPor quéCompilador
musllibc del targetzig cc (estático)
busybox o toyboxcoreutils + shellzig cc (estático)
arjeinit: PID 1 + supervisión (ADR 0007)toolchain Rust
hammerddiario + bus de agente + watcher, servicio supervisado por arjetoolchain 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')

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 (en la raíz del store). 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.

Implementado en hammer-bootstrap::manifest (BootstrapManifest / StageEntry): append-only e idempotente (de-dup por (stage, artifact_hash)), escritura atómica, y ts que no entra en ningún hash (dos reproducciones del mismo bootstrap difieren a lo sumo en ese campo). Stage 0 ya anota su línea: recipe_hash = None (la semilla se ingiere, no se compila) y artifact_hash == seed_hash (la semilla es a la vez insumo y producto).

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(spec: &Stage1Spec, cfg: &BuildConfig, store: &Store)
    -> Result<RootfsHash>;                                                       // ◑ musl+busybox+hammerd; init arje ☐
pub fn stage2(stage1: &RootfsHash, store: &Store) -> Result<VerifyReport>;        // ✅ ancla+verifica; rebuild in-rootfs en VM ✓ REPRODUCIBLE
pub fn all(seed: &SeedSpec, store: &Store) -> Result<BootstrapManifest>;          // ☐

stage1 cross-compila musl + busybox + hammerd con la semilla (no el zig del host), ensambla un rootfs FHS por hardlink y lo sella; el RootfsHash se deriva del contenido (hashes de componentes + init), no del árbol en disco, así que es reproducible. El PID 1 provisional es el init de busybox vía /etc/inittab, que arranca hammerd como servicio respawn. hammerd es una receta Cargo (repo pinned + deps vendoreadas en el fetch para build --offline). arje (ADR 0007) ya tiene su receta puente recipes/arje-zero.toml (Cargo, fuente = monorepo tawasuyu pinned al commit de la migración del CAS a BLAKE3, -p arje-zero): prueba que el lab construye el init, pero todavía no es PID 1. El paso "init real" —arje-zero como PID 1 del rootfs con su seed card, arje-bus y hammerd como Card de servicio supervisada, que entrega el CRASHED real— está diseñado en el SDD 12 y validado en QEMU (ver runbook).

stage2 ancla el content-hash del rootfs de Stage 1 (ArtifactHash::of_tree — los bytes reales, no el hash input-addressed del store) como la referencia a reproducir, y la anota en el manifiesto. verify_against la compara con un rebuild stage1' producido dentro del rootfs (en la VM, recompilando con sólo las herramientas de Stage 1): iguales ⇒ auto-alojado bit a bit. El determinismo necesario lo dan las rutas fijas (/src) + SOURCE_DATE_EPOCH del sandbox + CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 (impuesto por el sandbox tras cazar el no-determinismo del codegen paralelo de rustc en arje-zero). El rebuild in-rootfs ya se ejecutó end-to-end: KVM=1 MEM=24576 ./scripts/selfhost-verify.sh reconstruyó los 4/4 dentro del rootfs y su of_tree(stage1') = b3:0039b2b9… igualó la referencia ⇒ ✓ REPRODUCIBLE: stage1' == stage1 (ver runbook §8).

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 --seed-hash HASH [--seed zig] [--recipes DIR]                      # ✅ booteado en QEMU
hammer bootstrap stage2 --rootfs HASH [--verify CONTENT_HASH]                              # ✅ ancla+verifica; rebuild en VM ✓ REPRODUCIBLE
hammer bootstrap --all                # las tres + reporte de reproducibilidad             # ☐

6. Decisiones (fijadas en ADR 0008)

7. Auto-alojamiento: el builder rootfs (camino a Stage 2 pleno)

La verificación de reproducibilidad de Stage 2 está operativa, probada y ejecutada end-to-end: of_tree (content-hash de bytes) + verify_against, los 4 componentes reconstruyen bit-idéntico, y el rebuild dentro del rootfs ya corrió en la VM dando ✓ REPRODUCIBLE (variante (a) abajo; ver runbook §8). El obstáculo que hubo que resolver para llegar ahí:

El Stage 1 que booteamos es un runtime (musl+busybox+hammerd+arje-zero): no trae compilador (ni zig, ni make/autotools, ni cargo/rust, ni linux-headers, ni bwrap). No puede reconstruir nada. El rebuild in-rootfs exige un builder rootfs: Stage 1 + el toolchain adentro.

7.1 Qué necesita el builder rootfs

Para correr hammer bootstrap stage1 dentro de sí mismo: el binario hammer (estático), las recetas, la semilla (zig, ya un artefacto sellado), make+autotools, cargo+rust, linux-headers, y bwrap (el lab anida un sandbox ⇒ userns sin privilegios dentro del chroot/VM).

7.2 Dos niveles de pureza (fasable)

7.3 Enganche con lo ya hecho

stage2 ya ancla of_tree(stage1); el builder produce stage1' y verify_against emite el veredicto. El determinismo necesario está cubierto (paths fijos /src, SOURCE_DATE_EPOCH, locks deterministas). El sub-ítem era: ensamblar el builder (variante de assemble_rootfs que hidrata el toolchain) y correr el rebuild anidado en la VM.

7.4 Estado: builder ensamblado + rebuild in-VM ejecutado ✅ ✓ REPRODUCIBLE

El ensamblado del builder está implementado (hammer_bootstrap::builder_rootfs, variante a):

hammer bootstrap builder --stage1 <H> --seed-hash <H> \
    [--toolchain .dev-fs/alpine] [--toolchain-tag alpine-3.23.4] \
    [--hammer-bin <static-musl-hammer>] [--ref-content of_tree(stage1)] \
    [--work-cache work/] [--out work/builder-rootfs]

Sobre el rootfs de Stage 1 (hidratado como base) monta: /toolchain (el rootfs Alpine, el sandbox de build), /store con la semilla replicada (el hammer de adentro resuelve zig por hash), /usr/bin/hammer + /etc/hammer/recipes, y el driver /usr/bin/rebuild-stage1 que lee /etc/hammer/rebuild.env (SEED_HASH/SEED_KIND/REF_CONTENT), apunta HAMMER_ROOTFS=/toolchain, corre hammer bootstrap stage1 y compara stage1' contra la referencia con stage2 --verify.

El builder no se sella (su /toolchain Alpine no es content-addressed); se ensambla en un out_dir para empaquetar como initramfs. Su identidad sí es reproducible: un ArtifactHash lógico de sus insumos (stage1 + semilla + recetas + binario + tag del toolchain + el driver), anotado en el manifiesto (línea stage 2). Validado contra el store real (Stage 1 73d7a9be… + semilla 3ce721ec… ⇒ builder 81dad3d9…, 1.2 GB con el toolchain). El paso operacional ya se ejecutó: scripts/selfhost-verify.sh empaqueta el builder, lo bootea con KVM y corre rebuild-stage1 no-interactivo; in-VM reconstruyó los 4/4 (arje-zero cu=1 en ~27 min) y of_tree(stage1') = b3:0039b2b9… igualó la referencia ⇒ ✓ REPRODUCIBLE: stage1' == stage1. Lo que queda es la variante (b), el auto-alojamiento puro (toolchain construido por hammer desde fuente, §7.2). Arrancada: la pieza 1, GNU make 4.4.1 (recipes/make.toml), ya se construye desde fuente con el lab — estática, reproducible bit-a-bit (§7.2b). Ver runbook §8.

8. 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.

Estado (2026-06-12): el mecanismo está demostrado end-to-end con la variante (a) — el rootfs reconstruye su userland in-VM con of_tree idéntico (✓ REPRODUCIBLE, §7.4). Falta sólo la variante (b): que el toolchain dentro del builder lo construya hammer desde fuente (no provenga de Alpine), reemplazando una a una las piezas con Stage 2 verificando cada paso. Ese es el corte pleno del cordón.