Cuarta pieza del toolchain hammer-from-source (variante b, SDD 11 §7.2b): bubblewrap, el
sandbox del propio lab. Binario estático ⇒ swap de archivo sobre /toolchain/usr/bin/bwrap.
Como herramienta del toolchain (no input del 4/4) no necesita casar byte-a-byte con Alpine,
sólo aislar igual.
Tres piezas:
- recipes/libcap.toml (2.78): dep obligatoria de bwrap; el toolchain Alpine no trae el -dev
(libcap.a / sys/capability.h). Build estático musl con zig cc, sin patches (es tool, no input).
- materialización de build-deps (hammer-build): deps.build ahora se CONSTRUYE recursivamente
(build() llama build() por cada dep) y cada artefacto sellado se apila como capa --overlay-src
bajo el rootfs del sandbox, dejando usr/{include,lib,lib/pkgconfig} en /usr. pkgconf y zig cc
las hallan sin plumbing de flags. Recetas sin deps: sandbox byte-igual (baseline intacto).
Tests nuevos: no_deps_emits_single_overlay_src, deps_stack_as_overlay_layers_under_rootfs.
- recipes/bwrap.toml (0.11.0): el toolchain no trae meson/ninja/python, así que bypaseamos meson
compilando los 4 .c de bubblewrap directo con zig cc (+config.h trivial). deps.build=["libcap"].
Validación host fuerte: hammer-bwrap es estático, corre --version y sandboxea, y musl rebuildeó
BYTE-IDÉNTICO usándolo de sandbox (bisección). Expuesto con SWAP_BWRAP=1. Tests verdes.
Pendiente: corrida in-VM acumulando swaps para el sello ✓ REPRODUCIBLE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tercera pieza del toolchain hammer-from-source (variante b, SDD 11 §7.2b): los headers
UAPI del kernel 6.16.12 que musl/busybox #include, hoy tomados del paquete linux-headers
de Alpine.
- recipes/linux-headers.toml: `make headers` (no `headers_install` — su rsync final falta
en el toolchain hermético) + unifdef vía HOSTCC=zig cc; .tar.gz (busybox-tar lo
descomprime sin xz). Sella los 13 subdirs kernel-owned de usr/include.
- assemble_builder: el swap ahora soporta rel_path de DIRECTORIO (reemplaza el árbol
entero: remove + copy), no sólo binarios. Cubierto por test nuevo
(builder_rootfs_swaps_toolchain_header_tree_from_source, incl. borrado de huérfanos).
- recipes/linux-headers-alpine-compat.patch: el paquete de Alpine no es el `make headers`
crudo. Aporta content-pinned 5 archivos que difieren de la salida vainilla; 3 son
REQUERIDOS (scsi/{scsi,scsi_ioctl,sg}.h — legacy userspace, NO UAPI) porque el applet
`eject` de busybox los incluye y sin ellos no compila. install los pisa sobre /out y
limpia el junk (.cmd/Makefile/headers_check.pl/drm) que `cp -a` arrastra.
- scripts/selfhost-verify.sh: SWAP_LINUX_HEADERS=1 construye la receta y emite un --swap
por subdir kernel-owned (auto-derivado del artefacto sellado).
Criterio de éxito fuerte: `diff -r` del header-tree de hammer contra el de Alpine = VACÍO
⇒ el toolchain swapeado es byte-idéntico al baseline ⇒ reproducibilidad por construcción.
Ensamblado end-to-end en host OK (13 swaps, musl bits/sys intactos). 133 tests verdes.
Pendiente: corrida in-VM para el sello ✓ REPRODUCIBLE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
KVM=1 MEM=24576 SWAP_MAKE=1 SWAP_BUSYBOX=1 ./scripts/selfhost-verify.sh en libre:
la VM reconstruyó los 4/4 con el /toolchain hammerizado (make fbad44ac… +
busybox 56664d70… pisando los de Alpine, busybox compilándose a sí mismo con
hammer-busybox de shell). of_tree(stage1')==EXPECT_REF 9adefb82… ⇒
✓ REPRODUCIBLE: stage1' == stage1, DRIVER_RC=0 (~54 min, arje-zero cu=1 in-VM).
La procedencia del builder deja de ser "todo Alpine" y el auto-alojamiento
sigue bit-a-bit. Siguiente pieza: linux-headers → bwrap → rust/llvm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Variante (b) pieza 2: el sh+coreutils que el build usa del /toolchain. El /toolchain
de Alpine es mixto — busybox para sh/sed/grep/awk/tar/find (symlinks → /bin/busybox),
GNU coreutils para cp/mkdir/install. El swap monta el busybox estático de hammer
sobre /toolchain/bin/busybox: los applets busybox-backed pasan a usarlo, coreutils
GNU queda intacto. Sin código nuevo — el mecanismo --swap ya soporta rel_path
(--swap busybox=<hash>:bin/busybox).
Validado en host (sin VM): con make+busybox de hammer swapeados, los 4/4 componentes
construyen — incl. busybox compilándose a sí mismo con hammer-busybox de shell — y el
of_tree del rootfs ensamblado da 9adefb82 (el baseline determinista). Las piezas 1 y 2
son reproducibilidad-neutrales: el toolchain es intercambiable sin perturbar el byte-output.
selfhost-verify.sh: SWAP_BUSYBOX=1 (paralelo a SWAP_MAKE). Docs 10/11 actualizadas.
Pendiente: verify in-VM con los swaps para el ✓ REPRODUCIBLE end-to-end.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verificado: arje-zero con CARGO_BUILD_JOBS=1 y TODAS las CPUs del host sale
byte-idéntico al build serial ⇒ el jobserver serializa el backend paralelo de
rustc/LLVM (ThinLTO) independientemente del nº de CPUs. La pieza que faltaba para
la reproducibilidad host↔VM (codegen-units=1 no bastaba).
- EXPECT_REF re-baseado: 0039b2b9 (build paralelo no-fiable) → 9adefb82 (serial,
determinista, = lo que produce la VM con 1 vCPU).
- Baseline del host refrescado: store/ re-sellado con el arje-zero determinista.
- docs/10-roadmap.md: causa raíz documentada (make inocente; arje-zero/ThinLTO).
Pendiente: re-correr el verify in-VM con el fix para cerrar el ✓ REPRODUCIBLE y
validar de paso la pieza 1 (swap del make).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Segundo paso del auto-alojamiento *puro* (SDD 11 §7.2b): tras sellar GNU make
4.4.1 desde fuente (commit 749ea9e), ahora el builder puede *usarlo*. El swap
reemplaza una a una las piezas que el builder toma de Alpine por recetas hammer,
con Stage 2 reverificando que el byte-output no cambia.
- `BuilderSpec.swaps: Vec<ToolchainSwap{name,artifact,rel_path}>`: monta el binario
sellado sobre el path Alpine en /toolchain (estático musl ⇒ sin shim del loader).
- `swaps_digest` (ordenado por nombre) entra al hash lógico del builder ⇒ la
procedencia deja de ser "todo Alpine" y se vuelve auditable para el log de
transparencia. Vacío ⇒ digest "" (compat hacia atrás: builder pura-Alpine
conserva su hash previo).
- CLI: `hammer bootstrap builder --swap make=<hash>[:rel_path]` (repetible;
rel_path por defecto usr/bin/<name>).
- selfhost-verify.sh: opt-in `SWAP_MAKE=1` (construye recipes/make.toml y lo
swapea) + `SWAPS="name=hash …"` para swaps extra. Default off ⇒ corrida
pura-Alpine idéntica a la baseline conocida-buena.
Validado en el host contra el store real: pura `b3:8a370f5d…` vs swapped
`b3:e7e2282c…`, y /toolchain/usr/bin/make queda hardlinkeado al artefacto
fbad44ac… (ELF estático, no el dinámico de Alpine). 3 tests nuevos
(swap aplica, hash cambia, error si falta el binario). 37 tests verdes.
Pendiente: correr Stage 2 in-VM con el swap y confirmar que of_tree(stage1') == ref
(el make hammer compila los 4/4 idéntico al de Alpine ⇒ toolchain intercambiable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Arranca el auto-alojamiento *puro* (SDD 11 §7.2b): reemplazar una a una las
piezas que el builder toma de Alpine (/toolchain) por recetas hammer desde
fuente, con Stage 2 reverificando cada paso.
make es la pieza base de toda receta autotools. Build estático musl con zig cc
(mismo camino que grep: tarball release con configure → AutoconfReady), sellado
b3:fbad44ac… y reproducible bit-a-bit (dos builds en stores distintos ⇒ árbol
idéntico). Pendiente: swap al /toolchain del builder + Stage 2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cierra la deriva entre los docs y lo ya probado/commiteado (818c157):
el rebuild in-rootfs corrió end-to-end (host↔VM) y of_tree(stage1')=
0039b2b9… igualó la referencia ⇒ ✓ REPRODUCIBLE.
- roadmap §track posterior: Stage 2 ☐→✅; "Siguiente" reapunta al
auto-alojamiento puro (variante b) + ítems Stage 1 (bus único, atestación).
- SDD 11 §5/CLI: marcadores stage2 ◑→✅; §7 (intro/7.4/8) el rebuild in-VM
deja de ser "lo que falta" y queda la variante (b) como corte pleno.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Desde la consola del Stage 1 booteado: kill $(pidof hammerd) → arje-zero lo
supervisa de verdad:
Ente disuelto label=hammerd status=Killed(SIGTERM)
Restart programado label=hammerd delay_ms=200
Ente encarnado label=hammerd pid=Some(Pid(64))
Detecta la muerte con su ExitStatus, hace backoff y re-encarna hammerd (pid
59→64). Es la supervisión real del init propio (ADR 0007) que la Fase 5 había
diferido. Runbook §8 y roadmap actualizados. Pendiente: B.2 (exponerlo a la
capa de IA por agent.sock) y atestación A1/A2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
La corrida real cierra el lazo: con el build nativo crt-static (cargo rustc),
arje-zero y hammerd salen statically-linked (ET_EXEC, sin DT_NEEDED), el rootfs
se sella, y el boot en QEMU (-cpu Broadwell) levanta el sistema:
arje-zero (PID 1) → carga+valida /ente/seed.card.json → bucle primordial →
encarna hammerd (Pid 59) + console-getty (Pid 60) → shell en consola;
hammerd levanta su watcher fanotify y el agent.sock.
Runbook §8: bitácora completa (todos los fixes: gcc/HOSTCC, GNU-ld, rust en el
lab, triple nativo, wrapper saneador, vendoring condicional, AVX→Broadwell,
dinámico→crt-static vía cargo rustc). Roadmap: Stage 1 ◑→✅.
Pendiente: demo del CRASHED real (kill hammerd → restart con backoff; la
supervisión Restart ya corre), bus único (B.2), atestación (A1/A2).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reemplaza el init provisional de busybox por arje-zero como PID 1 — el paso
"init real" que entrega el CRASHED real (Fase 5 diferida, ADR 0007).
- STAGE1_COMPONENTS += arje-zero (la receta puente ya lo construía)
- assemble_rootfs: genera /ente/seed.card.json (template autocontenido), crea
/sbin/init -> /usr/bin/arje-zero, y los mount points que arje monta (incl.
/ente, /var/lib/hammer, /sys/fs/cgroup, /dev/pts, /dev/shm). Se retira el inittab.
- la seed declara hammerd como Payload::Native Restart (su on_death = el CRASHED
real) + console-getty supervisada; ULIDs fijos ⇒ RootfsHash reproducible (v2)
- la seed se VERIFICÓ contra el tipo real card_core::Card (from_json + validate
pasan), no sólo como JSON — evidencia, no aserción
- +2 tests (ensamblado con arje init + seed válida); 21 verdes en el crate
Falta para cerrar Stage 1: bus único (B.2) y atestación (A1/A2). Boot real en VM.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cierra el userland mínimo salvo el init definitivo. hammerd entra como tercer
componente de stage1, construido por el lab vía BuildSys::Cargo + vendoring:
- recipes/hammerd.toml: source git del repo hammer a commit fijado (ADR 0006),
flags=["-p","hammerd"] para construir sólo ese binario del workspace, estático
con zig cc; las deps se vendorean en el fetch (Lote 6) ⇒ build --offline
- STAGE1_COMPONENTS += hammerd; el inittab provisional lo arranca como servicio
respawn (/usr/bin/hammerd) tras montar los pseudo-FS
- el test de recetas del repo ahora valida también hammerd.toml (parse + hash)
Falta sólo sustituir el init provisional de busybox por arje (ADR 0007) para el
CRASHED real. El cross-compile se valida en la VM.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cross-compila musl + busybox con la semilla de Stage 0 (no el zig del host),
ensambla un rootfs FHS por hardlink y lo sella. CLI: `hammer bootstrap stage1`.
- seed_toolchain_dir(store, hash, kind): primitivo sin SeedSpec, para que Stage 1
resuelva el toolchain desde (seed_hash, kind) sin re-pasar url/sha256
- RootfsHash = hash de CONTENIDO (componentes ordenados + init), no del árbol en
disco ⇒ reproducible aunque difieran inodes/timestamps
- assemble_rootfs: esqueleto FHS + hydrate de cada componente + /etc/inittab
provisional (busybox init); descarta el sidecar .hammer del rootfs
- idempotente (no reensambla si el rootfs ya está sellado); anota línea stage 1
- hammerd (receta Cargo) y arje quedan como slots pendientes (plan M2 / ADR 0007)
+3 tests (rootfs_hash determinista/sensible, assemble_rootfs, recetas del repo
parsean y hashean). Workspace verde; el cross-compile real se valida en la VM.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cablea el Stage 0 del bootstrap a la CLI: subcomando `bootstrap stage0
--url --sha256 --version [--seed zig|musl-cross-make]` que abre el store,
arma el SeedSpec y llama a hammer_bootstrap::stage0, imprimiendo el b3 del
artefacto sellado.
- stage0 ahora limpia el staging `.bootstrap-tmp` pase lo que pase (extraído
stage0_into): un sha256 erróneo ya no deja el tarball a medio descargar bajo
el store. Test reforzado para afirmarlo.
- e2e de CLL `bootstrap_cli.rs`: tarball falso vía file:// → sella + imprime
hash, idempotente, sha erróneo falla sin sellar. Sin red.
Docs SDD 11 §5 y roadmap marcan Stage 0 (lib+CLI) hecho; Stage 1/2 pendientes.
Workspace 219 verde.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Coordina con tawasuyu/03_ukupacha/arje/PLAN-ATESTACION-Y-HAMMER.md:
- arje (init from-scratch ya existente, PID1 + supervisión real) cubre el
CRASHED diferido de la Fase 5 en vez de escribir init nuevo.
- frontera: PID1 fino, un solo bus (arje-bus + hammer-core::proto), CAS
unificado en BLAKE3, confianza en capas (procedencia hammer + atestación
arje; el expected_hash del .swm = el blake3 que arje atesta).
- caveat: la cage glibc es del mundo hammer/Linux, fuera de PID1.
Puntero añadido en docs/10-roadmap.md §Track posterior.
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>
Sincroniza las cabeceras con la realidad: tras cerrar la op del watcher (Fase 3)
y la anidación LIFO (Fase 2), ningún ítem de las fases 0–6 queda abierto salvo
CRASHED real, explícitamente diferido al track posterior. Reescribe "Estado
actual" (seguía describiendo el día 1) con el bucle agéntico validado, 214 tests
verdes y el siguiente paso: bootstrap from-scratch del track posterior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Un `try` sobre un target ya cubierto apila un overlay nuevo: el kernel toma el
merged view de la capa inferior como lowerdir. Para que esto sea seguro y no un
footgun:
- fresh_id añade un `seq` atómico por proceso (`<ts>-<pid>-<seq>` zero-padded),
eliminando la colisión de ids cuando un orquestador apila varios overlays en
el mismo segundo — el caso real de la anidación.
- stack_key define un orden de apilamiento total y determinista (created_at,
desempatado por id).
- blocking_overlays detecta capas más jóvenes que solapan targets (igualdad o
ancestro de path). commit/discard fallan con Error::Shadowed si existen,
exigiendo resolver LIFO de arriba hacia abajo — antes se desmontaba la capa
equivocada del target compartido en silencio.
Tests: 6 unit del guard (disjuntos / solape / ancestro / desempate / commit y
discard rechazados) sin privilegios; e2e overlay_nested_stack_inside_userns
prueba el stack real (capa 2 ve la 1, rechazo LIFO, fusión arriba→abajo),
gated en HAMMER_OVERLAY_TESTS. Docs 04 y roadmap actualizados.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CLOSE_WRITE ya no se materializa siempre como Replace. classify_op deriva:
- Delete: el fd resuelve a " (deleted)" (sufijo que ahora recortamos del path
en vez de colarlo al diario; content_hash queda None).
- Edit: el diario ya tenía un evento previo no-Delete para el path.
- 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 queda diferida: nix 0.30 no parsea
los info-records de FID y el modo FID perdería el fd que hashea el contenido.
classify_op se extrae como función libre para probarla sin CAP_SYS_ADMIN; 4
tests unitarios nuevos. Roadmap Fase 3 actualizado.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El campo content_hash (ya existía en MutationEvent) ahora se puebla y se
usa para no ensuciar el diario con reescrituras sin cambios.
hammer-journal:
- content_hash_of(bytes) / hash_file(path): blake3 plano (estilo b3sum,
"b3:<hex>"), distinto del of_inputs de artefactos — aquí la pregunta es
"¿cambió el archivo?".
- last_for_path(path): último evento de un path.
- record_dedup(ev): omite el evento si deja el archivo idéntico al último
estado de ese path (content_hash igual) o si es un Delete sobre algo ya
borrado. Devuelve si escribió o no. +7 tests.
hammerd watcher: hashea cada CLOSE_WRITE y usa record_dedup; una
reescritura idéntica ni registra ni emite Modified al bus.
22 binarios de test verdes, sin warnings nuevos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cierra el último hueco de "sólo inline": ahora un .swm puede referenciar
el patch y el contenido de un file_drop por URL.
- hammer-build/src/download.rs: fetch_url_bytes / fetch_url_to_file (curl,
sólo fuera del sandbox; acepta file:// para tests offline). 3 tests.
- swm_bridge::build_source_patch: descarga patch_url a
swm-recipes/<commit>.patch antes de compilar (ya no lo rechaza). Test
reescrito con file://.
- CLI apply: file_drop con content_url descarga y verifica BLAKE3 ANTES de
escribir; hash erróneo ⇒ nada tocado (verificado e2e).
Integridad: content_hash (file_drop) y build reproducible + expected_hash
(patch). 22 binarios de test verdes; e2e por file:// (apply + caso de
hash inválido que no escribe).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cuando una fase de build falla, el lab ahora conserva las últimas 50
líneas del log y las entrega a la IA por el bus.
- sandbox.rs: BuildFailure { reason, log_tail } (impl Error), wrap en
Error::Other. Sandbox::run hace tee de stdout/stderr al padre (streaming
en vivo intacto) y a un ring buffer acotado; al fallar, devuelve la cola
en log_tail. spawn_pump + push_tail con tests.
- BuildFailure::from_error(&Error) recupera la struct por downcast.
- bus.rs run_compile: separa reason del log_tail real del compilador en
vez de mandar e.to_string() y log_tail=None.
Tests: push_tail (ring), roundtrip por downcast, none para errores planos.
Firmas de build/build_source_patch sin cambios. 22 binarios verdes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sustituye la política hardcodeada por una declarativa (SDD 07 §4).
hammer-core/src/caps.rs:
- AgentCapsConfig { default, rule[] }; CapRule { uid?, gid?, caps }.
- caps_for(uid, gid): primera regla que casa (todos los campos
declarados deben coincidir) o default. load(path) → Ok(None) si falta.
- 8 tests: orden de reglas, match uid+gid, fallback, regla vacía ignorada.
hammerd:
- bus::policy_from_config(cfg) construye la CapsPolicy desde la config.
- main: --agent-caps (default /etc/hammer/agent-caps.toml). Con fichero
usa la config; sin fichero o con fichero inválido cae a default_policy
(avisando). +1 test del policy_from_config.
examples/agent-caps.toml: plantilla comentada.
22 binarios de test verdes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cierra el item de firma del modelo de confianza (SDD 09 §3, §5).
hammer-core/src/sign.rs:
- KeyPair: genera (getrandom), carga/serializa claves base64, escribe
<name>.ed25519 (0600) + <name>.ed25519.pub, y firma un Swm.
- TrustStore::load(dir): lee *.ed25519.pub; dir ausente ⇒ store vacío.
- Swm::verify_signature(&trust) → SigStatus {Trusted|UnknownKey|BadSig|
Unsigned}. Bytes firmados = JSON canónico de (swm_version, base,
mutations) sin la firma; deterministas (pins en BTreeMap).
- 8 tests: sign/verify, tamper, wrong-key, roundtrip en disco, dir ausente.
CLI:
- hammer keygen <name> [--out DIR]
- hammer swm-sign <file> --key K [--by N] [-o OUT]
- hammer swm-verify <file> --trust DIR (reporta trusted/unknown-key/
bad-sig/unsigned; bad-sig aborta, lo demás informa).
La firma da autoría + integridad pero NO autoriza promover: apply sigue
reproduciendo y comparando. Verificado e2e por el CLI (keygen→sign→verify
en los 4 estados). 22 binarios de test verdes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hasta ahora `hammer export` emitía sólo `file_drop` con `content_b64`.
Eso reproducía byte-a-byte pero perdía la receta: el receptor obtenía
binarios opacos, sin manera de auditarlos ni de recompilarlos desde
fuente. Esta fase cierra el hueco para los artefactos producidos por
`hammer-build::build`.
Mecanismo:
- `Store` reserva `.hammer/recipe.toml` (`RECIPE_SIDECAR_REL`) dentro
de cada artefacto. `hammer-build::build` lo escribe antes de sellar,
así queda inmutable junto con el árbol.
- `Store::recipe_for_dir` / `recipe_for_hash` lo leen de vuelta. Si el
artefacto es viejo y no trae sidecar, devuelven `None` sin fallar.
- `Recipe::to_toml` (nuevo) hace el roundtrip.
Lógica del export (función pura `build_export_mutations`):
1. Aplica semánticas de Delete: invalida estados previos del mismo path.
2. Particiona los eventos sobrevivientes en (a) los que tienen
`artifact_hash` con receta sidecar y (b) el resto.
3. Por cada grupo de (a) emite UN `source_patch` con repo+commit+build
de la receta y `expected_hash = artifact_hash`. El `target_bin` es
el primer path alfabético del grupo; el receptor hidrata el árbol
completo al aplicar.
4. Patches de la receta se concatenan inline en el `source_patch.patch`.
5. Los eventos del bucket (b) caen al fallback `file_drop` con
`content_b64 + content_hash` (orden alfabético).
Limitaciones explícitas:
- `SourcePatch` sólo modela `Source::Git`. Recetas con `tarball` se
reportan por stderr y caen a file_drop. Extender el SWM para
tarballs es trabajo aparte.
- Si la receta tiene patches pero alguno no se puede leer, no se
inlina; `expected_hash` sigue siendo el gate de integridad para
detectar la divergencia.
CLI: el subcomando `Export` ahora recibe `--store` para resolver
artefactos. Defaults igual que antes (`/store`).
Tests (12 nuevos):
- hammer-core (5): `Recipe::to_toml` roundtrip; `Store::recipe_for_dir`
sin/con sidecar; `Store::recipe_for_hash` por prefijo + hash inexistente.
- hammer-cli (7): export sin store; semánticas de Delete; hydrate con
sidecar emite source_patch agrupando dos archivos; hydrate sin sidecar
cae a file_drop contando `missing_recipe`; mix trazable + external;
path ilegible sólo warning.
`ClaudeTranslator` implementa el trait `IntentTranslator` igual que el
`MockTranslator`, así que el `Orchestrator` no cambia: se traduce una
intención NL a un `.swm` válido vía `/v1/messages`.
Diseño:
- Síncrono (ureq + rustls), consistente con `AgentClient`. Tokio
entraría sólo si el resto del crate lo pidiera.
- Trait `HttpClient` inyectable ⇒ tests sin red contra una fake que
captura el request y devuelve un body pre-armado.
- Modelo por defecto: `claude-opus-4-8` (Opus 4.8, el más capaz al día
de hoy). Adaptive thinking + `effort=high` por defecto. Override por
env (`HAMMER_LLM_MODEL`, `HAMMER_LLM_EFFORT`, `HAMMER_LLM_BASE_URL`).
- System prompt documenta el shape exacto del `.swm` (4 variantes de
mutación) y obliga JSON puro. Parseamos con `serde_json::from_str::
<Swm>` + `verify_schema()` como gate adicional. Manejamos `refusal`,
error envelopes de la API y code-fence markdown.
Activación:
- Sin feature: el módulo declara los tipos pero `ClaudeTranslator::new`
está bajo cfg. El binario compila sin red.
- Con `--features llm-claude`: trae `ureq` con rustls, y la CLI activa
`hammer ai --llm`.
CLI:
- `hammer ai` gana `--llm` (mutuamente excluyente con `--catalog`).
Refactor de `run_ai` en helpers `run_with_mock_translator` /
`run_with_llm_translator` para mantener legible el dispatch.
- Sin la feature, `--llm` falla con un mensaje claro pidiendo
recompilar.
Tests (7 unit): happy path con verificación de URL/headers/body,
unwrap de markdown, `refusal`, error envelope, SWM mal formado,
verify_schema, helpers de strip_code_fence.
`Orchestrator::run_with_repair(intent, &policy)` corre el ciclo
plan→try→apply→wait_for_crash→re-plan hasta `max_attempts` o
estabilización (sin Crashed en `crash_window`). Cuando llega un Crashed
asíncrono por el bus, la política convierte `(service, code)` en una
nueva intención (`default_crash_intent` por defecto) y reentra.
Diseño:
- `RepairPolicy { max_attempts, crash_window, event_sock, format_intent }`
acota el blast-radius desde el caller; la IA no decide reintentar.
- `Proposal` gana `repair_chain: Vec<RepairAttempt>` con la cadena de
intentos (intent, overlay_id, triggered_crash). La IA NUNCA promueve
al FHS; el humano lee el chain y decide commit/discard.
- `wait_for_crash` abre un AgentClient sólo para drenar async events;
reusa toda la maquinaria síncrona del cliente.
CLI: `hammer ai --repair-max-attempts N --repair-window-ms M`. Sin el
flag, `run` corre una sola vez (compatible hacia atrás).
Tests (3): stub bus multi-conexión que pre-programa eventos por
conexión. Cubre reacción a Crashed, cap por max_attempts con crashes
persistentes, y modo sin socket (una sola corrida).
Forma `kind:value`: `bin`, `file`, `pin`, `service`, `depends`. El
evaluador acepta un EvalContext con BaseRef, fs_root y path_env para
re-rootear contra un overlay o prefix sin tocar el FHS real. `depends:`
implementa un parser ELF64 LE mínimo que recorre PT_DYNAMIC para extraer
DT_NEEDED, suficiente para los binarios estáticos+dinámicos del lab.
- hammer-core::query: parse(expr) + eval(term, ctx) + eval_str(expr, ctx).
- proto: Command::Query gana `expr: Option<String>`.
- AgentClient::query_expr: equivalente a query_file pero por el bus.
- hammerd: maneja `what="expr"` invocando query::eval_str.
- CLI: `hammer query <expr> [--base-ref] [--fs-root]` evalúa en proceso.
Tests: 18 unit en `hammer-core::query::tests` (parse, eval con fs_root,
ELF gated en `HAMMER_HOST_ELF_TESTS`), 1 e2e en `hammerd::bus_e2e`
(query_expr_evaluates_against_host_path).
- proto: mover hammerd::proto a hammer-core::proto para que hammerd y hammer-agent
compartan los tipos del bus sin duplicar.
- hammer-agent (crate nuevo):
* client: AgentClient síncrono. Handshake hello/welcome; compile/inject/query/init
bloqueantes con timeout; reader thread interno demultiplexa async events (Modified/
Crashed) en una cola que el caller drena vía drain_async/next_async.
* translator: trait IntentTranslator + MockTranslator (HashMap<intent, Swm>) +
IntentCatalog YAML (swm_inline o swm_path). El traductor LLM real se enchufa
detrás del mismo trait sin cambios al orquestador.
* orchestrator: Orchestrator::run(intent) -> Proposal con plan -> schema -> base ->
try (overlay|prefix) -> apply (config_edit/file_drop con hammer-core::apply,
source_patch via bus opcional) -> verify (spot-checks) -> propose. Devuelve
overlay_id (para `hammer commit`) o prefix usado.
- hammer-cli: subcomando `hammer ai <intent> --catalog F [--prefix DIR --base-ref F
--bus SOCK --state-root DIR]`. Imprime el Proposal y el siguiente paso humano.
- Tests:
* 10 unit (translator + catalog + orchestrator).
* 3 e2e del bucle agéntico (intent -> archivos esperados bajo un prefix tmp).
* 1 e2e del cliente contra un stub bus (handshake + Compile -> BuildReady +
Modified asíncrono), sin depender de hammerd ni del lab.
- Docs: SDD 08 actualizado con el API del crate; roadmap marca lo cerrado y lo
pendiente (LLM real, lenguaje de consulta, bucle de auto-reparación con Crashed).
- proto: tipos Command/Event con serde tag "t", Hello/Welcome handshake, RecipeInline
reducida para COMPILE, Cap enum (Query/Compile/Inject/InjectReal/Init) y
required_for() para gating por comando.
- bus: serve_agent_bus listens en UnixListener, autenticación SO_PEERCRED por
conexión via nix::sys::socket::PeerCredentials, una conexión = reader thread +
writer thread + bus-forwarder thread + worker spawn por COMPILE. Dispatch a
hammer-build (Compile), find_by_hash+run_hydrate (Inject), stat (Query/file),
store.find_by_hash (Query/artifact) y send_to_fifo (Init).
- events: EventBus in-process (Arc<Mutex<Vec<Sender>>>), suscripción por conexión
y purga perezosa de subs muertos en publish.
- control: ensure_fifo (mkfifo idempotente, rechaza non-FIFO), run_reader (relog
por línea, reabre al EOF), send_to_fifo (bloqueante si no hay reader).
- watcher: nuevo start_with_events que clona el EventBus; cada MutationEvent
registrado se re-emite como Event::Modified a las conexiones abiertas.
- hammerd::main: orquesta FIFO + watcher + bus en threads; --no-watcher para
dev sin CAP_SYS_ADMIN.
- hammer-cli: hammer ctl <line> [--fifo PATH] escribe al FIFO con error claro si
no existe; reemplaza el stub "[fase 5 pendiente]".
- Tests:
* 14 unit tests nuevos en hammerd (proto/bus/events/control).
* 5 e2e en crates/hammerd/tests/bus_e2e.rs: handshake con peer creds, gating
no_cap, Query/file, Modified fan-out vía bus, Init -> FIFO end-to-end.
- Docs: docs/10-roadmap.md actualizado con lo cerrado y lo pendiente (policy
declarativa, CRASHED real con supervisor, log_tail en BuildFailed).
Nuevo crate hammer-journal y watcher fanotify en hammerd. Cierra el contrato
del SDD 05 §3 ("toda mutación trazable/opaca registrada y consultable") y
liquida la deuda Fase 2 sobre el hook al diario en commit.
hammer-journal:
- MutationEvent (op, path, by, content_hash?, note?), MutationOp
Create/Replace/Edit/Delete, Source enum tagged HammerHydrate /
HammerCommit / External (distingue trazable de opaco para `export`).
- Journal: record append-only + sync_data, read_all/tail/follow (offset),
resilencia a líneas malformadas (kernel-panic mid-write se ignora).
- RFC 3339 UTC sin chrono (Hinnant civil_from_days, válido para todo i64).
- 7 unit tests cubren append/read/tail/follow/malformed/serde/timestamp.
Watcher (hammerd):
- nix::sys::fanotify con FAN_CLASS_NOTIF + FAN_CLOSE_WRITE +
FAN_EVENT_ON_CHILD sobre /bin /sbin /usr/bin /usr/sbin /lib /usr/lib /etc.
- Resuelve path por readlink(/proc/self/fd/<n>) — sin FAN_REPORT_DFID_NAME
para Fase 3 v1; el refinamiento Create/Edit con metadata viene después.
- Filtro: hammer_overlay::status(state_root) → ignora paths bajo un overlay
activo (el ruido del experimento no entra hasta el commit).
- Sin CAP_SYS_ADMIN, fanotify_init falla; hammerd lo reporta y sigue
durmiendo (preparado para que Fase 5 monte el bus encima).
- 3 unit tests sobre defaults/filtros/uid lookup.
CLI:
- hammer journal [--dir] [--tail N] [--follow] [--format pretty|json].
- hammer commit registra cada archivo promocionado/removido vía
commit_with_journal; --no-journal opt-out.
Hook overlay::commit:
- commit_with_journal(id, root, journal): para cada copied/removed emite un
MutationEvent con Source::HammerCommit { overlay }. Test directo del hook
sin overlayfs (lógica pura) más el e2e existente que ya cubre los mounts.
57 tests workspace, 0 fallos.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Nuevo crate hammer-overlay + integración al CLI. Diseño:
- API: try_overlay(targets, state_root) → OverlayId, status, discard, commit.
- Layout: <state>/<id>/{state.json, mounts/<slug>/{upper,work}}. El manifiesto
permite a `status` enumerar overlays sin estado en memoria.
- commit: desmonta primero, luego cp -a upper→lower por archivo, procesa
whiteouts (char dev 0/0) como remove en el lower. Sin diario aún (Fase 3).
- discard: umount LIFO + rm del state. Idempotente sobre `not mounted`.
- Privilegios: la librería no asume; emite `mount`/`umount` y deja que el
caller los tenga. CLI sin sudo falla con mensaje claro. El daemon de
Fase 5 los obtendrá desde contexto privilegiado.
Tests:
- 6 unit tests (slug, defaults, manifest, errores).
- E2E bajo bwrap+user-ns con CAP_SYS_ADMIN: ciclo try → escribir → commit →
verificar persiste; try → escribir → discard → verificar revertido;
remove en merged → commit → whiteout aplicado al lower. Gateado en
HAMMER_OVERLAY_TESTS=1: por defecto, los kernels deniegan el mount
overlayfs en user-ns no-privilegiado (sysctl/AppArmor); el dev habilita
cuando su entorno lo permite o corre con sudo.
CLI: hammer try [targets…] --state-root, commit <id>, discard <id>, status.
Refactor mínimo: las cmds que no tocan artefactos ya no abren (ni crean) el
store. `hammer status --state-root /tmp` no toca /store.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Cierra el primer entregable del roadmap. Cambios:
- `Source`: ahora admite dos modos mutuamente excluyentes — `git` (repo+commit) o
`tarball` (url+sha256). El hash de entrada del artefacto usa el commit en modo
git y el sha256 en modo tarball; ambos son identificadores inmutables del
contenido fuente. Validación en `Source::kind()` con error claro.
- `fetch`: dispatch por modo. Tarball cacheado en `work/tarballs/<sha>.tar`,
descarga vía `curl -fL`, verificación sha256 con `sha2`, extracción con
`--strip-components` (default 1, GNU-style).
- `recipes/grep.toml`: GNU grep 3.12 desde el tarball release de gnu.org
(sha256 fijado, sin patches, `--disable-perl-regexp --disable-nls`).
- Bootstrap: añade `binutils` al rootfs Alpine — configure de autotools mira
ld/ar/ranlib incluso cuando el compilador real es zig cc.
- `.gitignore`: `/work/` (artefactos transitorios del build).
- `docs/10-roadmap.md`: Fase 0 y Fase 1 marcadas ✅; entregable cerrado.
Nota sobre v3.11 vs v3.12: probé primero v3.11 y el binario producido daba
"memory exhausted" en cualquier regex (bug conocido de grep+musl static al
inicializar DFA). v3.12 corrige y funciona limpio dentro del rootfs Alpine.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Arranque del proyecto hammer (distro AI-nativa: laboratorio funcional en el
sótano, terminal mutable clásica arriba, integración de IA programadora).
- Workspace Rust (compila, tests verdes): hammer-core, hammer-build,
hammer-cli (bin `hammer`), hammerd.
- SDDs 00-10 + 6 ADRs en docs/ con toda la arquitectura.
- Esqueletos navegables mapeados a las fases del roadmap; Fase 0/1 listas
para implementar el sandbox real.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>