Files
hammer/docs/10-roadmap.md
T
sergioandClaude Opus 4.8 a774552ead selfhost-verify: pieza 5 (coreutils swap) — SWAP_COREUTILS=1, musl+busybox byte-idénticos en host
Quinta pieza del toolchain hammer-from-source (variante b, SDD 11 §7.2b): GNU coreutils 9.8
(cp/mkdir/ln/chmod/mv/install…), que el `make install` de musl y busybox invocan.

- recipes/coreutils.toml: empaquetada multicall (--enable-single-binary), mismo layout que el
  paquete de Alpine (un /bin/coreutils + ~100 symlinks que despachan por argv[0]). Estática musl
  con zig cc, vainilla sin patches (es tool del toolchain, no input compilado del 4/4).
- scripts/selfhost-verify.sh: SWAP_COREUTILS=1 construye la receta y monta el único binario sobre
  /toolchain/bin/coreutils — los symlinks del toolchain (cp/mkdir/install/…) lo siguen, un solo --swap.

Bisección host fuerte: con hammer-coreutils pisando el de Alpine (trap-restore en .dev-fs), musl Y
busybox rebuildearon BYTE-IDÉNTICO (57b66a2e, 56664d70). Pendiente: corrida in-VM acumulando swaps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 02:23:28 -04:00

315 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](adr/0002-alpine-first.md).
---
## Fases (sobre Alpine)
### Fase 0 — Laboratorio de build ✅
- [x] `hammer-core`: tipos `Recipe`, `ArtifactHash`, `Store` (BLAKE3, layout `/store`).
- [x] `hammer-build`: sandbox con `bubblewrap` + `zig cc` → binario musl estático.
- [x] CAS: hashing de entrada, caché por hash, sellado en el store.
- [x] `hammer build <recipe.toml>` → imprime el hash del artefacto.
- [x] Fuente git **y** tarball (sha256 fijado), patches opcionales.
- [x] Heurística de build system: autotools / cmake / meson / make plano.
- [x] Caché persistente de zig entre builds (~30s/build ahorrados).
### Fase 1 — Hidratación ✅
- [x] `hydrate(hash, target, mode)`: hardlink a FHS, `patchelf` para el caso dinámico.
- [x] `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 ✅
- [x] Crate `hammer-overlay`: `try` / `commit` / `discard` / `status` sobre `overlayfs`.
- [x] Manifiesto persistente en `<state_root>/<id>/state.json`; `status` enumera.
- [x] `commit` promociona archivos y procesa whiteouts (char dev 0/0) como remove.
- [x] Subcomandos del CLI: `hammer try [targets…]`, `commit <id>`, `discard <id>`, `status`.
- [x] Tests E2E con bwrap+user-ns, gateados en `HAMMER_OVERLAY_TESTS=1` (kernel-dependiente).
- [x] Hook al diario en `commit` (Fase 3 lo añadió vía `commit_with_journal`).
- [x] 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 ✅
- [x] Crate `hammer-journal`: `MutationEvent`, `Source` (HammerHydrate/HammerCommit/External),
append/read/tail/follow JSON-líneas. RFC 3339 sin chrono.
- [x] `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.
- [x] `hammer journal [--tail N] [--follow] [--format pretty|json]`.
- [x] `hammer commit` registra cada archivo promocionado vía `commit_with_journal`.
- [x] 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.
- [x] 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` ✅
- [x] `Swm` de/serialización YAML estable (roundtrip).
- [x] `verify_schema` (estructura + invariantes por mutación) y `verify_base` (distro_version
+ pins) con `BaseRef` / `BaseCompat`.
- [x] `apply_config_edit` (hunks `-/+` con búsqueda exacta, no-fuzz) y `apply_file_drop`
(base64 inline + verificación BLAKE3 vs `content_hash`).
- [x] Bridge `Mutation::SourcePatch``Recipe` + `build` + hidratación.
- [x] CLI: `hammer apply [--prefix DIR] [--base-ref base.json]`,
`hammer swm-verify`, `hammer export --journal DIR > out.swm`.
- [x] `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).
- [x] 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`.
- [x] 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)*
- [x] `/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.
- [x] `/run/agent.sock` (JSON-líneas) con handshake `Hello/Welcome`, auth via
`SO_PEERCRED` y `CapsPolicy` por UID.
- [x] Comandos `Compile`/`Inject`/`Query`/`Init`; eventos
`Welcome`/`BuildReady`/`BuildFailed`/`Injected`/`QueryResult`/`InitAck`/
`Modified`/`Crashed`/`Error`.
- [x] EventBus in-process: el watcher publica `Modified` y todas las conexiones lo reciben.
- [x] Subsistemas independientes en `hammerd::main` (FIFO/watcher/bus en threads); si uno
falla en init, los demás siguen.
- [x] 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).
- [x] `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 (`Compile``BuildReady`/`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 ✅
- [x] 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.
- [x] CLI: `hammer ai <intent> --catalog F [--prefix DIR --base-ref F --bus SOCK]`.
- [x] Tipos del protocolo del bus movidos a `hammer-core::proto` para que `hammerd` y
`hammer-agent` los compartan.
- [x] 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).
- [x] 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.
- [x] 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`.
- [x] 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](11-bootstrap.md). Resumen:
- **Bootstrap from-scratch** en tres etapas ([SDD 11 §3](11-bootstrap.md)):
- **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](adr/0007-arje-como-init-propio.md)). Diseño en
[SDD 12](12-init-real.md); corrida y fixes en el [runbook](runbooks/stage1-vm-boot.md). 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](runbooks/stage1-vm-boot.md) §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](adr/0007-arje-como-init-propio.md). 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](09-trust-model.md) §4): el
`bootstrap.json` de [SDD 11 §4](11-bootstrap.md) es su embrión.
- Mini-lenguaje de consulta del sistema para la IA ([SDD 08](08-ai-integration.md) §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).
- 🚧 **En curso — auto-alojamiento *puro* (variante b, [SDD 11 §7.2b](11-bootstrap.md)):** 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`. **Pendiente:** correr in-VM acumulando los swaps
(`KVM=1 MEM=24576 SWAP_MAKE=1 SWAP_BUSYBOX=1 SWAP_LINUX_HEADERS=1 SWAP_BWRAP=1 SWAP_COREUTILS=1 ./scripts/selfhost-verify.sh`)
para el `✓ REPRODUCIBLE`, y seguir con la última pieza: rust/llvm (la grande — ya hay infra de deps).
-**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::Crashed`
`EventBus``/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`.