Commit Graph
309 Commits
Author SHA1 Message Date
sergioandClaude Opus 4.8 977f07ca6c Etapa F paquetería #4: firma del release (índice del repo firmado)
Cierra el hueco de seguridad: hoy se firmaba cada .swm (autoría del paquete) pero NO el
catálogo ⇒ un atacante podía añadir/quitar/intercambiar entradas del index.json. Firmar el
release ancla qué paquetes/versiones/hashes existen.

- hammer-core/sign.rs: extraídos `KeyPair::sign_raw` + `verify_raw` genéricos (bytes canónicos
  arbitrarios); Swm::{sign,verify_signature} ahora los reusan (DRY, sin cambio de comportamiento).
- hammer-core/repo.rs: `RepoIndex.signature` (Ed25519 sobre la lista de paquetes canónica,
  excluye la propia firma) + `sign`/`verify_signature`. `upsert` INVALIDA la firma (cualquier
  cambio al catálogo ⇒ re-firmar). 4 tests (sign→verify, survive save/load, upsert-invalida,
  tamper→BadSig).
- hammer-cli: `repo sign --key` / `repo verify`; `install` VERIFICA el release antes de resolver
  (BadSig ⇒ aborta "el índice fue manipulado"); `repo list` muestra si está firmado.
- Validado E2E host: sin firmar→firmar→trusted→install lo ve; MANIPULAR el índice sin re-firmar
  ⇒ install ABORTA; re-publicar invalida la firma. 31 suites verde.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:53:31 -04:00
sergioandClaude Opus 4.8 b89e2dba51 Etapa F paquetería #3: deps entre paquetes — install resuelve el cierre y reconstruye el catálogo
Cierra el hueco que pack/install advertían: un paquete con build-deps (bwrap→libcap,
openssh→zlib,openssl) ahora se instala por nombre reproduciéndose desde fuente CON sus deps.

Modelo: las build-deps viajan por NOMBRE en el source_patch y en la PackageEntry; install
resuelve el cierre transitivo desde el índice y reconstruye un catálogo de recetas que el lab
consulta al materializar deps en el sandbox.

- hammer-core: `Mutation::SourcePatch.deps` (Deps, serde-skip si vacío) + `from_recipe` lo
  carga. `Deps::is_empty`. `RepoIndex`/`PackageEntry.deps` + `resolve_closure(name)` (DFS
  topológico, deps antes que dependientes, detecta dep faltante y ciclo). 8 tests nuevos.
- hammer-build/swm_bridge: refactor — `recipe_from_source_patch` (síntesis pública, setea deps
  + base_dir=catálogo) + `catalog_dir_for` (dir determinista compartido). build_source_patch
  lo reusa. synthesize_recipe ahora setea recipe.deps + base_dir al catálogo (no "/").
- hammer-cli: pack puebla PackageEntry.deps; install resuelve el cierre y escribe un {dep}.toml
  por dep en el catalog_dir ANTES de aplicar el target (mismo dir determinista que usa
  build_source_patch ⇒ el lab resuelve {dep}.toml por nombre). Warning de pack actualizado.
- VALIDADO E2E REAL contra ./store: `install bwrap` resuelve libcap del catálogo y reproduce
  el artefacto CACHEADO EXACTO (b3:f89e716…) → hidrata bwrap (1.8MB ELF). El paquete con dep
  hashea bit-idéntico al original. Resolución/diamante/faltante/ciclo unit-tested. 31 suites verde.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:43:30 -04:00
sergioandClaude Opus 4.8 e867bac388 Etapa F paquetería #2: repositorio + hammer install <nombre> / repo list
Cierra el lazo "packié un .swm → lo instalo por nombre". El repo es el namespace que
le da identidad a los .swm (que en sí no la llevan).

- hammer-core/repo.rs: `RepoIndex` + `PackageEntry` (load/save index.json, find, upsert
  idempotente por nombre que reporta el .swm huérfano). Índice JSON plano, ordenado,
  diffeable, firmable a futuro como release. 4 tests.
- hammer-cli:
  * `pack --repo DIR` PUBLICA (escribe <repo>/<name>-<version>.swm + upsert al índice con
    distro_version/expected_hash/signed_by; retira el huérfano de una versión vieja).
  * `install <nombre> [--repo] [--trust] [--base-ref] [--prefix] [--skip-source-patch]`
    CONSUME: resuelve nombre→.swm, verifica firma (con --trust) ANTES de reproducir, delega
    en el camino de apply (reproduce source_patch + hidrata). Nunca corre binario ajeno.
    Nombre inexistente → error legible con los disponibles.
  * `repo list` imprime el catálogo.
- Validado E2E en host: publicar ripgrep (firmado) + findutils, repo list, index.json limpio,
  install ripgrep --trust → "firma: trusted (by alice)" → apply OK. 31 suites verde.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:28:16 -04:00
sergioandClaude Opus 4.8 339a7b07ef Etapa F paquetería #1: hammer pack — receta del corpus → paquete .swm (source_patch)
Cierra la dirección forward que faltaba (SDD 06 §6 la marcaba "para más adelante"):
una receta que el sistema ya sabe construir se vuelve un paquete distribuible y
reproducible-desde-fuente, inversa de `hammer apply`.

- hammer-core: `Swm::from_recipe(recipe, target_bin, patch_text, expected, distro)`
  (constructor puro: el caller lee los patches). `SwmBuild` gana `phases`+`zig_version`
  y `SourcePatch` gana `strip_components` (Option/skip ⇒ .swm viejos parsean igual) para
  reproducir con fidelidad el corpus real (22/34 recetas usan phases, 8 usan zig 0.13).
- hammer-build/swm_bridge: la dirección inversa (source_patch→Recipe→build) ahora traslada
  phases/zig_version/strip_components a la receta efímera ⇒ apply rehace idéntico.
- hammer-cli: `hammer pack <recipe> [--target-bin] [--out] [--expected|--build] [--sign]`.
  Concatena los patches inline; avisa si la receta declara deps (el source_patch aún no
  las modela = pieza posterior). `export` también enriquece su source_patch.
- Validado en host: ripgrep (git+patch+install custom), openssl (tarball+zig 0.13+phases),
  coreutils (multicall), findutils firmado → swm-verify "trusted". Tests core+bridge verde.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:20:44 -04:00
sergioandClaude Opus 4.8 be881ad099 auto-recover al arranque: crate hammer-recover + hook /sbin/init (E4 #4b)
El modelo de generaciones es in-place (sin menu NixOS que ofrecer en GRUB); lo
que encaja es auto-sanar un upgrade interrumpido al boot. crates/hammer-recover
= mini-binario static-musl (lo unico que el producto necesita, sin el CLI hammer
completo): sin pending.json es no-op; con uno completa (roll-forward) o deshace
(roll-back) -> FHS siempre consistente; nunca aborta el boot. iso-image.sh
INSTALLER=1 lo compila (target musl) y bundlea al payload; hammer-live-install.sh
lo copia a /usr/sbin/hammer-recover y el wrapper /sbin/init instalado lo corre
tras montar /store y /var/lib/hammer, antes de incarnar arje-zero. iso-install-
test.sh valida el hook al boot (marker 'hammer-recover: sin upgrade interrumpido').

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 05:57:59 -04:00
sergioandClaude Opus 4.8 01b3a225b2 hammer-upgrade: journal de intención + recover idempotente (E4 endurecimiento #4a)
El apply escribe el plan completo a pending.json ANTES de proyectar y lo limpia
al commitear; un corte a media proyección deja pending.json + FHS a medias. La
proyección se hizo re-entrante (project_plan, backups idempotentes via
backup_existing_once que nunca pisa el original) ⇒ recover COMPLETA (roll-forward,
re-verifica of_tree) o DESHACE (roll-back: restaura backups, borra la gen a
medias, current->padre). apply se niega con PendingExists si hay intento; status
lo avisa. CLI: hammer upgrade recover [--rollback]. 5 tests (corte a media
proyeccion -> ambos modos) + ejercicio en upgrade-e2e-test.sh. 14 tests verde.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 04:45:27 -04:00
sergioandClaude Opus 4.8 fd09e842b4 arranque UEFI: ISO híbrido BIOS+EFI con grubx64.efi + ESP de mtools (refinamiento #3)
iso-image.sh EFI=1 añade un 2º El Torito EFI: ESP FAT (poblada con el mtools de
hammer, recipes/mtools.toml) con EFI/BOOT/BOOTX64.EFI = grubx64.efi
(grub-mkimage -O x86_64-efi). GRUB EFI lee el mismo grub.cfg y carga el kernel
EFI_STUB (ya en el kernel, sin rebuild) + initrd vía protocolo EFI. El mismo ISO
sigue booteando por BIOS (El Torito i386-pc) ⇒ híbrido. efi-boot-test.sh valida
ambas firmwares E2E (OVMF→grubx64.efi→sshd, y SeaBIOS→sshd).

GOTCHA: mformat -F fuerza FAT32 (min ~33MiB); sobre ESP chica deja FAT inválido
que la firmware no lee -> sin -F, auto FAT12/16.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 04:39:13 -04:00
sergioandClaude Opus 4.8 3225fb10b8 recipes/mtools: GNU mtools 4.0.49 from-source estático (E5/#3 EFI, herramental)
Poblar una ESP FAT sin privilegios (ni loop-mount root) para el arranque UEFI
exige mtools (mformat/mcopy/mmd), ausente en el host. Pieza simétrica a xorriso
para la rama EFI. zig 0.13.0, estático musl, --without-x, sin deps (iconv
built-in de musl). Sella d7892990.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 04:22:27 -04:00
sergioandClaude Opus 4.8 d73e20a394 instalar DESDE el live: hammer-install in-live + payload de disco (refinamiento #1)
Cierra el lazo ISO live -> disco instalado -> bootea solo. iso-image.sh
INSTALLER=1 bundlea kernel + GRUB MBR (boot/core.img + modulos) en el initramfs
bajo /usr/lib/hammer/install/ + inyecta /usr/bin/hammer-install. hammer-install
corre dentro del live como root real (busybox fdisk/mke2fs/mount/dd + uutils cp):
particiona MBR (/,/store,/var/lib/hammer), formatea, copia la propia raiz del
live (autoinstalador), escribe /boot+wrapper, instala GRUB con 2 dd (boot->MBR,
core->hueco post-MBR; punteros default 1/2 ya valen en MBR contiguo, sin parcheo).
AUTO_INSTALL=<dev> = /init desatendido (install+poweroff). iso-install-test.sh
valida E2E: ISO+disco blanco -> HAMMER-INSTALL-OK -> boot del disco solo ->
GRUB->kernel->arje-zero->sshd, particiones dedicadas montadas.

GOTCHAS: busybox fdisk CHS-alinea a 63 (hueco 62 < core 281 sectores) -> pisa el
FS de p1 -> grub>; fix sectores explicitos (p1@2048). Copiar top-level de /
enumerado, no lista fija, o se escapa /ente/seed.card.json.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 01:50:07 -04:00
sergioandClaude Opus 4.8 2976f3e5fd E5 medio live: ISO El Torito booteable con xorriso de hammer (primer corte)
scripts/iso-image.sh arma un ISO 9660/El Torito isohybrid usando el xorriso
construido por hammer (recipes/xorriso.toml, dogfooding): GRUB core El Torito
(grub-mkimage -O i386-pc-eltorito + iso9660) carga kernel + initramfs del ISO;
el rootfs del producto va como cpio.gz (live de RAM). scripts/iso-boot-test.sh
valida E2E in-VM: SeaBIOS->GRUB 2.14->Linux 6.16.12->/init (marcador
HAMMER-ISO-LIVE-OK)->arje-zero PID1->hammerd->netup(DHCP)->sshd escuchando.
El producto entero corre desde el medio live, sin tocar disco. ISO 113M.
SDD 13 E5 marcado primer corte.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 01:25:34 -04:00
sergioandClaude Opus 4.8 a1b8b57676 recipes/xorriso: GNU xorriso 1.5.8 from-source estático (Etapa E5, herramental)
El host no trae xorriso (y grub-mkrescue lo exige) ⇒ se construye desde fuente
con hammer para fabricar el medio ISO. Tarball GNU (libburn+libisofs+libisoburn
en un árbol), zig 0.13.0, 100% estático musl, todas las libs opcionales
desactivadas (readline/edit/acl/xattr/zlib/bz2/cdio) ⇒ binario autocontenido sin
deps. Sella b63d1a64, corre standalone (xorriso 1.5.8 + personalidad xorrisofs
mkisofs-compatible). Mismo patrón -static/-target musl de openssh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 01:15:53 -04:00
sergioandClaude Opus 4.8 a22552e28f hammer-upgrade: GC de generaciones huérfanas — 'hammer upgrade prune' (E4 hardening)
Tras un rollback las generaciones más nuevas quedan inalcanzables (no hay redo).
prune(state, keep) borra esas huérfanas (la cadena viva = current+ancestros vía
parent siempre se conserva); --keep N recorta además la profundidad de rollback.
live_chain() expuesto. 2 tests nuevos + paso prune en upgrade-e2e-test.sh (3->1
gens, árbol vivo intacto).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:49:18 -04:00
sergioandClaude Opus 4.8 d23946fa3d hammer-upgrade: upgrades atómicos con generaciones y rollback (Etapa E4, primer corte)
Crate hammer-upgrade + CLI 'hammer upgrade apply|rollback|status'. Aplica un
árbol Stage1/producto del store sobre el FHS vivo dejando una generación
(manifest + backup de bytes previos), atómico fichero-a-fichero con 'current'
como commit-point. Rollback restaura EXACTO al árbol anterior (o pre-upgrades).
Cada cambio al journal (HammerHydrate, replay-able). Verificación opcional del
of_tree esperado (índice de mirror E3 / release firmada). Maneja ficheros +
symlinks. 7 tests unitarios + scripts/upgrade-e2e-test.sh (apply v1->v2->
rollback->rollback contra el binario real). SDD 13 actualizado.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:46:14 -04:00
sergioandClaude Opus 4.8 30daab35e6 Cargo.lock: registrar hammer-mirror (cierre E3 primer corte)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:37:25 -04:00
sergioandClaude Opus 4.8 b9d3170859 hammer-mirror: mirror del store content-addressed (Etapa E3, primer corte)
Nuevo crate hammer-mirror + CLI `hammer mirror push|pull|status <remote>`. El /store es un
CAS (dir `<hash>-<name>`, el hash ES la dirección); un mirror = ese CAS replicado entre
máquinas + resolución por hash. La integridad se ancla en of_tree (content-hash BLAKE3 del
árbol): el receptor RECOMPUTA of_tree sobre lo copiado y exige que case el del índice ANTES
de sellar (Store::seal, atómico + read-only en un staging del mismo FS) ⇒ una transferencia
corrupta/manipulada se RECHAZA, no se instala.

- index(store): enumera dirs `<64hex>-<name>` (filtra bootstrap.json/.bootstrap-tmp), of_tree
  cada uno. diff(src,dst): faltantes en cada lado + conflictos (mismo dir, otro contenido).
- sync(src,dst): copia los que faltan (verificados), idempotente, no sobrescribe conflictos.
  push = sync(local,remoto); pull = sync(remoto,local).
- Transporte = filesystem (el remoto es una ruta a otro store, p.ej. sshfs); SSH/red queda
  como envoltorio ortogonal posterior. Endurecimiento: anclar of_tree a una raíz firmada
  (bootstrap.json / atestación D) vs un origen plenamente malicioso.

5 tests unitarios (forma de dir, índice, sync idempotente, rechazo de transferencia corrupta,
conflicto-no-sobrescribe) + validado E2E con el CLI sobre artefactos reales (push/pull/status,
idempotencia, bytes idénticos, tamper→conflicto, conflicto no sobrescrito exit≠0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:35:14 -04:00
sergioandClaude Opus 4.8 f7ea593e2d recipes: arje-zero-attest/arje-packager a 06184e43 (Cargo.lock → build --locked, Etapa D #2)
Bump del commit pinneado de c78a0ada → 06184e43 (rama tawasuyu selfhost/arje-zero-attest-
lockfile = c78a0ada + Cargo.lock del workspace force-committeado). Sin lock, tawasuyu lo
gitignora y `cargo vendor` corre SIN --locked ⇒ las deps derivan ⇒ el gated arje-zero (y el
of_tree del producto atestado) no es reproducible. Con el lock, el build es
`cargo rustc --release --locked --offline` ⇒ bit-reproducible. Espeja el patrón del núcleo
(arje-zero.toml @ selfhost/arje-zero-lockfile 9967b02c).

Lock regenerado sobre c78a0ada reusando los pins de main (1 línea de diff ⇒ versiones
MSRV ≤ 1.91.1 del sandbox preservadas). Rebuild --locked validado en host: compila limpio
(418 crates, sin error MSRV), sella arje-zero-attest d1a6f5c7 + arje-packager 2267b9b0;
producto atestado d35a9c09 BOOTEA en QEMU (gate anclado a rootkey soberana, 3 ✓, SSH OK).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 20:54:34 -04:00
sergioandClaude Opus 4.8 4e7272730f hammer-bootstrap: ancla soberana externa /etc/arje/rootkey.pub (I4 / Etapa D, hardening #3)
El producto atestado escribe la pubkey de la rootkey en /etc/arje/rootkey.pub (32 bytes
raw), leyéndola del attest_rootkey del seed ya firmado (lo que arje-packager computó de
nuestra rootkey privada) ⇒ cero criptografía nueva en hammer. El gate de arje
(attest_gate.rs::ancla_externa) la PREFIERE sobre la rootkey auto-declarada del seed
(trust = ancla.or(seed.attest_rootkey)): cierra el ataque "seed reescrito por completo"
que el WARN "sin ancla soberana externa" señalaba (hueco A2). product_attested_hash v2.

- assemble_attested_product_rootfs: tras firmar, extrae attest_rootkey (array 32B) y lo
  escribe en etc/arje/rootkey.pub; test hermético verifica el ancla.
- scripts/attest-boot-test.sh: nuevo modo SEED_REWRITE=1 (re-firma el seed con rootkey
  ATACANTE, deja la rootkey.pub legítima intacta ⇒ debe HALT).

Validado E2E en QEMU sobre el artefacto REAL v2 (hammer bootstrap product --attest):
  · ÍNTEGRO   → WARN desaparece, "anclada a rootkey soberana externa", 3 ✓, SSH OK.
  · SEED_REWRITE → gate: ancla ≠ attest_rootkey, 3× "autor no confiable" → Halt → sin SSH.

Nota: el ancla en fichero cierra el seed-rewrite; el ancla compilada ARJE_ATTEST_ROOTKEY
(dentro del binario atestado) sería más fuerte pero exige rebuild por-rootkey (documentada
en attest_gate.rs, no hecha). Queda sólo el pendiente #2 (Cargo.lock en tawasuyu).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 20:23:59 -04:00
sergioandClaude Opus 4.8 83d589a952 hammer-bootstrap: producto atestado cableado en product() (I4 / Etapa D, estructurar bien #1)
`product_attested()` + CLI `hammer bootstrap product --attest [--policy] [--rootkey]`:
estructura el spike attest-boot-test.sh dentro de hammer-bootstrap. Hidrata el init
CON gate (arje-zero-attest) sobre /usr/bin/arje-zero, fija attest_policy, deriva los
--bin label=path de la PROPIA seed (PID1 + execs Native, DFS) y FIRMA con arje-packager
(host, static musl) → seed firmada en /ente/seed.card.json; regenera /ente/attest.json
coherente con el init nuevo y sella product-attested-rootfs APARTE (núcleo+product base
intactos, Separación Mecanismo/Política).

- AttestConfig{policy, rootkey:[u8;32]}; DEV_ATTEST_ROOTKEY determinista ⇒ firmas
  Ed25519 reproducibles ⇒ árbol sellado reproducible. product_attested_hash v1.
- GOTCHA hardlinks read-only del store: romper /ente/seed.card.json antes de que el
  packager escriba (EACCES) y /ente/attest.json antes de reescribir; tmp de firma en
  staging/.attest-build se limpia antes de sellar.
- critical_bins_from_seed: getty+sshd comparten /bin/busybox ⇒ 3 concesiones por hash.
- Validado en host contra los sellos reales (3 concesiones, init 13MB, hammer attest ✓).
- Test hermético assemble_attested_swaps_gate_signs_seed_and_regenerates_attest (packager
  sintético). 40 tests verde.
- scripts/attest-boot-test.sh: MODO A REAL (PRODUCT_ATTESTED=<hash> bootea el artefacto
  real) + MODO B SPIKE fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 20:01:41 -04:00
sergioandClaude Opus 4.8 a44d05e7e2 atestación firmada END-TO-END: hammer firma, arje-zero-attest verifica (I4/D)
Cierra la integración canónica firmada de I4 (decisión del usuario): la mitad de
arje (gate) + la mitad de hammer (firma) acopladas con la cripto real de
arje-attest/agora — NO el manifiesto plano de /ente/attest.json.

tawasuyu (pusheado, commit c78a0ada en main): `arje-packager --seed-out` emite el
seed FIRMADO standalone (el gate attest_gate.rs ya estaba en main).

hammer:
- recipes/arje-packager.toml: el firmador (build-time tool, static musl, corre en
  el host). GOTCHA: lib+bin + monorepo virtual ⇒ flags `-p ... --bin ...`.
- recipes/arje-zero-attest.toml: arje-zero CON gate, variante SÓLO de producto
  (commit c78a0ada). El arje-zero del núcleo (9967b02c) NO se toca ⇒ of_tree
  baseline (9adefb82/7fa6cb4e) BLINDADO. Separación Mecanismo/Política.
- scripts/attest-boot-test.sh: valida E2E sobre una copia del product-rootfs
  (override del PID1 por el gated + seed firmado por arje-packager + boot QEMU).

Validado in-VM (rootkey fija ⇒ firmas Ed25519 deterministas):
- ÍNTEGRO: gate atesta arje-zero+hammerd+busybox ✓ (politica=Halt) → servicios
  arriba → SSH OK.
- TAMPER (1 byte en hammerd tras firmar): "atestación ✗ binario no atestado" →
  "ARRANQUE FALLIDO ... Halt — abortando antes de incarnar" → shell de rescate,
  SIN SSH. La integridad comprometida NO levanta el entorno.

Pendiente (estructurar bien): cablear esto en hammer-bootstrap product() (firmar
+ hidratar el gated automáticamente) y reproducibilidad (Cargo.lock committeado
en tawasuyu para el gated). El mecanismo ya está probado.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 19:38:01 -04:00
sergioandClaude Opus 4.8 0d11d68b44 atestación de integridad al arranque — mitad de hammer (I4 / Etapa D)
Por la regla de oro del plan arje↔hammer (PLAN-ATESTACION-Y-HAMMER.md §B.1):
hammer es dueño del expected_hash + TrustStore; arje del gate al boot. Esta
es la mitad de hammer — PRODUCIR y auto-verificar el manifiesto de hashes
esperados que el gate A2 de arje consumirá ("el expected_hash de un .swm ES
el BLAKE3 que arje atesta").

- hammer-core: ArtifactHash::of_file = BLAKE3 CRUDO del fichero (sin framing),
  el mismo que computa arje-cas::blake3_of ⇒ casa con quien recompute el hash.
- hammer-bootstrap: el producto emite /ente/attest.json con el BLAKE3 esperado
  de los binarios críticos (ATTEST_PATHS: arje-zero PID1, hammerd, busybox,
  sshd, netup, coreutils). Fichero APARTE de la seed card ⇒ no toca el schema
  de card-core y el producto bootea igual con el arje-zero actual (ignora A2).
  verify_attestation() recomputa y compara (ok/diverge/falta). product hash v3.
- CLI: `hammer attest --rootfs <dir>` — el gate de integridad hecho hoy por
  hammer ("reproducir, no confiar"); exit≠0 si algo diverge/falta.
- 37 tests verde (manifiesto + verify + tamper + missing).

Validado: el producto emite attest.json con los 6 binarios críticos; `hammer
attest` ✓ los 6; alterar 1 byte de coreutils ⇒ "✗ DIVERGE" exit 1; el producto
con attest.json sigue booteando + SSH (arje ignora el fichero). El gate al boot
(A2) es la mitad de tawasuyu/arje-zero (cross-repo, fuera de este commit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:45:14 -04:00
sergioandClaude Opus 4.8 2d18ed6dcd scripts/hammer-install: instalador a disco in-place (Etapa E2)
`hammer install <device>`: vuelca el product-rootfs lean sobre un disco
destino (block device real /dev/sdX, o un fichero pre-dimensionado para
pruebas) con el mismo layout GPT + GRUB BIOS de E1 pero IN-PLACE. Tras
instalar, el disco arranca solo (SeaBIOS → GRUB → arje-zero → hammerd+
getty+sshd). Contraparte "a disco real" de product-image.sh (imagen portátil).

- install-image.sh generalizado: si IMG es block device (o PREALLOC=1 sobre
  fichero pre-dimensionado) NO trunca ni borra el nodo — valida capacidad e
  instala in-place. sfdisk/mke2fs -d/dd-splice/GRUB son idénticos (dd a un
  offset de device = a un offset de fichero). El camino rootless de E1
  (unshare -r + dd conv=sparse,notrunc + patch GRUB) sirve igual al device.
- hammer-install.sh: resuelve el product-rootfs, provisiona authorized_keys
  (AUTHKEYS=<file> o TESTKEY=1 efímera), guardas de seguridad (FORCE=1 para
  un block device, rechazo si está montado), y delega a install-image.sh.
  Con BOOT=1+TESTKEY=1 arranca el target y valida por SSH.

Validado: instalación IN-PLACE sobre un fichero pre-dimensionado de 2200M
(ejercita el camino exacto del device, sin hardware real) → el disco
instalado auto-bootea (sin -kernel) y sirve SSH: ls = "uutils coreutils
0.9.0", /dev/vda3→/store y /dev/vda4→/var/lib/hammer montadas. El device
real sólo añade el guard FORCE + autodetección [-b].

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:32:35 -04:00
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 3dcc0760fd scripts/ssh-e2e-test: spike E2E de sshd en QEMU (Etapa C, Paso 1 verde)
Spike de validación (sucio, temporal) que cierra el agujero que el bwrap
anidado dejó: bootea un work/rootfs-ssh-test desechable (4/4 limpio del
store + openssh + netup PRECOMPILADOS) como initramfs, con sshd como
genesis card supervisado por arje-zero, red slirp con hostfwd :2222->:22,
y hace el handshake SSH real desde el host.

VERDE: arje valida la seed e instancia el card sshd (Native/Restart);
netup toma lease 10.0.2.15 (DHCP slirp); ssh-keygen -A genera host keys;
sshd escucha :22; "Accepted publickey for root" y el guest ejecuta el
comando (HANDSHAKE_OK uid=0, Linux 6.16.12). Quema las 3 incógnitas:
(1) handshake post-chroot/setuid de privsep como root REAL del guest
(lo que el bwrap no pudo), (2) arje gobierna el lifecycle del daemon,
(3) los fds de red no segfaultean contra musl.

Gotchas: cpio -R 0:0 (sin root-owned /var/empty sshd lo rechaza);
chmod u+w sobre el staging (el store está sellado read-only).

NO es plomería de producto: la consolidación (SERVICE_COMPONENTS +
hidratacion tardia sobre el 4/4 verificado, of_tree blindado) es el Paso 2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 17:23:06 -04:00
sergioandClaude Opus 4.8 c8eebe12b0 recipes/openssh: sshd/ssh from-source, estático musl (Etapa C pieza 6)
OpenSSH 10.3p1 desde fuente — LA excepción C del userland Rust (no hay
servidor SSH Rust de producción). Provee sshd+ssh+ssh-keygen+scp/sftp.
deps.build = zlib + openssl (libcrypto.a/libz.a estáticas), zig 0.13.0.

Binarios 100% estáticos musl. Tres gotchas zig-cc/-static resueltos:
- -pie de OpenSSH ANULA -static (sale dinámico) -> --without-hardening
  desactiva el PIE automático del "toolchain hardening".
- zig cc -static -lz, con .a y .so en /usr/lib, PREFIERE el libz.so de
  Alpine (NEEDED libz.so.1) -> sed CHANNELLIBS a -l:libz.a/-l:libcrypto.a
  (archivo estático exacto).
- zig cc NATIVO (sin -target) -static deja libc.so dinámico igual ->
  CC con -target x86_64-linux-musl usa el musl embebido de zig.

Validado en host: todos los binarios statically linked y corren
(ssh -V, sshd -V, ssh-keygen genera ed25519 con cripto real). Handshake
loopback en bwrap llega a preauth (accept TCP + IPC privsep monitor/child
+ parse hostkey OK); sólo lo frena chroot("/var/empty") por falta de
CAP_SYS_CHROOT en el userns anidado -> el E2E completo va a la card de
servicio in-VM (root real). Runtime: usuario sshd + /var/empty + host
keys via ssh-keygen -A.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 16:59:29 -04:00
sergioandClaude Opus 4.8 405ef5930a recipes/netup: receta Cargo de netup (Etapa C pieza 5)
Construye `-p netup` del workspace hammer (commit 6886f81) con deps vendoreadas
--offline, static musl vía zig cc. A diferencia de uutils/findutils/ripgrep
(adopción de binario maduro), netup es código hammer-propio (patrón hammerd).
Construye+sella: b3:9189d86…-netup, ELF static sin interpreter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 16:21:15 -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 50960d3c38 Etapa E1: imagen de disco auto-booteable (GRUB BIOS), sin -kernel
Primer entregable de release engineering (SDD 13 nuevo): scripts/install-image.sh
produce una imagen GPT que se BOOTEA SOLA en QEMU (`-drive file=img`, sin
-kernel ni firmware extra) — SeaBIOS → MBR → GRUB → kernel del propio disco →
arje-zero PID 1. Capaz de arrancar en hardware real.

Layout: vda1=BIOS boot (ef02), vda2=/ (con /boot/bzImage + /boot/grub),
vda3=/store, vda4=/var/lib/hammer. Reusa el particionado sin-root de B3
(sfdisk + mke2fs -d bajo unshare -r + dd conv=sparse) y el wrapper /sbin/init.

GRUB instalado SIN root ni loop: grub-bios-setup sondea el disco físico del
host (/dev/nvme…, 660 root:disk) para adivinar el root device del dir -d y falla
sin privilegios. Lo reemplaza un patch binario determinista sobre la ABI estable
de GRUB i386-pc: grub-mkimage arma core.img; un script Python escribe core.img
en la BIOS boot partition y parchea los punteros (boot.img off 0x5c kernel_sector
→ LBA de core.img; core.img off 0x1F4 blocklist.start → resto de core.img),
con asserts de los valores por defecto (1 y 2) para fallar ruidoso si la ABI
cambia. boot.img va al MBR sin pisar la GPT protective (sólo 440 B).

Verificado in-VM (KVM, sin -kernel): SeaBIOS → GRUB 2.14 → Linux 6.16.12 →
vda1..4 detectadas, vda2 root + vda3/vda4 montadas por el wrapper (0 errores) →
arje-zero PID 1 + hammerd (store=/store journal=/var/lib/hammer/journal).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 13:29:47 -04:00
sergioandClaude Opus 4.8 ffaccb9a98 recipes/ripgrep: rg Rust-nativo (Etapa C pieza 4)
ripgrep 14.1.1 (tag, commit 4649aa97) como receta Cargo, mismo patrón que
uutils/findutils/diffutils. Es su propio workspace ⇒ sin patch [workspace].
Estático musl, zig cc sólo linker; MSRV 1.72 ≤ 1.91.1.

ripgrep-no-jemalloc.patch: ripgrep mete jemallocator incondicional en musl64,
pero jemalloc-sys compila C (autotools) y embebe rutas de OUT_DIR ⇒ rompería la
reproducibilidad bit-a-bit del lab. El patch lo quita (dep Cargo.toml + stanzas
Cargo.lock + global_allocator en main.rs) ⇒ build 100% Rust, hermético, con el
allocator de musl. PCRE2 ya es opt-in ⇒ regex puro, sin C.

Construye+sella+corre: ELF static sin interpreter, `ripgrep 14.1.1
features:-pcre2`, búsqueda recursiva/glob OK. `rg` no es flag-compatible con
POSIX grep ⇒ no se symlinkea a grep; convive con el grep interino.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 13:16:45 -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 462e0275d3 Etapa B (B1+B2): boot desde disco real ext4, sin switch_root
B1 — kernel con disco+FS: linux.toml añade VIRTIO_BLK/VIRTIO_PCI/VIRTIO_NET/EXT4_FS
built-in (=y, sin módulos), aditivo al path initramfs. B2 — infra de disco:
  - scripts/disk-image.sh: empaqueta un rootfs como imagen ext4 booteable con
    `mke2fs -d` bajo `unshare -r` (archivos root-owned sin sudo, como el cpio
    --owner=root:root; evita EACCES en el copy-up de overlay del rebuild).
  - scripts/drive-rebuild.py: modo DISK= ⇒ QEMU monta la imagen como virtio /dev/vda
    y el kernel arranca con `root=/dev/vda rw rdinit=/sbin/init` — sin initramfs.

Verificado en QEMU/KVM: el kernel monta /dev/vda ext4 como root REAL y arje-zero
arranca como PID 1 DIRECTO, sin el hack /init+switch_root (el muro pivot_root del
path initramfs desaparece por construcción: / ya es un mount pivotable).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 10:42:40 -04:00
sergioandClaude Opus 4.8 761b074d14 recipes/diffutils: diff/cmp Rust-nativo (Etapa C pieza 3)
uutils/diffutils v0.5.0 (--bin diffutils multicall, despacha por argv[0]; symlinks
diff/cmp). Construye+sella+corre: ELF estático, diff -u unified con rc 0/1, cmp
reporta byte/línea. Mismo gotcha workspace que findutils → diffutils-workspace.patch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 08:53:08 -04:00
sergioandClaude Opus 4.8 7bd326170d recipes/findutils-xargs: xargs Rust-nativo (Etapa C pieza 2b)
uutils/findutils 0.9.1 --bin xargs (mismo crate/commit/patch que findutils.toml).
Construye+sella+corre: xargs 0.9.1, ELF estático, -n1/-I{}/pipe OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 06:05:01 -04:00
sergioandClaude Opus 4.8 c2c84a96ac recipes/findutils: find Rust-nativo (Etapa C pieza 2)
uutils/findutils 0.9.1 (tag 17d852c4, --bin find, static musl). Construye+sella
+corre: find (Rust) 0.9.1, ELF estático sin interpreter, -name/-type/-maxdepth OK.
onig_sys (C vía cc-rs) linkea con el wrapper zig-cc.

Gotcha general de toda receta Cargo de un crate suelto: hammer copia la fuente a
work/sources/ dentro del repo hammer (un workspace), así cargo vendor la absorbe y
aborta. Fix: findutils-workspace.patch inyecta un [workspace] vacío. coreutils lo
esquiva por ser ya raíz de workspace.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 21:44:48 -04:00
sergioandClaude Opus 4.8 cb9cfd9df4 docs(linux): veredicto formal — kernel al gueto gcc (zig-cc x86 kbuild)
zig 0.13 compila los host-tools del kernel (wrapper que traduce el depfile
-Wp,-MMD,PATH → -MMD -MF PATH; cache de zig deshabilitado), pero el target
choca con la traducción de flags x86 de zig cc (-mtune=generic, -march=x86-64).
gcc se retiene SOLO para {kernel, cmake}; el resto del userland C es gcc-free
vía zig 0.13. Futuro intento limpio: make LLVM=1 con el clang que zig empaqueta.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 21:44:41 -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 54ccc130dd recipes/uutils: coreutils Rust-nativo (Etapa C pieza 1)
Primera pieza del userland Rust-nativo (Etapa C): adopta uutils/coreutils 0.9.0
(MIT) como receta Cargo — el patrón de hammerd/arje-zero, no recompila C ni se
escribe de cero. Multicall static-musl (12 MB) + 79 symlinks por applet
(ls/cp/cat…), drop-in de busybox/GNU coreutils.

- Esquiva el miscompile de zig por construcción: codegen rustc/LLVM, zig cc sólo
  como linker.
- `--bin coreutils` (no `-p`): el paquete tiene lib+bin y `cargo rustc -- flags`
  exige un único target (gotcha vs arje-zero).
- Install custom (sólo esa fase; configure/compile siguen auto-Cargo) crea los
  symlinks con `coreutils --list`.

Construido + sellado + verificado funcional (ls/cat/echo vía dispatch argv0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 19:12:12 -04:00
sergioandClaude Opus 4.8 5f12692ccf docs: sincronizar roadmap con los frentes cerrados + Etapa A
El roadmap (SDD 10, "Estado actual") estaba detrás de la realidad ya verificada
in-VM. Sincroniza:

- Auto-alojamiento puro (variante b): 🚧 CERRADO (5 swaps in-VM
  ✓ REPRODUCIBLE, of_tree 9adefb82; binutils 2.45.1 añadido).
- Frente rust/llvm:  CERRADO (mrustc→1.91.1, auto-consistencia in-VM 7fa6cb4e).
  Capstone: los 6 swaps juntos in-VM ✓ REPRODUCIBLE.
- Frente kernel-from-source:  CERRADO (6.16.12 hammer-built bootea + rebuild
  in-VM reproducible; build-deps de-Alpinizados).
- Etapa A:  CERRADA (bootstrap all + manifest).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 19:12:12 -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 b37f11b666 kernel-from-source: de-Alpinizar flex/bison (recetas reales, no eclipsadas)
flex/bison estaban en el devfs base (apk) Y como recipes/{flex,bison}.toml en
deps.build del kernel — pero las capas de deps van BAJO el rootfs, así que el
Alpine flex/bison eclipsaba a los hammer y las recetas eran decorativas. Quito
flex/bison del devfs/bootstrap-devfs.sh; ahora el kernel los toma de deps.build
(hammer). Validado: rebuild en store fresco → kconfig compila con flex/bison
hammer (lexer.lex.o/parser.tab.o), HAMMER_KERNEL=1 verify → ✓ REPRODUCIBLE.
(El bzImage difiere en 1 byte del Alpine-flex/bison build: el timestamp embebido
UTS_VERSION, no flex/bison; el kernel no es input del of_tree del 4/4.)

m4 (que bison invoca) se deja en el base como tool autotools general. Con esto
los build-deps ESPECIALIZADOS del kernel (flex/bison/openssl/elfutils) son todos
hammer-from-source; sólo quedan las libs base/runtime (m4, libz/libzstd de libelf,
gcc) como capa de arranque, igual status que libc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 14:47:57 -04:00
sergioandClaude Opus 4.8 5937f746e5 recipes/elfutils: de-Alpinizar libelf (último build-dep Alpine del kernel)
tools/objtool del kernel enlaza -lelf (libelf+gelf.h); venía de `apk add
elfutils-dev`. Ahora recipes/elfutils.toml construye SÓLO libelf (0.194, lo que
objtool necesita — no libdw/libdwfl/src) desde fuente, wired como deps.build del
kernel. Con esto NINGÚN build-dep del kernel viene de Alpine: el camino del
kernel es 100% hammer-from-source.

elfutils es glibc-céntrico; musl no trae <error.h>/<argp.h>/<libintl.h>/fts/
obstack/rawmemchr. En vez del parche completo de Alpine, shims mínimos en compat/
(vía CPPFLAGS/-include): error.h y argp.h (sólo los usan color.c/printversion.c,
cuyos .o van en libeu.a pero objtool NO referencia ⇒ basta que compilen); libintl
no-op (+ --disable-nls); rawmemchr inline; y libargp/libfts/libobstack.a stubs
para pasar los AC_SEARCH_LIBS del configure (símbolos inertes para libelf). CC=gcc.

bootstrap-devfs.sh: elfutils-dev fuera del NEEDED. Validado: purgué elfutils-dev
del devfs, rebuild → objtool linkea el libelf hammer (sin gelf.h Alpine), y
HAMMER_KERNEL=1 verify → ✓ REPRODUCIBLE bit a bit, DRIVER_RC=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 13:51:15 -04:00
sergioandClaude Opus 4.8 7304eb3381 recipes/openssl: de-Alpinizar el build-dep openssl del kernel
certs/extract-cert (host-tool del kernel, lo arrastra CONFIG_SYSTEM_DATA_
VERIFICATION=y) enlaza -lcrypto; venía de `apk add openssl-dev`. Ahora
recipes/openssl.toml (OpenSSL 3.5.4, CC=gcc, libcrypto/libssl ESTÁTICAS vía
no-shared) lo construye desde fuente, wired como deps.build del kernel
(materializado como capa overlay en /usr: libcrypto.a + headers + pkgconfig).

bootstrap-devfs.sh: openssl-dev fuera del NEEDED. El runtime libcrypto3/libssl3
(que curl/git necesitan) lo sigue trayendo Alpine aparte — no es openssl-dev.

Validado: purgué openssl-dev del devfs, rebuild del kernel → certs/extract-cert
compila con el libcrypto hammer (sin Alpine openssl), y HAMMER_KERNEL=1 verify →
✓ REPRODUCIBLE bit a bit, DRIVER_RC=0. libelf (objtool) sigue como bootstrap-lib
apk (elfutils-en-musl es el difícil; de-Alpinizable luego).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 12:25:40 -04:00
sergioandClaude Opus 4.8 546c9df18e recipes/linux: config lean (build ~35min→~14min) sigue ✓ REPRODUCIBLE
El defconfig compilaba un PC completo (i915/DRM, sound, USB, media, wireless,
infiniband, BT, HID...) — nada de eso lo usa la VM headless del selfhost-verify
(consola serie + e1000 + initramfs + bwrap). Desactivo esos subsistemas + ATA/
SCSI/NVME/MD (sin disco, todo initramfs) + filesystems de disco innecesarios.

Verificado: HAMMER_KERNEL=1 con el kernel lean → ✓ REPRODUCIBLE bit a bit,
DRIVER_RC=0. Build baja de ~35min a ~14min.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:52:45 -04:00
sergioandClaude Opus 4.8 10961ce47f kernel-from-source: ✓ REPRODUCIBLE con kernel hammer (switch_root wrapper)
CIERRA el frente kernel-from-source: el bzImage hammer-built (Linux 6.16.12,
recipes/linux.toml) bootea la VM del selfhost-verify y el rebuild in-VM reproduce
el of_tree bit a bit (✓ REPRODUCIBLE, DRIVER_RC=0, ~3min).

El muro era bwrap `pivot_root: Invalid argument`: con un kernel hammer el / del
initramfs es el rootfs absoluto del mount-namespace (sin mount padre movible), y
bwrap del sandbox no puede pivotar de ahí (el kernel host lo permitía — quirk
suyo; diagnosticado con un debug-loop de initramfs mínimo, boot ~10s).

Fix portable (HAMMER_KERNEL=1): un /init wrapper PID1 que copia el rootfs a un
tmpfs y hace switch_root, dejando / como mount tmpfs real (pivotable). bwrap
pivota en CUALQUIER kernel. Condicional ⇒ el camino del kernel host (rdinit=
/sbin/init directo) queda intacto. drive-rebuild.py: APPEND env override para
pasar rdinit=/init.

Uso: HAMMER_KERNEL=1 KERNEL=<bzImage> KVM=1 MEM=16384 PRESEED=hammerd \
  ./scripts/selfhost-verify.sh

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:21:35 -04:00
sergioandClaude Opus 4.8 3627894045 drive-rebuild: APPEND env override para el cmdline del kernel
Generaliza el -append hardcodeado a una env var APPEND (default igual). Útil para
el frente kernel-from-source: experimentar con params del kernel sin tocar el
script. Documenta el hallazgo del muro pivot_root del bwrap (el bzImage hammer
bootea pero bwrap falla porque / es el rootfs absoluto, sin mount padre;
rootfstype=tmpfs no lo arregla).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 09:35:44 -04:00
sergioandClaude Opus 4.8 071d7d55b2 kernel-from-source: bzImage BOOTEA + iterar config (fanotify, overlay subfeatures)
Hito: el kernel hammer-built 6.16.12 BOOTEA la VM del selfhost-verify:
console ttyS0, arje-zero (PID1) levanta, hammerd corre, y la red e1000 vendorea
los crates OK (cargo vendor completo). El compile in-VM falla sólo en el sandbox
bwrap: "pivot_root: Invalid argument" (--tmp-overlay /) ⇒ DRIVER_RC=1.

Iteración de config (vs el kernel Arch/Artix que sí corre bwrap):
- +FANOTIFY +FANOTIFY_ACCESS_PERMISSIONS: hammerd lo usa para el watcher del
  diario (sin él: WARN ENOSYS, no fatal — pero ahora queda funcional).
- +OVERLAY_FS_{REDIRECT_DIR,INDEX,XINO_AUTO,METACOPY}: el defconfig los deja off,
  el working los tiene =y; hipótesis para el pivot_root del overlay en userns.

Estado: el kernel compila+bootea+inicia+red; falta el pivot del sandbox.
Pendiente tras esto: optimizar config (defconfig es grande, build ~35min).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 09:02:07 -04:00
sergioandClaude Opus 4.8 f108cc626d kernel-from-source: resolver build-deps (libelf+openssl) + refinar config
Avance del frente kernel: el build pasó objtool y certs.
- bootstrap-devfs.sh: +flex +bison +elfutils-dev +openssl-dev al NEEDED.
  * elfutils-dev (libelf/gelf.h): objtool lo exige — en x86_64 las mitigaciones
    seleccionan CONFIG_OBJTOOL aunque se use frame-pointer unwinder.
  * openssl-dev: el host-tool certs/extract-cert lo #incluye — el defconfig
    fuerza CONFIG_SYSTEM_DATA_VERIFICATION=y vía KEYS←NFS/integrity/dns_resolver
    (no apagable sin desarmar media config).
  Ambas son build-libs de arranque (status g++/zlib-dev), SEGURAS para el of_tree
  del self-host: hammerd no usa openssl (Cargo.lock limpio) y arje-zero va
  preseeded/cacheado (no se reconstruye). De-Alpinizables luego con
  recipes/{elfutils,openssl}.toml.
- linux.toml: quité los -d de keyring que no pegan (olddefconfig/syncconfig los
  revierten por el select-chain); openssl resuelve extract-cert. Comentario
  actualizado con el diagnóstico.

Estado: build compila el kernel completo (objtool+certs OK); defconfig es grande
(GPU/wireless), pendiente el bzImage + boot-test + optimización de config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 08:19:47 -04:00
sergioandClaude Opus 4.8 630cc3185d recipes/linux: WIP kernel 6.16.12 desde fuente (frente kernel-from-source)
Arranque del último frente de soberanía del bootstrap: construir el kernel que
bootea la VM del selfhost-verify (hoy importado pinned). Versión 6.16.12
(coherente con recipes/linux-headers.toml).

Estado: defconfig + ajustes monolíticos (e1000/overlay/userns =y, BTF/firma
off, frame-pointer unwinder). deps.build=[flex,bison,m4] (materializados como
capa overlay en /usr). flex/bison YA funcionan: kconfig parsea y asm-offsets se
genera. BLOQUEO confirmado: tools/objtool necesita libelf (gelf.h) — en x86_64
las mitigaciones seleccionan CONFIG_OBJTOOL aunque se use frame-pointer.

PRÓXIMO PASO: recipes/elfutils.toml (libelf) como dep.build → completa el
bzImage. Luego: KERNEL=<bzImage> ./scripts/selfhost-verify.sh debe dar
✓ REPRODUCIBLE (iterar la config hasta que el builder bootee).

CC=gcc/HOSTCC=gcc (zig miscompila binarios grandes; el kernel es el mayor).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 00:00:12 -04:00
sergioandClaude Opus 4.8 c67caa3c5b recipes/flex+bison: build-deps del kernel (frente kernel-from-source)
Primeros prerequisitos para construir el kernel Linux desde fuente: el kbuild
usa flex (lexer de kconfig) y bison (parser LALR de kconfig/dtc), ausentes del
toolchain Alpine base. Herramientas de build-time, no inputs del 4/4.

- flex 2.6.4: CC=gcc (zig miscompila el stage1flex que procesa su propio scan.l
  → "unrecognized rule"; mismo gotcha que binutils/python/cmake). link=dynamic.
- bison 3.8.2: zig cc estático (tool chica, corre bien). Necesita m4 en runtime
  (recipes/m4.toml) y sus skeletons en /usr/share/bison (BISON_PKGDATADIR).

Validados: flex 2.6.4 / bison 3.8.2 corren y procesan specs (.l→1736 líneas,
.y→1270 líneas).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 23:54:22 -04:00