Files
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

28 KiB
Raw Permalink Blame History

SDD 10 — Roadmap

Estrategia: validar la capa AI-nativa sobre Alpine antes de la distro propia

La innovación de hammer no es la distro — es el substrato AI-nativo (build determinista + mutable + diario + .swm). Construir bootstrap + init + userland de cero antes de validar esa capa sería arriesgar meses contra una hipótesis no probada. Por eso:

Fase 06: montamos hammer sobre Alpine (musl + busybox + FHS mutable, ya cocidos). Validamos el bucle completo en semanas. Track posterior: una vez probado, bajamos a distro propia (init propio, bootstrap propio, userland propio) reusando todo el tooling sin retrabajo.

Ver ADR 0002.


Fases (sobre Alpine)

Fase 0 — Laboratorio de build

  • hammer-core: tipos Recipe, ArtifactHash, Store (BLAKE3, layout /store).
  • hammer-build: sandbox con bubblewrap + zig cc → binario musl estático.
  • CAS: hashing de entrada, caché por hash, sellado en el store.
  • hammer build <recipe.toml> → imprime el hash del artefacto.
  • Fuente git y tarball (sha256 fijado), patches opcionales.
  • Heurística de build system: autotools / cmake / meson / make plano.
  • Caché persistente de zig entre builds (~30s/build ahorrados).

Fase 1 — Hidratación

  • hydrate(hash, target, mode): hardlink a FHS, patchelf para el caso dinámico.
  • hammer hydrate <hash> --into /.

🎯 Primer entregable (Fase 0 + 1)

GNU grep 3.12 compilado estático desde el tarball upstream, sellado en el store (b3:b81e893e…), hidratado por hardlink a /usr/bin/grep y ejecutado dentro del rootfs Alpine vía bwrap — confirma el corazón "fábrica funcional → FHS mutable" sobre el substrato Alpine. La VM/LXC dedicada queda como ejercicio de empaque, no como pre-requisito de validación.

Fase 2 — Overlay de experimentación

  • Crate hammer-overlay: try / commit / discard / status sobre overlayfs.
  • Manifiesto persistente en <state_root>/<id>/state.json; status enumera.
  • commit promociona archivos y procesa whiteouts (char dev 0/0) como remove.
  • Subcomandos del CLI: hammer try [targets…], commit <id>, discard <id>, status.
  • Tests E2E con bwrap+user-ns, gateados en HAMMER_OVERLAY_TESTS=1 (kernel-dependiente).
  • Hook al diario en commit (Fase 3 lo añadió vía commit_with_journal).
  • Anidación real de overlays. Un try sobre un target ya cubierto apila un overlay nuevo (el kernel usa el merged view inferior como lowerdir). Orden de apilamiento total y determinista vía stack_key (created_at + id, con seq por proceso en fresh_id para evitar colisiones en ráfaga). commit/discard exigen LIFO: blocking_overlays detecta capas más jóvenes que solapan targets (igualdad o ancestro de path) y la operación falla con Error::Shadowed si las hay. Guard cubierto por 6 unit tests; el stack real (capa 2 ve la 1, LIFO rechaza commit de abajo, commit arriba→abajo fusiona) por overlay_nested_stack_inside_userns, gated en HAMMER_OVERLAY_TESTS. Pendiente menor: unicidad de orden entre procesos concurrentes (track posterior).

Fase 3 — Diario de mutaciones

  • Crate hammer-journal: MutationEvent, Source (HammerHydrate/HammerCommit/External), append/read/tail/follow JSON-líneas. RFC 3339 sin chrono.
  • hammerd con fanotify (FAN_CLOSE_WRITE) sobre /bin,/sbin,/usr/bin,/usr/sbin, /lib,/usr/lib,/etc. Filtra eventos bajo overlays activos. Fallback graceful sin CAP_SYS_ADMIN.
  • hammer journal [--tail N] [--follow] [--format pretty|json].
  • hammer commit registra cada archivo promocionado vía commit_with_journal.
  • Refinar la op del watcher. watcher::classify_op(&journal, path, deleted) deriva Delete (el fd resuelve a " (deleted)", que ahora recortamos del path en vez de colarlo al diario), Edit (el diario ya tenía un evento previo no-Delete del path) o 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 (nombre en eventos de directorio, rename-sobre-destino, open_by_handle_at+CAP_DAC_READ_SEARCH) queda para el track posterior: nix 0.30 no parsea los info-records de FID y el modo FID perdería el fd que hoy hashea el contenido.
  • Hash del contenido tras la mutación (content_hash) y de-dup idempotente. hammer_journal::content_hash_of/hash_file (blake3 plano, estilo b3sum, distinto del of_inputs de artefactos). Journal::record_dedup omite el evento si deja el archivo idéntico al último de ese path (o un Delete sobre algo ya borrado); last_for_path lo resuelve. El watcher hashea cada CLOSE_WRITE y usa record_dedup: una reescritura sin cambios ni ensucia el diario ni despierta el bus.

Fase 4 — Formato y flujo .swm

  • Swm de/serialización YAML estable (roundtrip).
  • verify_schema (estructura + invariantes por mutación) y verify_base (distro_version + pins) con BaseRef / BaseCompat.
  • apply_config_edit (hunks -/+ con búsqueda exacta, no-fuzz) y apply_file_drop (base64 inline + verificación BLAKE3 vs content_hash).
  • Bridge Mutation::SourcePatchRecipe + build + hidratación.
  • CLI: hammer apply [--prefix DIR] [--base-ref base.json], hammer swm-verify, hammer export --journal DIR > out.swm.
  • patch_url / content_url remotos. hammer_build::download (curl, sólo fuera del sandbox; acepta file:// para tests offline). build_source_patch descarga patch_url al swm-recipes/<commit>.patch antes de compilar; hammer apply descarga content_url y verifica BLAKE3 antes de escribir (hash erróneo ⇒ nada tocado). Su integridad la cubre content_hash (file_drop) y el build reproducible + expected_hash (patch).
  • Provenance en export: mapa artefacto→receta vía sidecar .hammer/recipe.toml que hammer-build::build escribe dentro del artefacto antes de sellar. hammer export agrupa eventos por artifact_hash y emite UN source_patch por grupo cuya receta sea recuperable; lo demás cae al fallback file_drop. Source git y tarball modelados: Mutation::SourcePatch (y RecipeInline) llevan campos repo/commit xor tarball/sha256 (resueltos por swm::swm_source_kind, misma regla que recipe::Source::kind); hammer export reconstruye ambos sin caer a file_drop.
  • Firma signature (ed25519) y TrustStore local. hammer_core::sign: KeyPair (genera/carga/escribe claves), TrustStore::load(dir) (lee *.ed25519.pub), Swm::verify_signature(&trust) → SigStatus (trusted/unknown-key/bad-sig/ unsigned) sobre bytes canónicos JSON del manifiesto sin la firma. CLI: hammer keygen, hammer swm-sign, y hammer swm-verify --trust DIR. La firma reporta autoría/integridad; NO autoriza promover (sigue mandando reproducir + commit). SDD 09 §3,§5.
  • Hecho cuando: exportas un cambio, lo aplicas en otra máquina y reproduce idéntico. Demostrado en crates/hammer-cli/tests/swm_roundtrip.rs para config_edit + file_drop. source_patch reusa el camino de Fase 0/1 (gated en HAMMER_NETWORK_TESTS).

Fase 5 — Bus de agente (queda CRASHED real, diferido al track posterior)

  • /run/init.control (FIFO humano): mkfifo, reader que loguea cada línea, y hammer ctl <line> que escribe al FIFO con error claro si no existe.
  • /run/agent.sock (JSON-líneas) con handshake Hello/Welcome, auth via SO_PEERCRED y CapsPolicy por UID.
  • Comandos Compile/Inject/Query/Init; eventos Welcome/BuildReady/BuildFailed/Injected/QueryResult/InitAck/ Modified/Crashed/Error.
  • EventBus in-process: el watcher publica Modified y todas las conexiones lo reciben.
  • Subsistemas independientes en hammerd::main (FIFO/watcher/bus en threads); si uno falla en init, los demás siguen.
  • Política expresiva leída de /etc/hammer/agent-caps.toml (hammerd --agent-caps). hammer_core::AgentCapsConfig: default + [[rule]] por uid/gid (primera que casa gana), caps_for(uid,gid). bus::policy_from_config la enchufa; sin fichero o con fichero inválido, cae a la política por-UID por defecto (avisando). Ejemplo en examples/agent-caps.toml. El default_policy hardcoded queda como fallback.
  • CRASHED real (requiere supervisión de servicios, que llega con el init propio del track posterior).
  • BuildFailed.log_tail con cola real del lab. Sandbox::run hace tee de stdout/stderr (sigue viéndose en vivo) y retiene las últimas 50 líneas; al fallar una fase devuelve BuildFailure { reason, log_tail } envuelto en Error::Other. El bus lo recupera por BuildFailure::from_error (downcast) y separa reason del log textual del compilador, para que la IA reaccione al error real, no sólo al mensaje de Rust.
  • Hecho cuando: un cliente externo dispara un build y recibe el evento de fin por el socket. Camino implementado (CompileBuildReady/BuildFailed) y la maquinaria alrededor cubierta por crates/hammerd/tests/bus_e2e.rs: handshake con peer creds, gating no_cap, Query, Modified fan-out, Init→FIFO. Un Compile real reusa el camino de Fase 0/1 (gated en HAMMER_NETWORK_TESTS).

Fase 6 — Integración de la IA

  • Crate hammer-agent con tres piezas: - AgentClient: cliente síncrono del bus (handshake, compile/inject/query/init bloqueantes, drenado de eventos asíncronos). - IntentTranslator (trait) + MockTranslator cargado desde un IntentCatalog YAML (intent → .swm pre-armado). La integración con un LLM real se enchufa detrás del mismo trait sin tocar el bucle. - Orchestrator con run(intent) → Proposal: plan → schema → base → try (overlay o prefix) → apply (mutaciones puras + source_patch vía bus opcional) → verify → propose.
  • CLI: hammer ai <intent> --catalog F [--prefix DIR --base-ref F --bus SOCK].
  • Tipos del protocolo del bus movidos a hammer-core::proto para que hammerd y hammer-agent los compartan.
  • Tests: - 10 unit en hammer-agent (translator + catalog + orchestrator). - 3 e2e del bucle agéntico (prefix tmp → archivos esperados en disco). - 1 e2e del cliente contra un stub del bus (handshake + Compile→BuildReady + Modified asíncrono).
  • Traductor LLM real (Claude API u otro), opcional vía feature flag o crate aparte. ClaudeTranslator detrás de la feature llm-claude, con ureq + rustls. Modelo por defecto claude-opus-4-8, adaptive thinking + effort=high. Trait HttpClient inyectable para tests sin red. CLI: hammer ai --llm (mutuamente excluyente con --catalog). Sin la feature, el flag falla con mensaje claro.
  • Lenguaje de consulta del sistema (SDD 08 §6) para que la IA refiera servicios y archivos sin rutas frágiles. Forma kind:value (bin, file, pin, service, depends), evaluable local (hammer query <expr>) y remoto vía bus (Command::Query{what:"expr"}). Parser ELF64 mínimo para extraer DT_NEEDED.
  • Bucle de auto-reparación (cliente reacciona a Crashed con un nuevo plan). Orchestrator::run_with_repair(intent, &RepairPolicy) drena async events del bus tras cada apply; si llega Crashed, formatea una nueva intención y reentra hasta max_attempts. El Proposal final lleva el repair_chain completo para que el humano vea la evolución antes de hacer commit. CLI: hammer ai --repair-max-attempts.
  • Hecho cuando: una intención en lenguaje natural produce un cambio probado en overlay, presentado para commit humano. Demostrado por hammer ai con MockTranslator: intent → .swm → mutaciones aplicadas → Proposal con overlay_id + checks. El humano decide commit/discard. El upgrade a LLM real reusa todo el bucle.

Track posterior — distro propia

Diseño completo en SDD 11 — Bootstrap from-scratch. Resumen:

  • Bootstrap from-scratch en tres etapas (SDD 11 §3):
    • Stage 0 — crate hammer-bootstrap + hammer bootstrap stage0: ingiere el toolchain semilla pinned (SeedSpec, hash por identidad), verifica sha256 antes de sellar, idempotente. Cubierto por 4 unit + 1 e2e de CLI (offline, file://).
    • Stage 1 — userland mínimo, booteado end-to-end en QEMU. Recetas pinned musl 1.2.5 + busybox 1.36.1 (estáticas, zig cc) + hammerd + arje-zero (recetas Cargo: repo pinned + deps vendoreadas en el fetch, build nativo crt-static vía cargo rustc); hammer bootstrap stage1 los construye con la semilla, ensambla el rootfs FHS y lo sella. El init es arje-zero como PID 1 (/sbin/init→arje-zero) con su seed card /ente/seed.card.json; al bootear, arje-zero carga+valida la seed y supervisa hammerd + getty (el agent.sock y el watcher fanotify de hammerd levantan). El CRASHED real está demostrado: kill hammerd → arje lo detecta (Killed(SIGTERM)), programa restart con backoff y lo re-encarna — la supervisión del init propio que la Fase 5 difirió (ADR 0007). Diseño en SDD 12; corrida y fixes en el runbook. Pendiente: bus único (B.2: exponer el CRASHED a la capa de IA) y atestación (A1/A2).
    • Stage 2 — rebuild nativo dentro del rootfs y diff de hashes ⇒ auto-alojamiento bit-reproducible CONFIRMADO end-to-end (host↔VM). KVM=1 MEM=24576 ./scripts/selfhost-verify.sh reconstruye los 4/4 dentro del rootfs Stage 1 con sólo su propio toolchain (arje-zero cu=1 compila en ~27 min in-VM), sella el rootfs y su of_tree(stage1') = b3:0039b2b9… iguala la referencia ⇒ ✓ REPRODUCIBLE: stage1' == stage1. El último no-determinismo (#3, arje-zero/codegen-units) se cazó y eliminó imponiendo CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 en el sandbox (commit 4c9bcc0). Corrida y veredicto en el runbook §8.
  • Reemplazar el init de Alpine por tu init (bus por pipes nativo) — habilita el CRASHED real que la Fase 5 dejó diferido. Decisión: adoptar arje (init from-scratch ya existente en el monorepo tawasuyu, con PID 1 + supervisión real) en lugar de escribir uno nuevo — ver ADR 0007. Trae bus único, CAS BLAKE3 compartido y confianza en capas (procedencia hammer + atestación arje).
  • Userland propio (busybox/toybox a elección), /etc/service/* propios.
  • Decidir particionado/montaje de /store y /var/lib/hammer.
  • Log de transparencia para compartir en comunidad (SDD 09 §4): el bootstrap.json de SDD 11 §4 es su embrión.
  • Mini-lenguaje de consulta del sistema para la IA (SDD 08 §6).

Estado actual

  • Fases 06 cerradas sobre Alpine: build determinista → hidratación → overlay (con anidación LIFO) → diario (con op Create/Edit/Delete) → .swm firmado → bus de agente → bucle agéntico con IA (mock + Claude real tras llm-claude).
  • El bucle completo está validado: una intención en lenguaje natural produce un cambio probado en overlay, presentado para commit humano.
  • 214 tests verdes en el workspace (1 e2e de overlayfs gated en HAMMER_OVERLAY_TESTS; los de red en HAMMER_NETWORK_TESTS).
  • Único ítem de las fases diferido: CRASHED real (necesita supervisión de servicios → init propio del track posterior).
  • Track posterior arrancado y con su hito mayor cerrado: bootstrap from-scratch Stage 0 → Stage 1 (booteado en QEMU, arje-zero como PID 1, CRASHED real) → Stage 2 auto-alojamiento bit-a-bit confirmado (✓ REPRODUCIBLE, host↔VM). Baseline reproducible 4/4: of_tree(stage1)=b3:0039b2b9… (cu=1).
  • CERRADO — auto-alojamiento puro (variante b, SDD 11 §7.2b): que hammer construya el toolchain del builder desde fuente (no Alpine), reemplazando piezas una a una con Stage 2 verificando cada paso. Pieza 1 hecha: GNU make 4.4.1 (recipes/make.toml) se compila desde fuente con el lab — estática musl, sellada b3:fbad44ac…, reproducible bit-a-bit. Swap implementado: BuilderSpec.swaps + el flag hammer bootstrap builder --swap make=<hash> montan el make hammer sobre /toolchain/usr/bin/make (pisando el de Alpine) y lo anclan en el hash lógico del builder ⇒ la procedencia deja de ser "todo Alpine" y se vuelve auditable (pura b3:8a370f5d… vs swapped b3:e7e2282c… en el host). El verify lo expone con SWAP_MAKE=1. Stage 2 in-VM con el swap (2026-06-13): ✗ DIVERGENTE → causa raíz hallada y arreglada, make inocente. El swap dio of_tree(stage1')=9adefb82… ≠ 0039b2b9…, pero el control sin swap dio el mismo 9adefb82… ⇒ el make no era la causa. Bisección (descartando uno a uno: musl/busybox idénticos con hammer-make incluso a -j1; hammerd reproduce): el único flaky era arje-zero, no-determinista incluso host-a-host (~62 KB de diferencia, mismo rustc/commit/flags) — el backend paralelo de rustc/LLVM (ThinLTO) que codegen-units=1 NO serializa. Builds seriales (la VM tiene 1 vCPU, o taskset -c 0) reproducían bit-a-bit a 9adefb82…; los paralelos divergían. El viejo 0039b2b9… era un build paralelo no-fiable. Fix: CARGO_BUILD_JOBS=1 en el sandbox (serializa el jobserver ⇒ backend de 1 hilo, independiente del nº de CPUs) — verificado: arje-zero a todas las CPUs + JOBS=1 da byte-idéntico al serial. Baseline del host refrescado y EXPECT_REF re-baseado a 9adefb82…. Pieza 2 — busybox (sh+coreutils) validada en host: el /toolchain Alpine es mixto (busybox para sh/sed/grep/awk/tar/find, GNU coreutils para cp/mkdir/install); el swap monta el busybox estático de hammer sobre /toolchain/bin/busybox (--swap busybox=<hash>:bin/busybox, sin código nuevo). Con make+busybox de hammer los 4/4 (incl. busybox compilándose a sí mismo con hammer-busybox de shell) reproducen of_tree=9adefb82…. Expuesto con SWAP_BUSYBOX=1. ✓ REPRODUCIBLE in-VM con ambos swaps (2026-06-13): KVM=1 MEM=24576 SWAP_MAKE=1 SWAP_BUSYBOX=1 ./scripts/selfhost-verify.sh en libre reconstruyó los 4/4 dentro de la VM con el /toolchain hammerizado (make fbad44ac… + busybox 56664d70… pisando los de Alpine) y el of_tree(stage1') igualó la referencia 9adefb82…✓ REPRODUCIBLE: stage1' == stage1, DRIVER_RC=0 (~54 min, arje-zero cu=1 in-VM). La procedencia del builder ya no es "todo Alpine" y el auto-alojamiento sigue bit-a-bit. Pieza 3 — linux-headers (recipes/linux-headers.toml, kernel 6.16.12, validada en host): los headers UAPI que musl/busybox #include. Construidos con make headers (NO headers_install: su rsync final falta en el toolchain hermético) + unifdef vía HOSTCC=zig cc. El swap es de directorio, no de binario: se extendió assemble_builder para reemplazar árboles enteros (los 13 subdirs kernel-owned usr/include/{linux,asm, asm-generic,cxl,fwctl,misc,mtd,rdma,regulator,scsi,sound,video,xen}), dejando intactos los de musl (bits/sys/net…). Hallazgo clave: el paquete de Alpine NO es el make headers crudo — trae 5 archivos distintos a la salida vainilla; 3 de ellos (scsi/{scsi,scsi_ioctl,sg}.h, legacy userspace, NO UAPI) son requeridos porque el applet eject de busybox los incluye y sin ellos no compila. Se aportan content-pinned vía recipes/linux-headers-alpine-compat.patch (+ 2 cosméticos: kernel.h/if_tunnel.h) y se pisan sobre la salida. Criterio de éxito fuerte alcanzado: diff -r del header-tree hammer contra el de Alpine = vacío ⇒ el toolchain swapeado es byte-idéntico al baseline ⇒ todo lo que compile abajo reproduce de por sí (garantía por construcción, más fuerte que un rebuild). Expuesto con SWAP_LINUX_HEADERS=1; ensamblado end-to-end en host OK. Falta sólo la corrida in-VM para el sello. Pieza 4 — bwrap (recipes/bwrap.toml, bubblewrap 0.11.0, validada en host): EL sandbox del propio lab (hammer-build/src/sandbox.rs). Binario estático ⇒ swap de archivo sobre /toolchain/usr/bin/bwrap. Como tool del toolchain (no input del 4/4) NO necesita casar byte-a-byte con Alpine, sólo aislar igual. Tres sub-hallazgos que la hicieron más jugosa que make/busybox: (1) el toolchain Alpine no trae meson/ ninja/python — bypaseamos meson compilando los 4 .c de bubblewrap directo con zig cc (+config.h trivial generado a mano); (2) depende de libcap, cuyo -dev (libcap.a + sys/capability.h) tampoco está en el toolchain ⇒ nueva receta recipes/libcap.toml; (3) para que el build de bwrap viera libcap se agregó materialización de build-deps al lab: deps.build ahora se construye recursivamente y cada artefacto sellado se apila como capa --overlay-src BAJO el rootfs del sandbox, dejando sus usr/{include,lib,lib/pkgconfig} en /usr — pkgconf y zig cc los hallan sin plumbing de flags (recetas sin deps quedan byte-iguales, baseline intacto). Validación host fuerte: hammer-bwrap es estático, corre --version/sandboxea, y musl rebuildeó byte-idéntico usándolo de sandbox (bisección). Expuesto con SWAP_BWRAP=1; falta sólo correrla in-VM. Pieza 5 — coreutils (recipes/coreutils.toml, GNU 9.8, validada en host): cp/mkdir/ln/chmod/mv/ install que el make install de musl/busybox usa. Empaquetada multicall (--enable-single-binary, igual layout que Alpine: un /bin/coreutils + ~100 symlinks), así un solo swap del binario rutea todos los applets. Tool, no input ⇒ build vainilla sin patches. Bisección host fuerte: musl Y busybox rebuildearon byte-idéntico (57b66a2e, 56664d70) con hammer-coreutils pisando el de Alpine. Expuesto con SWAP_COREUTILS=1. Falta correrla in-VM. Herramientas de toolchain extra desde fuente (provenance, no en el camino del 4/4 mínimo): recipes/{patch,m4,pkgconf}.toml — GNU patch 2.8 (lo usa apply_patches), GNU m4 1.4.20 (base autotools) y pkgconf 2.5.1 (lee los .pc que materializa el lab). Construidas estáticas con zig cc y validadas (corren, versión correcta, funcionales). El 4/4 mínimo (musl/busybox/hammerd/arje-zero) no las invoca, así que no llevan flag SWAP_* dedicado: son swapeables con el escape SWAPS="name=hash:rel". Avanzan el "builder reconstruible al completo, no todo-Alpine" aunque el verify no las ejercite hoy. CORRIDA in-VM acumulada (2026-06-14): los 5 swaps de sandbox juntos ✓ REPRODUCIBLE. KVM=1 MEM=... SWAP_MAKE=1 SWAP_BUSYBOX=1 SWAP_LINUX_HEADERS=1 SWAP_BWRAP=1 SWAP_COREUTILS=1 ./scripts/selfhost-verify.sh reconstruyó los 4/4 dentro de la VM con el /toolchain hammerizado y el of_tree(stage1') igualó 9adefb82… bit a bit (~46 min, sobrevive presión de memoria). Recetas extra de toolchain desde fuente añadidas después: recipes/binutils.toml (GNU binutils 2.45.1, CC=gcc porque zig miscompila → segfault, link dinámico, inerte al of_tree porque zig ya provee as/ld/ar; swapeable vía SWAPS=).
  • CERRADO — frente rust/llvm (la última pieza grande del toolchain): cadena purista mrustc → rustc 1.90.0 → 1.91.0 → 1.91.1 con x.py real, reusando un LLVM 20.1.8 externo en todo (sin rebuild). rust-1.91.1-prefix/bin/{rustc,cargo} corren standalone (rpath, host musl), versión EXACTA de Alpine. Auto-consistencia VERIFICADA in-VM (2026-06-18): SWAP_RUST=1 KVM=1 PRESEED=hammerd ./scripts/selfhost-verify.sh✓ REPRODUCIBLE bit a bit (host y VM dan of_tree=7fa6cb4e… = RUST_EXPECT_REF, ≠ 9adefb82… Alpine). Para el rebuild bit-reproducible se committeó el Cargo.lock de arje-zero en tawasuyu + se bumpeó la receta (--locked); PRESEED=hammerd es OBLIGATORIO (arje-zero vendorea ~1973 crates y un rebuild in-VM completo desborda RAM). Artefactos en scripts/rust-frontier/.
  • CAPSTONE (2026-06-19): LOS 6 SWAPS JUNTOS ✓ REPRODUCIBLE IN-VM — los 5 tools de sandbox + SWAP_RUST a la vez, of_tree=7fa6cb4e… bit a bit (~7 min, MEM=12288 PRESEED=hammerd, sin swap en el host). El auto-alojamiento del toolchain del builder está COMPLETO; las piezas inertes al 4/4 (binutils/pkgconf) quedan salteadas por diseño. Capa de arranque restante (status libc, no vale de-Alpinizar): gcc, m4, libz/libzstd runtime.
  • CERRADO — kernel-from-source (SDD 11, último eslabón de soberanía): el kernel Linux 6.16.12 construido por hammer (recipes/linux.toml, CC=gcc, monolítico e1000/overlay/userns=y) BOOTEA la VM del selfhost-verify y el rebuild in-VM da ✓ REPRODUCIBLE bit a bit (HAMMER_KERNEL=1 KERNEL=<bzImage> KVM=1 MEM=16384 PRESEED=hammerd). Config lean (build ~35→~14 min, bzImage 13→8.7 MB). El muro pivot_root (/ = rootfs absoluto del mount-ns ⇒ bwrap no pivota; quirk que el kernel host enmascaraba) se resolvió con un /init wrapper switch_root a tmpfs (condicional a HAMMER_KERNEL=1, no toca el camino del kernel host). Build-deps del kernel TODOS de-Alpinizados: recipes/{flex,bison}.toml (kconfig), recipes/openssl.toml (3.5.4, certs/extract-cert), recipes/elfutils.toml (sólo libelf 0.194 para objtool, con shims musl mínimos). Capa de arranque restante: m4, libz/libzstd, gcc (status libc).
  • Etapa A — orquestador bootstrap all + log de transparencia exportable: hammer_bootstrap::all encadena stage0→stage1→stage2 en una corrida del host (hammer bootstrap all --url … --sha256 … --version …) y deja el bootstrap.json poblado; hammer bootstrap manifest lo imprime (la superficie de publicación del log, SDD 11 §4). El veredicto ✓ REPRODUCIBLE lo sigue sellando el rebuild in-VM (selfhost-verify.sh), no el host — all ancla la referencia para que ese rebuild la compare. Pendiente de la Etapa A: el smoke end-to-end de I3 (bus único) contra un init vivo (arje+hammerd corriendo) — no reproducible sin la VM, mismo status que cualquier sello in-VM.
  • B.2 — CRASHED real a la capa de IA (cableado end-to-end): el Event::Crashed del bus de agente ya tiene fuente real. Fuente (arje): arje-bus ganó BusRequest::Subscribe
    • BusPayload::Event(BusEvent); arje-zero difunde en on_death EnteCrashed{id,label,status} / EnteRestarting{delay_ms} / EnteExited a las conexiones suscritas, purgando las muertas (4 tests). Sink (hammer): hammerd::crashes traduce la señal normalizada → Event::CrashedEventBus/run/agent.sock (3 tests). Transporte (hammer): hammerd::arje_link se suscribe al bus de arje ($ENTE_BUS_SOCK) y reenvía cada BusEvent. Decisión de wire = relectura del frame postcard (mirror mínimo del subconjunto de suscriptor; no arrastra el crate-graph de arje ⇒ hammer sigue standalone). El layout está verificado byte-a-byte contra arje-bus real (mismas versiones ulid 1.2 / postcard 1.1; frame Subscribe = [00,01,00,0d]) y cubierto por un round-trip local (2 tests). Resta sólo el smoke end-to-end contra un init vivo (no reproducible sin arje corriendo).
  • ⏭️ También pendiente (Stage 1): atestación arje (A1/A2 — ya cableada en el repo arje, resta sólo enchufar su veredicto de boot a este roadmap).

Notas de entorno

  • Desarrollo principal: laptop del autor.
  • Host actual: Proxmox (gioser.net). La VM/LXC Alpine de pruebas vivirá aquí o en la laptop.
  • Repo: https://gitea.gioser.net/sergio/hammer.