Files
hammer/docs/10-roadmap.md
T
sergioandClaude Opus 4.8 42c7f3b21d docs(roadmap): make-swap in-VM dio DIVERGENTE, pero el make es inocente
Stage 2 in-VM con SWAP_MAKE=1 dio of_tree(stage1')=9adefb82 ≠ 0039b2b9. El
diagnóstico en host exonera al make: musl+busybox construidos con hammer-make
salen byte-idénticos al baseline (diff -r limpio) y el ensamblado completo da
of_tree EXACTO 0039b2b9. La divergencia es no-determinismo in-VM (coincidió con
swap thrashing: 97 min wall para ~27 min compute), no el swap del toolchain.

Pendiente: re-correr el verify sin swap (control) para aislar la flakiness in-VM.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 00:39:10 -04:00

255 lines
18 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` modelado;
`tarball` cae a file_drop con warning (pendiente extender SourcePatch).
- [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 corrido (2026-06-13):** dio `✗ DIVERGENTE` (`of_tree(stage1')=9adefb82… ≠
0039b2b9…`). **Pero el make es inocente:** el diagnóstico en host (musl+busybox con hammer-make en un
store desechable ⇒ byte-idénticos al baseline vía `diff -r`, y el ensamblado completo ⇒ `of_tree`
EXACTO `0039b2b9…`) prueba que hammer-make reproduce el baseline. La divergencia es **no-determinismo
in-VM** que coincidió con una corrida bajo presión de memoria fuerte (97 min wall para ~27 min de
compute, swap thrashing), no el swap. Pendiente: re-correr el verify **sin** swap (control) para
confirmar la flakiness in-VM independiente del make (sospechoso: arje-zero o la presión de memoria),
cerrarla, y *después* validar la pieza 1; luego make/autotools → linux-headers → bwrap → rust/llvm.
- ⏭️ **También pendiente (Stage 1):** bus único (B.2: exponer el `CRASHED` a la capa de IA) y
atestación arje (A1/A2).
## 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`.