Diseña el corte del cordón umbilical con Alpine en tres etapas, reusando el lab de Fase 0 sin mecanismo nuevo: - Stage 0: toolchain semilla (zig primario, musl-cross-make como escotilla) ingerido al store como fuente pinned por sha256 — el único insumo del host. - Stage 1: userland mínimo cross-compilado (musl, busybox/toybox, init propio, hammerd) ensamblado en un rootfs sellado. El init propio habilita el CRASHED real diferido de Fase 5. - Stage 2: rebuild nativo dentro del rootfs y diff de hashes stage1 vs stage1' ⇒ auto-alojamiento bit-reproducible. Cada etapa anota (stage, recipe_hash, artifact_hash) en bootstrap.json, embrión del log de transparencia (SDD 09 §4). Propone el crate hammer-bootstrap y la CLI `hammer bootstrap stage0|stage1|stage2|--all`. Decisiones abiertas marcadas para un futuro ADR 0007. Enlazado desde el índice de docs y el roadmap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
14 KiB
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 0–6: montamos
hammersobre 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: tiposRecipe,ArtifactHash,Store(BLAKE3, layout/store).hammer-build: sandbox conbubblewrap+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,patchelfpara 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/statussobreoverlayfs. - Manifiesto persistente en
<state_root>/<id>/state.json;statusenumera. commitpromociona 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íacommit_with_journal). - Anidación real de overlays. Un
trysobre un target ya cubierto apila un overlay nuevo (el kernel usa el merged view inferior comolowerdir). Orden de apilamiento total y determinista víastack_key(created_at+id, conseqpor proceso enfresh_idpara evitar colisiones en ráfaga).commit/discardexigen LIFO:blocking_overlaysdetecta capas más jóvenes que solapan targets (igualdad o ancestro de path) y la operación falla conError::Shadowedsi 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) poroverlay_nested_stack_inside_userns, gated enHAMMER_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. hammerdconfanotify(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 commitregistra cada archivo promocionado víacommit_with_journal.- Refinar la
opdel watcher.watcher::classify_op(&journal, path, deleted)derivaDelete(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-Deletedel path) oCreate(primera mutación observada, o resurrección tras unDelete). Es la divergencia que el diario atestigua, no la verdad absoluta del FS. La atribución plena víaFAN_REPORT_DFID_NAME(nombre en eventos de directorio,rename-sobre-destino,open_by_handle_at+CAP_DAC_READ_SEARCH) queda para el track posterior:nix0.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, estilob3sum, distinto delof_inputsde artefactos).Journal::record_dedupomite el evento si deja el archivo idéntico al último de ese path (o unDeletesobre algo ya borrado);last_for_pathlo resuelve. El watcher hashea cadaCLOSE_WRITEy usarecord_dedup: una reescritura sin cambios ni ensucia el diario ni despierta el bus.
Fase 4 — Formato y flujo .swm ✅
Swmde/serialización YAML estable (roundtrip).verify_schema(estructura + invariantes por mutación) yverify_base(distro_version + pins) conBaseRef/BaseCompat.apply_config_edit(hunks-/+con búsqueda exacta, no-fuzz) yapply_file_drop(base64 inline + verificación BLAKE3 vscontent_hash).- Bridge
Mutation::SourcePatch→Recipe+build+ hidratación. - CLI:
hammer apply [--prefix DIR] [--base-ref base.json],hammer swm-verify,hammer export --journal DIR > out.swm. patch_url/content_urlremotos.hammer_build::download(curl, sólo fuera del sandbox; aceptafile://para tests offline).build_source_patchdescargapatch_urlalswm-recipes/<commit>.patchantes de compilar;hammer applydescargacontent_urly verifica BLAKE3 antes de escribir (hash erróneo ⇒ nada tocado). Su integridad la cubrecontent_hash(file_drop) y el build reproducible +expected_hash(patch).- Provenance en
export: mapa artefacto→receta vía sidecar.hammer/recipe.tomlquehammer-build::buildescribe dentro del artefacto antes de sellar.hammer exportagrupa eventos porartifact_hashy emite UNsource_patchpor grupo cuya receta sea recuperable; lo demás cae al fallbackfile_drop. Sourcegitmodelado;tarballcae a file_drop con warning (pendiente extender SourcePatch). - Firma
signature(ed25519) yTrustStorelocal.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, yhammer 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.rsparaconfig_edit+file_drop.source_patchreusa el camino de Fase 0/1 (gated enHAMMER_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, yhammer ctl <line>que escribe al FIFO con error claro si no existe./run/agent.sock(JSON-líneas) con handshakeHello/Welcome, auth viaSO_PEERCREDyCapsPolicypor UID.- Comandos
Compile/Inject/Query/Init; eventosWelcome/BuildReady/BuildFailed/Injected/QueryResult/InitAck/Modified/Crashed/Error. - EventBus in-process: el watcher publica
Modifiedy 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]]poruid/gid(primera que casa gana),caps_for(uid,gid).bus::policy_from_configla enchufa; sin fichero o con fichero inválido, cae a la política por-UID por defecto (avisando). Ejemplo enexamples/agent-caps.toml. Eldefault_policyhardcoded queda como fallback. CRASHEDreal (requiere supervisión de servicios, que llega con el init propio del track posterior).BuildFailed.log_tailcon cola real del lab.Sandbox::runhace tee de stdout/stderr (sigue viéndose en vivo) y retiene las últimas 50 líneas; al fallar una fase devuelveBuildFailure { reason, log_tail }envuelto enError::Other. El bus lo recupera porBuildFailure::from_error(downcast) y separareasondel 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 porcrates/hammerd/tests/bus_e2e.rs: handshake con peer creds, gatingno_cap,Query,Modifiedfan-out,Init→FIFO. UnCompilereal reusa el camino de Fase 0/1 (gated enHAMMER_NETWORK_TESTS).
Fase 6 — Integración de la IA ✅
- Crate
hammer-agentcon tres piezas: -AgentClient: cliente síncrono del bus (handshake,compile/inject/query/initbloqueantes, drenado de eventos asíncronos). -IntentTranslator(trait) +MockTranslatorcargado desde unIntentCatalogYAML (intent →.swmpre-armado). La integración con un LLM real se enchufa detrás del mismo trait sin tocar el bucle. -Orchestratorconrun(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::protopara quehammerdyhammer-agentlos 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.
ClaudeTranslatordetrás de la featurellm-claude, conureq + rustls. Modelo por defectoclaude-opus-4-8, adaptive thinking +effort=high. TraitHttpClientinyectable 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 extraerDT_NEEDED. - Bucle de auto-reparación (cliente reacciona a
Crashedcon un nuevo plan).Orchestrator::run_with_repair(intent, &RepairPolicy)drena async events del bus tras cada apply; si llegaCrashed, formatea una nueva intención y reentra hastamax_attempts. ElProposalfinal lleva elrepair_chaincompleto 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
commithumano. ✅ Demostrado porhammer aiconMockTranslator: intent →.swm→ mutaciones aplicadas →Proposalconoverlay_id+ checks. El humano decidecommit/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 (toolchain
semilla
zig/musl-cross-makesellado por hash), Stage 1 (userland mínimo cross-compilado: musl + busybox/toybox + init +hammerd), Stage 2 (rebuild nativo dentro del rootfs y diff de hashes ⇒ auto-alojamiento bit-reproducible). ⏭️ Primer paso del track. - Reemplazar el init de Alpine por tu init (bus por pipes nativo) — habilita el
CRASHEDreal que la Fase 5 dejó diferido. - Userland propio (busybox/toybox a elección),
/etc/service/*propios. - Decidir particionado/montaje de
/storey/var/lib/hammer. - Log de transparencia para compartir en comunidad (SDD 09 §4): el
bootstrap.jsonde SDD 11 §4 es su embrión. - Mini-lenguaje de consulta del sistema para la IA (SDD 08 §6).
Estado actual
- ✅ Fases 0–6 cerradas sobre Alpine: build determinista → hidratación → overlay
(con anidación LIFO) → diario (con
opCreate/Edit/Delete) →.swmfirmado → bus de agente → bucle agéntico con IA (mock + Claude real trasllm-claude). - ✅ El bucle completo está validado: una intención en lenguaje natural produce un cambio
probado en overlay, presentado para
commithumano. - ✅ 214 tests verdes en el workspace (1 e2e de overlayfs gated en
HAMMER_OVERLAY_TESTS; los de red enHAMMER_NETWORK_TESTS). - ⏳ Único ítem de las fases diferido:
CRASHEDreal (necesita supervisión de servicios → init propio del track posterior). - ⏭️ Siguiente: arrancar el track posterior (distro propia). Primer paso natural sin
bloquear nada: el bootstrap from-scratch Stage 0/1 con
zig/musl-cross-make.
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.