Commit Graph
69 Commits
Author SHA1 Message Date
sergioandClaude Opus 4.8 8ba571aebc scripts/product-image: imagen de disco auto-booteable del producto (Etapa E)
Cierra el pipeline de release engineering para el product-rootfs: del
artefacto sellado por `hammer bootstrap product` a un disco GRUB-booteable
que arranca solo en QEMU (`-drive file=img`, SIN -kernel) y sirve SSH.

- product-image.sh: hidrata el product-rootfs sellado a un dir escribible,
  provisiona authorized_keys de prueba y delega el armado del disco a
  install-image.sh (GRUB BIOS, particiones dedicadas vda2=/ vda3=/store
  vda4=/var/lib/hammer). A diferencia de install-image por defecto (que
  empaqueta el BUILDER con todo el toolchain), la root es el product-rootfs
  LEAN ⇒ imagen de PRODUCTO. Con BOOT=1 auto-bootea + handshake SSH (slirp
  hostfwd) como validación.
  GOTCHA: BOOT=1 (para el boot propio) se hereda por entorno a
  install-image.sh, que haría su PROPIO `exec qemu` foreground y bloquearía
  ⇒ se pasa BOOT=0 explícito al delegar.
- hammer-bootstrap: el producto crea los mountpoints /store y /var/lib/hammer
  (disk-ready) para el wrapper /sbin/init de la imagen. Test actualizado.

Validado in-VM: la imagen auto-bootea por GRUB (sin -kernel) → arje PID1 →
hammerd+getty+sshd; SSH OK, ls = "uutils coreutils 0.9.0" (userland Rust del
disco), /dev/vda3→/store y /dev/vda4→/var/lib/hammer montadas. 36 tests verde.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:09:09 -04:00
sergioandClaude Opus 4.8 20936e1f96 hammer-bootstrap: userland Rust en el producto + adelgazar busybox (Etapa C)
Extiende la capa de producto con el userland Rust-nativo ADOPTADO, fuera del
núcleo blindado (sigue sin tocar STAGE1_COMPONENTS ni el of_tree).

- USERLAND_COMPONENTS = [uutils, findutils, findutils-xargs, diffutils,
  ripgrep]: se hidratan sobre el 4/4 DESPUÉS de busybox ⇒ sus symlinks en
  /usr/bin ensombrecen los applets busybox.
- ADELGAZAR BUSYBOX (determinista, sin depender del PATH del shell): por cada
  nombre que el userland Rust provee en /usr/bin, se RETIRA el symlink
  homónimo de busybox en /bin, /sbin, /usr/sbin (busybox suele dejar `ls` en
  /bin, fuera del /usr/bin ya ensombrecido). La tool Rust queda ÚNICA en PATH.
  El binario busybox y lo no reemplazado (sh/ash, tar, mount) se preservan.
- product_rootfs_hash v2 (incluye userland); build_components() helper;
  assemble_product_rootfs hidrata base→userland→servicios.
- 36 tests verde (nuevo: ensombrecido + retiro de applet + sh/busybox quedan).

Validado in-VM (product-boot-test.sh sobre el product-rootfs de la ruta real):
ls = "uutils coreutils 0.9.0" (/usr/bin/ls), find = "find (Rust) 0.9.1",
rg = "ripgrep 14.1.1"; /bin/ls y /bin/cat retirados, /bin/sh + busybox
preservados. SSH sigue verde. "Adelgazar busybox" cerrado en el producto.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 17:47:20 -04:00
sergioandClaude Opus 4.8 7baaf4afc5 hammer-bootstrap: capa de servicios (sshd como producto) — Etapa C Paso 2
Consolidación arquitectónica de sshd-como-servicio (Separación Mecanismo/
Política, decisión del usuario). El núcleo NO se toca: STAGE1_COMPONENTS +
STAGE1_SEED_CARD siguen siendo el mecanismo base atómico que el
selfhost-verify reconstruye bit a bit (of_tree 9adefb82/7fa6cb4e blindado).

Nuevo en hammer-bootstrap:
- SERVICE_COMPONENTS = [netup, openssh] (política de producto).
- SSHD_SERVICE_CARD: card genesis Native/Restart (netup + ssh-keygen -A +
  exec sshd -D), validado E2E en QEMU.
- product_seed_card(): compone la seed de producto = seed base + cards de
  servicio apendados al genesis vía serde_json (hammer sigue autocontenido,
  sin dep de card-core). El núcleo (hammerd+getty) se preserva.
- assemble_product_rootfs() + product(): HIDRATACIÓN TARDÍA — hidrata el
  stage1-rootfs ya sellado (4/4 verificado) + inyecta openssh/netup encima
  + escribe configs (passwd con sshd, sshd_config con PidFile /run, /var/empty
  0711, /root/.ssh) y sella un `product-rootfs` APARTE. of_tree del núcleo
  intacto. Idempotente, reproducible.
  GOTCHA: los ficheros hidratados son hardlinks read-only al store ⇒ romper
  el hardlink (remove+write) en vez de chmod (mutaría el inodo del store).
- CLI: `hammer bootstrap product --rootfs <base> --recipes recipes`.
- 4 tests nuevos (seed compone, hash determinista, assemble inyecta, recetas
  de servicio parsean). 36/36 verde.

scripts/product-boot-test.sh: valida que el product-rootfs de la RUTA REAL
bootea en QEMU y sirve SSH (sólo provisiona authorized_keys de prueba, no
ensambla nada). VERDE: arje levanta hammerd+getty+sshd, handshake real
"Accepted publickey for root", guest responde (seed=hammer-product con 3
cards, Linux 6.16.12). Cierra el agujero de verificación con la arquitectura
final, no con el spike sucio.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 17:31:20 -04:00
sergioandClaude Opus 4.8 6886f810bd crates/netup: red mínima Rust-nativa (Etapa C pieza 5)
netup = binario hammer-propio (sync, sólo libc, sin tokio) que configura la red:
levanta el link (netlink RTM_NEWLINK), negocia un lease DHCPv4 a mano sobre UDP
(DISCOVER/OFFER/REQUEST/ACK con flag broadcast), y aplica IP + ruta default +
/etc/resolv.conf (RTM_NEWADDR/RTM_NEWROUTE). Hand-roll de netlink y DHCP porque
no hay cliente DHCP Rust "maduro" para adoptar al estilo ripgrep, y el workspace
es 100% sync (rtnetlink arrastraría tokio). Reemplaza el `ip` estático de busybox.

Validado in-VM contra el DHCP de QEMU slirp: ✓ lease 10.0.2.15 + ruta + DNS +
egress TCP. Autodetecta la NIC (primera no-loopback) y espera carrier por sysfs.

Infra de prueba:
- scripts/drive-netup.py: bootea la VM, corre netup y verifica lease/NAT/DNS.
  SLIRP=1 ⇒ red user-mode (cero setup de host); si no, tap del lab.
- scripts/labnet.sh: lab de LAN real opcional (tap directo + dnsmasq + NAT) para
  fidelidad de hardware; no requerido para validar netup.
- scripts/netdiag.sh: diagnóstico del camino DHCP del lab (tcpdump+nft+dnsmasq).

Lección: para validar el CÓDIGO conviene slirp primero (cero plomería de host);
la LAN real (firewall INPUT, sutilezas del bridge, perms) es fidelidad posterior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 16:20:22 -04:00
sergioandClaude Opus 4.8 4dfb0ff127 Etapa B3: /store y /var/lib/hammer en particiones dedicadas (GPT)
scripts/disk-image.sh ahora arma una imagen GPT de 3 particiones ext4 en vez
de una sola: vda1=/ , vda2=/store (CAS inmutable), vda3=/var/lib/hammer (estado
mutable). Construcción sin root ni loopback: cp -al stagea el rootfs por
hardlinks (vacía store/ y var/lib/hammer/, instala el wrapper /sbin/init),
mke2fs -d puebla cada ext4 bajo unshare -r (root-owned), sfdisk escribe la GPT
y dd conv=sparse,notrunc empalma cada fs en su offset (imagen sparse, ~2G
reales). El wrapper /sbin/init monta vda2/vda3 y hace exec de arje-zero (el
kernel sólo monta vda1). drive-rebuild.py: root=/dev/vda1 en modo DISK.

Consecuencia resuelta: con /store en su propia partición el sellado cruza
filesystems y rename(2) da EXDEV. Store::seal cae a copia recursiva a un
staging dentro del store (preserva symlinks+modos) + rename store-interno
(atómico, mismo FS) + borrado del origen. Test copy_tree añadido.

Verificado in-VM (kernel hammer, KVM): vda{1,2,3} montados dedicados, 0
errores Cross-device, stage1' == stage1 ✓ REPRODUCIBLE. Cierra el ☐ de
SDD 11 §6 (particionado/montaje en la imagen destino).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 13:05:35 -04:00
sergioandClaude Opus 4.8 7bf49fb960 build: zig_version por-receta — 5 víctimas C dropean gcc (zig 0.13)
Mata gcc para 5 de las 7 recetas que lo forzaban, vía una escotilla nueva:

- hammer-core/hammer-build: campo `[build].zig_version` por receta. Cuando se
  fija, el lab resuelve ese zig (hermano del por defecto, `zig-x86_64-linux-<v>`)
  en vez del global, y entra al hash SÓLO si está presente (baseline 9adefb82
  intacto). `effective_zig_dir` lo aplica en ensure_layout + Sandbox.

- Causa: BISECT con oráculo flex (reproducido sólo vía lab: musl DINÁMICO) — el
  miscompile es una REGRESIÓN de zig 0.14; 0.13.0 compila limpio, 0.14/0.15/0.16
  fallan. Es C/musl-dinámico, NO afecta C++.

- Flip a zig_version="0.13.0" (quitando CC=gcc): flex, openssl, elfutils,
  binutils, python3. Verificados: `as` 2.45.1 corre, python3 3.12.10 corre
  (deepfreeze OK), libcrypto/libelf sellan. Todas son tools (no inputs del 4/4).

cmake queda en gcc: su segfault es C++ (libc++/musl), bug distinto que 0.13 NO
arregla (ni con -static). El kernel queda pendiente de verificar.

Tests: hammer-core/hammer-build verdes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 20:20:17 -04:00
sergioandClaude Opus 4.8 98f0b2d228 bootstrap: orquestador all + export del manifiesto (Etapa A)
Cierra la Etapa A del camino a la distro (SDD 11 §5):

- `hammer_bootstrap::all(seed, recipes_dir, base_cfg, store) -> AllReport`
  encadena stage0→stage1→stage2 en una corrida del host y deja el
  `bootstrap.json` poblado. El veredicto ✓ REPRODUCIBLE lo sigue sellando el
  rebuild in-VM (selfhost-verify.sh); `all` ancla la referencia para comparar.
- CLI: `hammer bootstrap all --url … --sha256 … --version …` y
  `hammer bootstrap manifest` (imprime el log de transparencia, §4).
- Refactor: `parse_seed_kind` factoriza el match de `--seed` (estaba duplicado
  en 4 sitios del despacho).
- hammer-build: arregla el test stale `detect_cargo` — la fase Cargo evolucionó
  a `RF=…`+`-C link-self-contained=no` condicional, pero la aserción esperaba la
  forma vieja `RUSTFLAGS="-C linker=…`. Restaura el workspace en verde (43/43).

Tests: cargo test --workspace verde.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 19:11:56 -04:00
sergioandClaude Opus 4.8 9f7e75901b hammer-build: -C link-self-contained=no CONDICIONAL (Alpine rechaza la opción)
Fix de 503e09a, que añadía el flag SIEMPRE: el rust de Alpine (host x86_64-alpine-linux-
musl) NO sólo tiene self-contained apagado — PARCHEA la opción para que sea un error
("option `-C link-self-contained` is not supported on this target"), así que pasarla
rompía el build baseline (verificado: baseline-check falló en el build-script de
proc-macro2). Un rust vanilla (host x86_64-unknown-linux-musl, p. ej. hammer-rust) SÍ la
soporta y la NECESITA (su musl trae self-contained/rcrt1.o que choca con el crt1.o de zig
⇒ duplicate _start).

Ahora condicional por host-triple del rustc: se añade el flag SÓLO si el host NO es
`-alpine-`. Para Alpine ⇒ comando idéntico al pre-503e09a (sin flag) ⇒ 9adefb82 intacto
por construcción. Para hammer-rust ⇒ con flag ⇒ linkea con zig sin duplicar _start.
(`--print`/`--version` no validan -C; sólo el link real lo hace — por eso se discrimina
por host-triple, no por probe.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 22:50:10 -04:00
sergioandClaude Opus 4.8 503e09aeed hammer-build: -C link-self-contained=no en el build rust nativo (zig provee crt1.o)
El link de las recetas Cargo nativas lo hace `zig cc` (un driver de compilador completo
que ya aporta sus startfiles crt1.o). El target musl vanilla `x86_64-unknown-linux-musl`
trae CRT AUTOCONTENIDO (self-contained/rcrt1.o) y lo pasaría también al linker ⇒
`ld.lld: duplicate symbol: _start` (rcrt1.o vs crt1.o de zig). Añadir
`-C link-self-contained=no` a RUSTFLAGS hace que rustc NO aporte startfiles propios y
deje que zig los provea — para TODAS las unidades (deps, build-scripts, proc-macros y la
crate top; RUSTFLAGS las alcanza a todas, los bin_flags de `cargo rustc --` sólo a la top).

No-op para el rust de Alpine (host x86_64-alpine-linux-musl): su self-contained ya está
APAGADO — de hecho el baseline 9adefb82 linkea sin chocar con zig, lo que sólo es posible
si Alpine NO aporta startfiles self-contained. Imprescindible para un rust vanilla, p. ej.
el hammer-rust del frente self-host (SWAP_RUST): con él, hammerd YA compila y sella con
hammer-rust (antes fallaba en el build-script de proc-macro2).

Verificación baseline (of_tree(stage1) sin swap == 9adefb82 con este hammer) en cola.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 22:12:31 -04:00
SergioandClaude Opus 4.8 a23630c50d feat(arje-link): transporte arje-bus → bus de agente, cierra B.2 end-to-end
hammerd::arje_link se suscribe al bus del init (ENTE_BUS_SOCK), relee el frame
postcard de arje-bus con un mirror mínimo de suscriptor (sin arrastrar el
crate-graph de arje ⇒ hammer sigue standalone) y reenvía cada BusEvent →
crashes → Event::Crashed → agent.sock. Wire verificado byte-a-byte contra
arje-bus real (ulid string, frame Subscribe=[00,01,00,0d]); 2 tests de round-trip
local + frame. Se lanza en thread si ENTE_BUS_SOCK está definido (no-op si no).
Roadmap B.2 marcado  (resta sólo el smoke contra init vivo).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 03:30:04 +00:00
SergioandClaude Opus 4.8 f6b337f6cb feat(hydrate): hidratación dinámica real con patchelf (LinkMode::Dynamic)
Deja de ser un error "pendiente": hydrate(.., DynamicSpec{interpreter,rpath})
detecta ELF por magic, copia los que necesitan parcheo (no hardlink: patchelf
mutaría el store) y aplica --set-interpreter/--set-rpath atómicamente
(copy→patch→rename); no-ELF y spec vacío siguen hardlinkeando. ensure_patchelf
falla limpio antes de tocar el FHS si falta la herramienta. HydrateReport.patched
cuenta los reescritos. Callers (bus/cli/bootstrap/e2e) pasan None=estático.
4 tests nuevos (incl. camino de error sin patchelf y real gated). Doc §4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 03:23:12 +00:00
SergioandClaude Opus 4.8 8a5e75344a feat(swm): init_rule se materializa a /etc/hammer/init.d/{service}.rule
Deja de ser un no-op "pendiente fase 5": apply::apply_init_rule escribe una
regla TOML por servicio (service/action/command); enable/start/restart la
escriben, disable/stop la retiran (idempotente), con guardas de nombre y acción.
CLI y orchestrator la aplican (rebaseando con prefix/overlay). Es el contrato
on-disk que el init (arje) lee para supervisar. 3 tests + doc del formato.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 03:17:56 +00:00
SergioandClaude Opus 4.8 0f42c38106 feat(swm): source_patch modela tarballs además de git (Fase 4)
Mutation::SourcePatch y RecipeInline ganan campos repo/commit XOR tarball/sha256
(Option, serde-default ⇒ compat con .swm git existentes), resueltos por
swm::swm_source_kind con la misma regla que recipe::Source::kind. swm_bridge
sintetiza la receta según el modo; hammer export reconstruye fuentes tarball
como source_patch en vez de caer a file_drop (provenance fina recuperada).
Prompt del traductor y roadmap actualizados. Tests nuevos para tarball + XOR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 03:06:35 +00:00
SergioandClaude Opus 4.8 7c099afe4f feat(bus): B.2 sink del CRASHED — hammerd::crashes traduce eventos de arje
Nuevo módulo hammerd::crashes: traduce la señal de ciclo de vida normalizada
(fuente = arje-bus BusEvent) a proto::Event::Crashed y la bombea al EventBus →
agent.sock (capa de IA). Traducción + bucle de bombeo desacoplados del
transporte y testeados (3 tests). Resta sólo el adaptador de transporte
arje-bus ↔ hammerd. Roadmap B.2 actualizado.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 02:51:43 +00:00
sergioandClaude Opus 4.8 e9f535a1bb selfhost-verify: pieza 4 (bwrap swap) + materialización de build-deps en el lab
Cuarta pieza del toolchain hammer-from-source (variante b, SDD 11 §7.2b): bubblewrap, el
sandbox del propio lab. Binario estático ⇒ swap de archivo sobre /toolchain/usr/bin/bwrap.
Como herramienta del toolchain (no input del 4/4) no necesita casar byte-a-byte con Alpine,
sólo aislar igual.

Tres piezas:
- recipes/libcap.toml (2.78): dep obligatoria de bwrap; el toolchain Alpine no trae el -dev
  (libcap.a / sys/capability.h). Build estático musl con zig cc, sin patches (es tool, no input).
- materialización de build-deps (hammer-build): deps.build ahora se CONSTRUYE recursivamente
  (build() llama build() por cada dep) y cada artefacto sellado se apila como capa --overlay-src
  bajo el rootfs del sandbox, dejando usr/{include,lib,lib/pkgconfig} en /usr. pkgconf y zig cc
  las hallan sin plumbing de flags. Recetas sin deps: sandbox byte-igual (baseline intacto).
  Tests nuevos: no_deps_emits_single_overlay_src, deps_stack_as_overlay_layers_under_rootfs.
- recipes/bwrap.toml (0.11.0): el toolchain no trae meson/ninja/python, así que bypaseamos meson
  compilando los 4 .c de bubblewrap directo con zig cc (+config.h trivial). deps.build=["libcap"].

Validación host fuerte: hammer-bwrap es estático, corre --version y sandboxea, y musl rebuildeó
BYTE-IDÉNTICO usándolo de sandbox (bisección). Expuesto con SWAP_BWRAP=1. Tests verdes.
Pendiente: corrida in-VM acumulando swaps para el sello ✓ REPRODUCIBLE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 20:05:55 -04:00
sergioandClaude Opus 4.8 b05badee23 selfhost-verify: pieza 3 (linux-headers swap) — SWAP_LINUX_HEADERS=1, byte-idéntico a Alpine en host
Tercera pieza del toolchain hammer-from-source (variante b, SDD 11 §7.2b): los headers
UAPI del kernel 6.16.12 que musl/busybox #include, hoy tomados del paquete linux-headers
de Alpine.

- recipes/linux-headers.toml: `make headers` (no `headers_install` — su rsync final falta
  en el toolchain hermético) + unifdef vía HOSTCC=zig cc; .tar.gz (busybox-tar lo
  descomprime sin xz). Sella los 13 subdirs kernel-owned de usr/include.
- assemble_builder: el swap ahora soporta rel_path de DIRECTORIO (reemplaza el árbol
  entero: remove + copy), no sólo binarios. Cubierto por test nuevo
  (builder_rootfs_swaps_toolchain_header_tree_from_source, incl. borrado de huérfanos).
- recipes/linux-headers-alpine-compat.patch: el paquete de Alpine no es el `make headers`
  crudo. Aporta content-pinned 5 archivos que difieren de la salida vainilla; 3 son
  REQUERIDOS (scsi/{scsi,scsi_ioctl,sg}.h — legacy userspace, NO UAPI) porque el applet
  `eject` de busybox los incluye y sin ellos no compila. install los pisa sobre /out y
  limpia el junk (.cmd/Makefile/headers_check.pl/drm) que `cp -a` arrastra.
- scripts/selfhost-verify.sh: SWAP_LINUX_HEADERS=1 construye la receta y emite un --swap
  por subdir kernel-owned (auto-derivado del artefacto sellado).

Criterio de éxito fuerte: `diff -r` del header-tree de hammer contra el de Alpine = VACÍO
⇒ el toolchain swapeado es byte-idéntico al baseline ⇒ reproducibilidad por construcción.
Ensamblado end-to-end en host OK (13 swaps, musl bits/sys intactos). 133 tests verdes.
Pendiente: corrida in-VM para el sello ✓ REPRODUCIBLE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:35:01 -04:00
sergioandClaude Opus 4.8 be6f2e6e89 fix(sandbox): CARGO_BUILD_JOBS=1 para serializar el backend paralelo de rustc (determinismo)
[VERIFICACIÓN EN CURSO — no confirmado aún] Stage 2 variante (b) destapó que
codegen-units=1 NO basta para reproducibilidad: el backend paralelo de rustc/LLVM
(ThinLTO + codegen) dimensiona su pool de hilos por el paralelismo disponible y sus
decisiones varían build-a-build a >1 hilo.

Evidencia: arje-zero divergía ~62 KB entre dos builds host idénticos (mismo rustc
1.91.1, mismo commit fijado, mismos flags), mientras un build serial (la VM tiene
1 vCPU, o taskset -c 0 en host) reproducía bit-a-bit a of_tree=9adefb82. El baseline
0039b2b9 se construyó en paralelo ⇒ referencia no-fiable. make/musl/busybox/hammerd
están limpios (descartados uno a uno); el único flaky es arje-zero por paralelismo.

CARGO_BUILD_JOBS=1 limita el jobserver a 1 token para serializar el backend. Test
in-flight: rebuild de arje-zero con todas las CPUs + JOBS=1 debe dar 9adefb82. Si el
pool de LLVM ignora el jobserver (usa affinity), habrá que fijar afinidad o LTO.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 04:34:22 -04:00
sergioandClaude Opus 4.8 d71ef179eb fix(cli): --swap no debe confundir el : de b3: con el separador del rel_path
`--swap make=b3:fbad…` se parseaba como hash=`b3`, rel_path=`fbad…` porque el
split del rel_path opcional (`name=hash[:rel_path]`) corría ANTES de quitar el
prefijo `b3:`. Resultado: el assemble del builder fallaba con "no existe en el
artefacto sellado b3:b3" (lo cazó el primer intento de SWAP_MAKE=1 in-VM, antes
de bootear la VM ⇒ cero tiempo de máquina perdido).

Fix: quitar `b3:` del hash antes de buscar el `:` del rel_path. Extraje el parseo
inline a `parse_swap()` y le puse 4 tests de regresión (b3:+default, hash pelado+
rel explícito, b3:+rel explícito, falta '=').

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 22:13:24 -04:00
sergioandClaude Opus 4.8 7bc2ee6019 builder: --swap del toolchain — montar make hammer sobre Alpine (variante b, pieza 1)
Segundo paso del auto-alojamiento *puro* (SDD 11 §7.2b): tras sellar GNU make
4.4.1 desde fuente (commit 749ea9e), ahora el builder puede *usarlo*. El swap
reemplaza una a una las piezas que el builder toma de Alpine por recetas hammer,
con Stage 2 reverificando que el byte-output no cambia.

- `BuilderSpec.swaps: Vec<ToolchainSwap{name,artifact,rel_path}>`: monta el binario
  sellado sobre el path Alpine en /toolchain (estático musl ⇒ sin shim del loader).
- `swaps_digest` (ordenado por nombre) entra al hash lógico del builder ⇒ la
  procedencia deja de ser "todo Alpine" y se vuelve auditable para el log de
  transparencia. Vacío ⇒ digest "" (compat hacia atrás: builder pura-Alpine
  conserva su hash previo).
- CLI: `hammer bootstrap builder --swap make=<hash>[:rel_path]` (repetible;
  rel_path por defecto usr/bin/<name>).
- selfhost-verify.sh: opt-in `SWAP_MAKE=1` (construye recipes/make.toml y lo
  swapea) + `SWAPS="name=hash …"` para swaps extra. Default off ⇒ corrida
  pura-Alpine idéntica a la baseline conocida-buena.

Validado en el host contra el store real: pura `b3:8a370f5d…` vs swapped
`b3:e7e2282c…`, y /toolchain/usr/bin/make queda hardlinkeado al artefacto
fbad44ac… (ELF estático, no el dinámico de Alpine). 3 tests nuevos
(swap aplica, hash cambia, error si falta el binario). 37 tests verdes.

Pendiente: correr Stage 2 in-VM con el swap y confirmar que of_tree(stage1') == ref
(el make hammer compila los 4/4 idéntico al de Alpine ⇒ toolchain intercambiable).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 21:38:04 -04:00
sergioandClaude Opus 4.8 4c9bcc016b sandbox: CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 — arje-zero reproducible (Stage 2 no-det #3)
El rebuild in-VM daba ✗ DIVERGENTE (of_tree host 984e002f ≠ VM 94b93585) pese a
que los 4 componentes sellaban bajo el mismo key of_inputs. Bisección por componente
en el host: musl, busybox y hammerd reconstruyen byte-idéntico, y la ensambladura del
rootfs (of_tree) es determinista e independiente del entorno. El culpable era arje-zero:
dos builds del MISMO host daban binarios distintos (Δ ~9.6 KB por .text/.rodata/.eh_frame/
.gcc_except_table) — firma del codegen paralelo de rustc.

Raíz: el workspace de hammer pinea [profile.release] codegen-units=1 (por eso hammerd
reproducía), pero el monorepo tawasuyu no declara perfil → cargo default codegen-units=16,
cuyo reparto del crate en N objetos varía build-a-build.

Fix: el sandbox impone codegen-units=1 para TODAS las crates Cargo (el var de cargo gana
sobre el profile del repo fuente). El lab elimina el no-determinismo en vez de confiar en
upstream (SDD 09 §2). Verificado: dos builds cu=1 de arje-zero → byte-idénticos.

Nueva referencia reproducible 4/4: of_tree(stage1)=b3:0039b2b9… (reemplaza 198f209f… de
cu=16). Actualizados EXPECT_REF (selfhost-verify.sh) y runbook §8b/§8c.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 04:31:51 -04:00
sergioandClaude Opus 4.8 1ab53a650f selfhost-verify: módulos del kernel booteado + build a stderr (E2BIG)
Dos bugs latentes que sólo aparecían en un build real (in-VM); en el host
quedan tapados porque todo sale de caché.

1. scripts/selfhost-verify.sh: los módulos se sacaban de /lib/modules/$(uname -r)
   pero la VM bootea $KERNEL (/boot/vmlinuz-linux), que puede ser otra versión.
   Mismatch de version-magic ⇒ overlay.ko no carga ⇒ bwrap muere "No such device".
   Ahora la versión se deriva del bzImage booteado (file -bL "$KERNEL"; fallback uname -r).

2. crates/hammer-build/src/sandbox.rs: spawn_pump teeaba el stdout del build al
   stdout del padre. rebuild-stage1 hace PRIME=$(hammer ... bootstrap stage1); en
   un build real son miles de líneas de configure/make capturadas en $PRIME ⇒ el
   paso siguiente --rootfs "$PRIME" exec con un arg gigante ⇒ E2BIG (Argument list
   too long). Ahora el tee va a stderr; stdout queda para el hash legible-por-máquina.

Con esto el verify corre end-to-end in-VM: los 4 componentes reproducen bit a bit,
pero of_tree(stage1) diverge (host vs VM vs baseline) — no-determinismo del ensamblado
a cazar (SDD 09 §2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 03:04:45 -04:00
SergioandClaude Opus 4.8 4fe452848f bootstrap: -mcpu=baseline en zig cc — reproducibilidad CPU-independiente (Stage 2)
El rebuild in-rootfs de Stage 2 cazó un no-determinismo real: stage1 reconstruido
en la VM divergía del host (of_tree host=b3:94a4f1… vs VM=b3:ab615b…), aun con el
seal hash idéntico (el seal es of_inputs: mismo key ≠ mismos bytes).

Diagnóstico (control en el host, sin VM): un rebuild de hammerd en el host es
byte-idéntico al cacheado, así que la receta es determinista host-a-host y el cache
no está stale. La divergencia es del entorno de build de la VM: `zig cc` default a
`-mcpu=native` y hornea la ISA del builder en el C/asm de los build-scripts (blake3,
curve25519-dalek). El host tiene SHA-NI+AVX2; `-cpu Broadwell` (VM bajo TCG) no tiene
SHA-NI ⇒ codegen distinto ⇒ bytes distintos ⇒ of_tree distinto. Misma raíz que el
SIGILL de AVX del runbook §7 (binarios al CPU del builder, no a un baseline genérico).

Fix: `-mcpu=baseline` en TODOS los sitios zig cc — CC/CXX del sandbox, los dos
wrappers Cargo (.hammer-zig-cc) y CC/HOSTCC de la receta de busybox. Las rutas SIMD
del runtime (blake3/sha2) son asm con dispatch en runtime: siguen presentes. Bonus:
cierra el AVX/SIGILL del §7 (corre en qemu64 sin -cpu Broadwell).

Validado en el host: con baseline hammerd cambia de bytes (1672448 vs 1677584 —
confirma que el default no era baseline) y los 3 componentes rebuildan limpio
(stage1 rc=0). Nueva referencia CPU-independiente of_tree(stage1-baseline)=
b3:4408e44e2ec51c3769deffcd64dd1f6c2010d414418937c3fa7930a0ded3f845. Runbook §8c
documenta el cruce del muro Rust-offline (cargo vendor por la NIC, hammerd sellado
in-VM en 74m35s) y este diagnóstico+fix.

Nota (plan C.2): el flag de CPU no entra en of_inputs ⇒ cambio de toolchain/flags da
cache-hit con bytes viejos; cerrar ese "pin de toolchain al hash" es trabajo aparte.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 20:07:36 +00:00
SergioandClaude Opus 4.8 fd20e38d38 bootstrap: CA bundle para el TLS de cargo vendor en la VM
Con red, `cargo vendor` resolvió crates.io (DNS OK) pero el TLS falló: la VM no
tenía CA bundle en /etc/ssl/certs/ca-certificates.crt ([77] SSL CA cert). El
toolchain trae el bundle de Alpine; rebuild-stage1 lo copia a la ruta estándar y
exporta SSL_CERT_FILE/CURL_CA_BUNDLE/GIT_SSL_CAINFO (cargo usa libcurl/openssl
según el build). Penúltimo eslabón del vendoring Rust offline-asistido.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 18:22:04 +00:00
SergioandClaude Opus 4.8 09034a5e2b bootstrap: red en el builder para el vendoring Rust (cargo vendor)
Los componentes Rust hacen `cargo vendor` (baja de crates.io); la VM no tenía red
⇒ `failed to sync`. rebuild-stage1 ahora levanta una NIC e1000 (qemu user-mode
10.0.2.0/24): insmod del módulo e1000.ko (como overlay, =m y no cargado), config
estática de eth0 (10.0.2.15, gw 10.0.2.2) y DNS de slirp (10.0.2.3). Best-effort:
sin NIC es no-op (los componentes C no necesitan red). El workspace no tiene deps
git, así que crates.io basta. e1000.ko se inyecta en el builder (.dev-fs/alpine).

Driver de boot: añade `-netdev user -device e1000` y sube el techo a 4h (builds
Rust bajo TCG).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:20:38 +00:00
SergioandClaude Opus 4.8 890a37b473 bootstrap: empaquetar el builder con cpio --owner=root:root (copy-up de overlay)
El run en la VM, ya con overlay cargado, fallaba en `bwrap: Can't mkdir /opt/zig:
Permission denied`. Diagnóstico (el rootfs resultó ser tmpfs, no ramfs, así que
no era el fs): los ficheros del builder viajaban con el uid del que lo armó
(1000). En la VM corren como root (0); el userns de bwrap mapea sólo 0→0, así que
el 1000 queda SIN mapear y el copy-up de overlay no puede preservar el owner del
/opt copiado ⇒ EACCES. En el host funciona porque bwrap mapea 1000→0.

Fix: empaquetar el initramfs con `cpio --owner=root:root` (runbook §8c). Revierte
el staging-a-tmpfs (era una hipótesis ramfs equivocada); rebuild-stage1 vuelve a
HAMMER_ROOTFS=/toolchain y documenta el requisito de ownership. 28 tests verdes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:29:17 +00:00
SergioandClaude Opus 4.8 cf15995623 bootstrap: builder carga overlay.ko — el sandbox de build lo exige en la VM
El run en la VM avanzó hasta `configure` y ahí bwrap falló: `Can't make overlay
mount … No such device`. Causa raíz: el kernel trae overlay como MÓDULO
(CONFIG_OVERLAY_FS=m) y el initramfs arranca sin módulos cargados ⇒ ENODEV. El
sandbox de hammer-build usa `bwrap --tmp-overlay` (raíz efímera sobre el
toolchain), así que necesita overlayfs.

Fix: rebuild-stage1 hace `insmod /toolchain/lib/overlay.ko` tras el shim del
loader (overlay no tiene deps; vermagic del kernel destino). El toolchain suma
`kmod` (insmod/modprobe). El `overlay.ko` es específico del kernel (no de Alpine):
se inyecta aparte en el builder (.dev-fs/alpine/lib/overlay.ko), documentado en
bootstrap-devfs.sh.

Progreso del run: shim OK ⇒ bwrap/git/curl corren ⇒ fetch OK ⇒ configure arranca;
overlay era el siguiente muro. 28 tests verdes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:20:06 +00:00
SergioandClaude Opus 4.8 64894d863c bootstrap: builder ejecutable — toolchain con bwrap/git/curl + shim del loader
Correr el rebuild EN la VM destapó dos faltantes reales del builder (justo lo que
la verificación de Stage 2 debe cazar):

1. El toolchain (variante a, desde Alpine) traía compiladores (cargo/make/zig)
   pero NO las herramientas de orquestación del lab — bwrap (anida el sandbox),
   git (git archive del mirror) y curl (tarballs). En el host las aporta el
   sistema; en la VM el toolchain es el único userland capaz, así que deben vivir
   ahí. bootstrap-devfs.sh las añade (paquete `bubblewrap`, no `bwrap`).

2. Esos binarios son Alpine *dinámicos* y no corren desde el userland Stage 1
   *estático* (musl --disable-shared ⇒ no hay loader en /lib). rebuild-stage1
   ahora instala un shim del loader musl (cp a /lib) + LD_LIBRARY_PATH/PATH al
   toolchain antes de invocar hammer; el sandbox de build sigue anidando en
   /toolchain. El hammer estático corre por ruta absoluta.

El builder boot end-to-end ya estaba probado; esto lo hace además *capaz de
reconstruirse*. 28 tests verdes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:13:32 +00:00
SergioandClaude Opus 4.8 83d4b2aa17 bootstrap: builder rootfs — fixes destapados al armarlo con caché real
Al construir el hammer estático musl y armar el builder con --work-cache work,
dos problemas reales (cazados antes de llenar el disco):

1. Recursión. --work-cache work --out work/builder-rootfs deja el destino DENTRO
   de la caché: copiar la caché se tragaba el propio builder a medio armar
   (work/builder-rootfs/work/builder-rootfs/… sin fondo). Fix: link_or_copy_tree
   toma un `skip` (path canónico a no descender), que corta el ciclo.

2. Bloat de sources. --work-cache copiaba TODO work/, incluidos los árboles ya
   materializados de work/sources (4 GB, regenerables). Fix: sólo se embeben
   work/repos (mirrors git) y work/tarballs (descargas verificadas); de ahí
   `fetch` rematerializa los sources en la VM. El builder pasa de ~5 GB a ~750 MB.

+2 tests (recursión termina y no se auto-copia; sources/ no viaja). Runbook §8c
aclara la semántica de --work-cache. El hammer estático musl
(target/x86_64-unknown-linux-musl/release/hammer) sale static-pie linked, listo
para bootear como PID-algo en la VM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:52:49 +00:00
SergioandClaude Opus 4.8 f17a8fa301 bootstrap: builder rootfs — ensambla el rebuild in-rootfs (Stage 2 pleno, §7)
El único sub-ítem que le quedaba a Stage 2: el rebuild *dentro* del rootfs. El
Stage 1 que booteamos es runtime (musl+busybox+hammerd+arje-zero), sin compilador
— no puede reconstruirse. El builder rootfs es Stage 1 + el toolchain adentro.

Implementa `hammer_bootstrap::builder_rootfs` (variante a pragmática, SDD 11 §7.2):
sobre el Stage 1 rootfs hidratado monta /toolchain (rootfs Alpine = 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 apunta HAMMER_ROOTFS=/toolchain, corre `bootstrap stage1` y compara stage1'
contra la referencia con `stage2 --verify` (lee SEED_HASH/SEED_KIND/REF_CONTENT
de /etc/hammer/rebuild.env).

- BuilderSpec/BuilderReport + builder_hash lógico (insumos: stage1+semilla+
  recetas+binario+tag toolchain+driver) — reproducible y auditable sin hashear el
  árbol Alpine; anota línea stage 2 en el manifiesto.
- link_or_copy_tree (hardlink-or-copy, sin chmod: no toca permisos del .dev-fs).
- El builder no se sella (toolchain Alpine no es content-addressed); se ensambla
  en out_dir para empaquetar como initramfs.
- CLI `hammer bootstrap builder --stage1 H --seed-hash H [--hammer-bin] [--toolchain]
  [--ref-content] [--work-cache] [--out]`. +4 tests (assemble, hash determinista,
  sin-ref, stage1 no sellado).

Validado contra el store real: Stage 1 73d7a9be… + semilla 3ce721ec… ⇒ builder
81dad3d9… (1.2 GB con el toolchain). Lo que queda es operacional: bootear en la
VM y correr rebuild-stage1 — runbook §8c documenta la receta (hammer estático
musl, initramfs, qemu -cpu Broadwell, --work-cache para rebuild offline).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:32:14 +00:00
SergioandClaude Opus 4.8 79ad2cad69 bootstrap: stage2() — ancla y verifica reproducibilidad (pre-Stage 2)
Esqueleto de Stage 2 (SDD 11 §3): la verificación de auto-alojamiento.

- VerifyReport + Reproducibility {Reproducible|Divergent|RebuildPending}
- artifact_content_hash(store, hash, name): of_tree del artefacto sellado
- stage2(stage1, store): ancla el content-hash del rootfs (los BYTES reales, no
  el hash input-addressed del store) como referencia y lo anota en el manifiesto
  (línea stage 2); verdict = RebuildPending
- verify_against(report, rebuilt): compara stage1 vs stage1' → Reproducible/Divergent
- CLI: `hammer bootstrap stage2 --rootfs HASH [--verify CONTENT_HASH]`

Validado sobre el rootfs real: content-hash b3:d0d5669f… (≠ store hash 73d7a9be…,
confirma que of_tree hashea bytes); --verify igual ⇒ ✓ REPRODUCIBLE, distinto ⇒
✗ DIVERGENTE. +2 tests. El rebuild nativo DENTRO del rootfs (produce stage1')
corre en la VM destino — el único sub-ítem que queda de Stage 2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:53:34 +00:00
SergioandClaude Opus 4.8 974504e068 build: SOURCE_DATE_EPOCH + TZ=UTC en el sandbox (determinismo pre-Stage 2)
Stage 2 compara el content-hash (of_tree) de stage1 vs stage1' rebuildeado; para
que coincidan, el build debe ser determinista. Las rutas ya lo son (el source se
bindea en /src, constante entre rebuilds); el knob que faltaba es el timestamp:
SOURCE_DATE_EPOCH=1 (+ TZ=UTC) fija los que tar/ar/gzip y __DATE__/__TIME__ embeben.

No cambia ningún ArtifactHash (input-addressed) ⇒ caché intacta; sólo normaliza
los bytes de salida de builds futuros. Plan C.2 #5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:49:35 +00:00
SergioandClaude Opus 4.8 9676fff0dd core: ArtifactHash::of_tree — content-hash determinista de un árbol (pre-Stage 2)
Stage 2 verifica bit-reproducibilidad comparando hash(stage1) vs hash(stage1').
Pero el ArtifactHash del store es INPUT-addressed (Merkle de inputs de receta:
fuente+flags+deps), así que esa comparación sería trivialmente true. Stage 2
necesita un hash de la SALIDA real (los bytes).

of_tree(root) hashea el contenido: rutas relativas ordenadas + tipo + bit de
ejecución + contenido / target de symlink, BLAKE3 length-prefijado. Determinista
e independiente de la ruta raíz y del orden del filesystem; no sigue symlinks.

+2 tests (determinismo entre dos árboles idénticos; detección de cambios de
contenido/exec/symlink-target).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:48:15 +00:00
SergioandClaude Opus 4.8 879af17f5c build: flags estáticos vía cargo rustc -- … (no RUSTFLAGS) para no romper proc-macros
Tercera iteración del fix de la corrida real (runbook §8). +crt-static (y
relocation-model=static) en RUSTFLAGS rompen los proc-macros en build nativo:
RUSTFLAGS alcanza a las deps, y un proc-macro es un dylib PIC que no puede ser
estático ("cannot produce proc-macro for clap_derive ... crate types").

Fix: pasar los flags de codegen del binario por `cargo rustc --release … -- <flags>`,
que los aplica SÓLO a la crate top-level, dejando proc-macros/deps intactos. Así el
binario final es crt-static + relocation-model=static (AUTOCONTENIDO y ET_EXEC sin
interpreter, como busybox: sin libc.so, sin loader, sin soname). El linker (zig cc)
sigue por RUSTFLAGS. musl queda --disable-shared (no se necesita loader).

Validado por unit tests; la corrida real rebuildea hammerd/arje-zero crt-static.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:24:01 +00:00
SergioandClaude Opus 4.8 ce7b570a62 build: recetas Cargo estáticas = crt-static + no-PIE (binarios autocontenidos)
Cierra el último bloqueo del boot que la corrida en QEMU destapó (runbook §8):
arje-zero/hammerd buildeaban DINÁMICOS contra musl (el rust de Alpine es dinámico
por defecto) → DT_NEEDED libc.musl-x86_64.so.1, y nuestro libc.so (musl shared con
zig cc) no exporta memcpy/memset/… → el loader falla y el init muere.

Fix golden-path: para link=static, RUSTFLAGS ahora fuerza
  -C target-feature=+crt-static -C relocation-model=static
⇒ binarios AUTOCONTENIDOS (musl dentro), ET_EXEC sin interpreter, como busybox:
sin libc.so, sin loader, sin el soname de Alpine. musl vuelve a --disable-shared
(no se necesita el loader). Boot en QEMU: usar -cpu Broadwell (el qemu64 default no
tiene el AVX que zig emite).

Validado por la corrida real: los 4 componentes buildan y el rootfs se sella; el
kernel arranca el initramfs y ejecuta arje-zero como PID 1. Falta rebuildear
hammerd/arje-zero con crt-static para cerrar el boot (cache-bust + rebuild).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 06:45:11 +00:00
SergioandClaude Opus 4.8 8d98a6053a build: vendoring con --locked condicional al Cargo.lock committeado (Opción 2)
Desbloquea recetas Cargo cuya fuente no committea Cargo.lock (p.ej. el monorepo
tawasuyu, que lo gitignora) sin tocar la política de ese repo.

vendor_cargo_deps: si la fuente trae Cargo.lock → `cargo vendor --locked`
(reproducible, deps pineadas por el lock del commit). Si no → vendoreo sin
--locked (genera el lock al vuelo) + warn! explícito de "build no pineado en el
tiempo". El end-state reproducible sigue siendo committear el lock (plan C.2 #5).

+1 test del camino sin-lock. Aplica a arje-zero (tawasuyu); hammerd (hammer,
que sí committea el lock) sigue por el camino --locked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 06:21:08 +00:00
SergioandClaude Opus 4.8 4895c1c884 build: BuildSys::Cargo nativo para el target del sandbox (Opción A) — hammerd builda
Resuelve el bloqueo Rust de la primera corrida: el rust de Alpine es
x86_64-alpine-linux-musl y --target x86_64-unknown-linux-musl no tiene std.

Cuando recipe.build.target == SANDBOX_NATIVE_TARGET (x86_64-linux-musl, el del
sandbox), se construye NATIVO (sin --target): el std nativo del rust del sandbox
sirve. El wrapper .hammer-zig-cc ahora:
- es un único ejecutable (resuelve el `zig cc` de dos palabras que cargo y cc-rs
  mal-parsean),
- SANEA el triple: cc-rs (build scripts, p.ej. blake3) detecta el host triple de
  Alpine y pasa --target=x86_64-alpine-linux-musl, que zig rechaza
  (UnknownOperatingSystem); lo reescribe a x86_64-linux-musl.
Se usa como CC (cc-rs) y como linker (RUSTFLAGS). El cross real conserva --target.

Validado en VM real: hammerd compila nativo con zig cc y se sella. +1 test (cross
mantiene el triple), detect_cargo actualizado. arje-zero queda bloqueado aparte:
tawasuyu no commitea Cargo.lock (vendor --locked falla) — ver runbook §8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 02:51:05 +00:00
SergioandClaude Opus 4.8 14e10d9292 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>
2026-06-11 01:55:52 +00:00
SergioandClaude Opus 4.8 778c3900ea recipes: arje-zero como receta puente Cargo a tawasuyu (track arje, build-side)
Puente concreto hacia "arje es el init de hammer" (ADR 0007): recipes/arje-zero.toml
construye el init PID 1 de arje con el lab de hammer.

- Cargo recipe: fuente = monorepo tawasuyu a commit fijado (el de la migración A0
  arje-cas → BLAKE3, así el CAS es coherente con hammer), -p arje-zero, estático
  con zig cc; deps vendoreadas en el fetch (Lote 6) ⇒ build --offline
- prueba que el lab cruza al repo de tawasuyu y produce el binario del init; NO es
  PID 1 todavía (eso es el paso "init real": seed card + arje-bus + hammerd Card)
- nuevo test que escanea recipes/ y valida que toda receta parsea + hashea offline

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 01:39:56 +00:00
SergioandClaude Opus 4.8 02827f62c9 bootstrap: hammerd como receta Cargo en Stage 1 (Lote 7)
Cierra el userland mínimo salvo el init definitivo. hammerd entra como tercer
componente de stage1, construido por el lab vía BuildSys::Cargo + vendoring:

- recipes/hammerd.toml: source git del repo hammer a commit fijado (ADR 0006),
  flags=["-p","hammerd"] para construir sólo ese binario del workspace, estático
  con zig cc; las deps se vendorean en el fetch (Lote 6) ⇒ build --offline
- STAGE1_COMPONENTS += hammerd; el inittab provisional lo arranca como servicio
  respawn (/usr/bin/hammerd) tras montar los pseudo-FS
- el test de recetas del repo ahora valida también hammerd.toml (parse + hash)

Falta sólo sustituir el init provisional de busybox por arje (ADR 0007) para el
CRASHED real. El cross-compile se valida en la VM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 01:06:40 +00:00
SergioandClaude Opus 4.8 e9b0551213 build: vendoring de deps Cargo en el fetch para builds --offline (Lote 6)
Completa BuildSys::Cargo: las recetas Rust necesitan sus deps de crates.io
disponibles para el `cargo build --offline` hermético del sandbox. fetch::
vendor_cargo_deps corre `cargo vendor --locked` (red OK fuera del sandbox) y
escribe .cargo/config.toml en el árbol del source; build() lo invoca cuando
detecta una receta Cargo, tras los patches.

- --locked fija por Cargo.lock (dentro del commit que hashea la receta) ⇒ sin
  no-determinismo nuevo: mismo commit ⇒ mismas deps vendoreadas
- crate sin deps ⇒ config vacío, inocuo
- test gated (skip si no hay cargo) del camino spawn → stdout → config.toml

Habilita hammerd (y a futuro arje) como recetas Cargo construibles offline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 01:04:46 +00:00
SergioandClaude Opus 4.8 60a9451a03 bootstrap: stage1() ensambla el userland mínimo (Lote 5)
Cross-compila musl + busybox con la semilla de Stage 0 (no el zig del host),
ensambla un rootfs FHS por hardlink y lo sella. CLI: `hammer bootstrap stage1`.

- seed_toolchain_dir(store, hash, kind): primitivo sin SeedSpec, para que Stage 1
  resuelva el toolchain desde (seed_hash, kind) sin re-pasar url/sha256
- RootfsHash = hash de CONTENIDO (componentes ordenados + init), no del árbol en
  disco ⇒ reproducible aunque difieran inodes/timestamps
- assemble_rootfs: esqueleto FHS + hydrate de cada componente + /etc/inittab
  provisional (busybox init); descarta el sidecar .hammer del rootfs
- idempotente (no reensambla si el rootfs ya está sellado); anota línea stage 1
- hammerd (receta Cargo) y arje quedan como slots pendientes (plan M2 / ADR 0007)

+3 tests (rootfs_hash determinista/sensible, assemble_rootfs, recetas del repo
parsean y hashean). Workspace verde; el cross-compile real se valida en la VM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 00:43:03 +00:00
Sergio c5ab7e4502 feat(build): BuildSys::Cargo — el lab construye crates Rust
Desbloquea integrar el workspace tawasuyu (y cualquier crate Rust) en el lab:
- detección por Cargo.toml (gana sobre Makefile huérfano, cede ante CMake de
  un políglota C+Rust).
- traducción de triple zig→rustc (x86_64-linux-musl → x86_64-unknown-linux-musl).
- compile: cargo build --release --locked --offline --target <triple>; link por
  wrapper zig cc (CARGO_TARGET_<T>_LINKER) que provee el libgcc_s que el link
  dinámico musl exige (cierra el 'cannot find -lgcc_s' que destapó M2). link=dynamic
  añade -C target-feature=-crt-static para dlopen (apps gráficas: Vulkan/Wayland).
- install: copia los ejecutables de target/<triple>/release a /out/usr/bin
  (busybox-safe).
- doc 02-build-lab §Cargo (vendoreo de deps para build offline) + receta de ejemplo
  recipes/llimphi-counter.toml (caso gráfico dinámico).
- 5 tests nuevos (34/34 verdes en hammer-build).

Coordina con tawasuyu/03_ukupacha/arje/PLAN-ATESTACION-Y-HAMMER §C (milestone:
falta BuildSys::Cargo en el lab).
2026-06-11 00:37:33 +00:00
SergioandClaude Opus 4.8 989e0d2c7f bootstrap: seed como toolchain del sandbox (Lote 3) + decisiones ADR 0008
SeedSpec::toolchain_dir resuelve el dir del compilador dentro de la semilla
sellada, listo para que el sandbox lo bindee como /opt/zig; seed_build_config
deriva una BuildConfig que cross-compila con el seed pinned, no con el zig del
host. Stage 0 sigue siendo ingesta pura: el layout se resuelve al usarlo
(zig en la raíz o en el hijo versionado), con error claro si falta o es ambiguo.

Decisiones del Lote 4 registradas en ADR 0008 / SDD 11:
- coreutils+shell = busybox (balsa desechable; GPLv2 no contamina el final)
- init arje diferido (Stage 1 con init mínimo provisional; CRASHED real luego)
- layout de semilla: ingesta pura + resolución al usar

+5 tests (layouts: raíz / hijo versionado / no-sellada / ambiguo / build_config).
Workspace verde.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 00:28:29 +00:00
SergioandClaude Opus 4.8 1f1fae3508 build: resolver deps.build recursivamente en artifact_hash (cierra TODO fase-0)
Lote 2 del track posterior. El hash de un artefacto ahora incluye el hash de
cada dep de build (Merkle sobre el subgrafo), no solo su fuente+flags.

- catálogo = base_dir de la receta: cada dep se resuelve a <base_dir>/<dep>.toml
  (firma de artifact_hash/build sin cambios; cero ripple en call sites)
- detección de ciclos con la cadena en el mensaje (a -> b -> a)
- errores claros: dep .toml ausente, o deps sin base_dir (receta de TOML crudo)
- 6 tests nuevos; ninguna receta actual declara deps ⇒ sin impacto en e2e

Prepara Stage 1: seed_hash entrará al hash de toda receta del userland mínimo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 23:59:34 +00:00
SergioandClaude Opus 4.8 ba4d520c44 Bootstrap: manifiesto versionado (bootstrap.json) + Stage 0 anota su línea
Lote 1 del track posterior. Implementa el embrión del log de transparencia
(SDD 11 §4): crate hammer-bootstrap::manifest con BootstrapManifest/StageEntry.

- append-only e idempotente (de-dup por (stage, artifact_hash))
- escritura atómica (tmp + rename); ts informativo, fuera de todo hash
- stage0() anota su línea en ambos caminos (sello nuevo e idempotente);
  un sha erróneo no genera línea
- 9 tests nuevos (4 de manifest + 5 de stage0/manifest), workspace verde

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 23:54:28 +00:00
SergioandClaude Opus 4.8 1731e7ef23 CLI: hammer bootstrap stage0 + limpieza de staging y e2e
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>
2026-06-10 21:22:44 +00:00
SergioandClaude Opus 4.8 3d6d25661c Track posterior: Stage 0 del bootstrap (crate hammer-bootstrap) + ADR 0008
Primer eslabón ejecutable del bootstrap from-scratch (SDD 11). Stage 0 ingiere un
toolchain semilla pinned al store sin compilarlo:

- SeedSpec{kind,version,url,sha256}. seed_hash() deriva el ArtifactHash de la
  identidad pinned (clase+versión+sha256), no de rutas del host ni del url → el
  mismo tarball desde otro espejo produce el mismo artefacto.
- stage0(seed, store): descarga (curl, file:// offline-ok), verifica sha256
  ANTES de extraer, untar y seal en el store. Idempotente si ya está sellado;
  un sha erróneo no sella nada.
- Reusa el lab: hammer_build::download (+ nuevo verify_sha256 público), Store::seal.
  Staging bajo el root del store para que el rename de seal sea atómico.

ADR 0008 fija la decisión (3 stages, zig semilla primaria, musl-cross-make
escotilla, ingerir-no-compilar el Stage 0). Renumerado a 0008 porque el 0007 lo
tomó la decisión de adoptar arje como init; SDD 11 y refs alineados con arje
(init supervisado que entrega el CRASHED real, ADR 0007).

4 unit tests (hash por identidad, ingest+seal+readonly, idempotencia, rechazo de
sha) sin red (file://). Workspace 218 verde.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 21:03:09 +00:00
SergioandClaude Opus 4.8 fe9a2d50cf Fase 2: anidación real de overlays con guard LIFO
Un `try` sobre un target ya cubierto apila un overlay nuevo: el kernel toma el
merged view de la capa inferior como lowerdir. Para que esto sea seguro y no un
footgun:

- fresh_id añade un `seq` atómico por proceso (`<ts>-<pid>-<seq>` zero-padded),
  eliminando la colisión de ids cuando un orquestador apila varios overlays en
  el mismo segundo — el caso real de la anidación.
- stack_key define un orden de apilamiento total y determinista (created_at,
  desempatado por id).
- blocking_overlays detecta capas más jóvenes que solapan targets (igualdad o
  ancestro de path). commit/discard fallan con Error::Shadowed si existen,
  exigiendo resolver LIFO de arriba hacia abajo — antes se desmontaba la capa
  equivocada del target compartido en silencio.

Tests: 6 unit del guard (disjuntos / solape / ancestro / desempate / commit y
discard rechazados) sin privilegios; e2e overlay_nested_stack_inside_userns
prueba el stack real (capa 2 ve la 1, rechazo LIFO, fusión arriba→abajo),
gated en HAMMER_OVERLAY_TESTS. Docs 04 y roadmap actualizados.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 20:39:32 +00:00
SergioandClaude Opus 4.8 303dfa2b58 Fase 3: op del watcher (Create/Edit/Delete) derivada del diario
CLOSE_WRITE ya no se materializa siempre como Replace. classify_op deriva:
- Delete: el fd resuelve a " (deleted)" (sufijo que ahora recortamos del path
  en vez de colarlo al diario; content_hash queda None).
- Edit: el diario ya tenía un evento previo no-Delete para el path.
- Create: primera mutación observada, o resurrección tras un Delete.

Es la divergencia que el diario atestigua, no la verdad absoluta del FS. La
atribución plena vía FAN_REPORT_DFID_NAME queda diferida: nix 0.30 no parsea
los info-records de FID y el modo FID perdería el fd que hashea el contenido.

classify_op se extrae como función libre para probarla sin CAP_SYS_ADMIN; 4
tests unitarios nuevos. Roadmap Fase 3 actualizado.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 20:33:00 +00:00
SergioandClaude Opus 4.8 6c34484fbe Fase 3: content_hash post-mutación + de-dup idempotente en el diario
El campo content_hash (ya existía en MutationEvent) ahora se puebla y se
usa para no ensuciar el diario con reescrituras sin cambios.

hammer-journal:
- content_hash_of(bytes) / hash_file(path): blake3 plano (estilo b3sum,
  "b3:<hex>"), distinto del of_inputs de artefactos — aquí la pregunta es
  "¿cambió el archivo?".
- last_for_path(path): último evento de un path.
- record_dedup(ev): omite el evento si deja el archivo idéntico al último
  estado de ese path (content_hash igual) o si es un Delete sobre algo ya
  borrado. Devuelve si escribió o no. +7 tests.

hammerd watcher: hashea cada CLOSE_WRITE y usa record_dedup; una
reescritura idéntica ni registra ni emite Modified al bus.

22 binarios de test verdes, sin warnings nuevos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 20:28:16 +00:00