El diseño completo del hueco: qué declara el PAQUETE (`[[service]]`, hecho) y
qué decide el PERFIL (si arranca, pendiente), que es la misma partición que
systemd hace entre [Service] e [Install] y que `arje-absorb` respeta al absorber
sólo lo habilitado.
Deja escritas las dos cosas que cuestan caro si se descubren después:
1. El SDD 06 decía «el init (arje) lee ese árbol» de /etc/hammer/init.d/*.rule.
Es FALSO: el único uso de INIT_RULES_DIR en el repo es escribirlo. Había TRES
convenciones de dónde vive un servicio y ninguna se tocaba con las otras
(más una cuarta en `query service:`). Canónicos son los de arje —genesis de
la seed y cards.d/—; la mutación del .swm queda derogada o reapuntada, y la
afirmación falsa queda marcada en su propio doc.
2. La trampa para el emisor: el sidecar .hammer/recipe.toml NO entra al
ArtifactHash, así que el openssh ya sellado no lleva el bloque y, con el hash
sin mover, NUNCA se reconstruye solo. Derivar la seed del sidecar hoy daría
un producto SIN sshd, en silencio. Por eso product_seed_card() sigue usando
la constante a propósito, y re-sellar es una unidad aparte CON control de
reproducibilidad — openssh no está certificado como reproducible.
645 líneas. Los ADR entran porque en este repo SON documentos vivos, no
registros inmutables: el 0013 tiene 5 commits, el 0009 dos. Eso se comprobó
antes de decidir, no se asumió por convención general.
EXCLUIDOS por ser REGISTRO o generado: docs/evidencia/ (6), el HANDOFF de la
noche de KDE (1) y docs/state/ (24, se regenera solo). Reescribir un comando
dentro de una evidencia la falsifica.
Y el ADR 0016 se excluye de todo barrido, con un aviso adentro para el próximo
que barra: habla SOBRE el renombre, así que necesita seguir diciendo 'hammer'.
El barrido se lo llevó puesto y lo dejó titulado 'Renombre del sistema: takana
→ takana'; revertido.
Congelados, verificados uno por uno con controles: /opt/hammer, /var/lib/hammer,
/usr/bin/hammer, /mnt/vvv/hammer, la URL de gitea, hammer-farm.service,
hammer-live-install.sh, BRIEFING-hammer.md, hammerd, hammer-recover y
HAMMER_LIVE.
Poblar el catálogo no es opcional: 34 recetas a mano = userland desierto. nixpkgs es el mayor set
de recetas DESDE FUENTE ⇒ semilla natural. Importamos la RECETA (source+hash+deps), nunca el
binario del cache de nix — hammer reconstruye desde fuente ("verificar, no confiar").
- crates/hammer-cli/nix_import.rs: consume el JSON normalizado de nix y emite una receta hammer.
Clasifica el origen: fetchurl flat → tarball+sha256 (convierte el hash nix SRI/base32/hex → hex);
fetchFromGitHub → repo+commit (hammer pinea por commit, no necesita el hash NAR). Filtra el ruido
de stdenv (setup-hooks, wrappers). nix_base32 decode portado. 11 tests.
- `hammer import-nix [FILE|-]` (stdin) → receta .toml; valida que parsee como Recipe.
- scripts/nix-import.sh <attr>: `nix eval --apply` produce el JSON normalizado y lo pipea al
importador. NIX_STORE= para store local si /nix/store no es escribible.
- VALIDADO contra nixpkgs REAL (nix 2.34): import hello (tarball, sha256→hex) + ripgrep (github→
repo+commit); pipeline completo nix→import→pack→.swm probado con hello. 31 suites verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dos bordes ásperos de la paquetería:
- uninstall ahora retira los directorios que quedaron VACÍOS por el borrado (rmdir de abajo
arriba; remove_dir sólo borra dirs vacíos ⇒ se detiene solo al toparse con contenido de otro
paquete). Antes dejaba /usr/bin, etc. huérfanos.
- `install --require-signed`: modo estricto que ABORTA si el release no está firmado por una
clave confiada (Unsigned o UnknownKey ⇒ error). No basta con que el .swm reproduzca: exige
autoría verificada del catálogo. Default off (no rompe flujos sin firma).
Validado E2E: uninstall bwrap poda 4 dirs; --require-signed aborta sin firma y procede con
release trusted. 31 suites verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El repo son ficheros estáticos (index.json + .swm) ⇒ cualquier servidor estático lo sirve.
- hammer-cli: `RepoSource` {Local(path) | Http(url)}. `install --repo` ahora acepta path o URL.
Para HTTP: lee index.json por GET, materializa un repo LOCAL temporal bajando el índice + los
.swm del cierre de deps (curl, vía download::fetch_url_bytes), y de ahí el flujo es IDÉNTICO al
local (resolución de deps, verificación de release/firma/base, reproduce + hidrata). tempfile
pasa a dep normal de hammer-cli.
- Validado E2E: server HTTP estático + install openssh vía http:// → "release: trusted" (índice
firmado bajado por red) → "repo: bajados 3 .swm" (cierre openssh+zlib+openssl) → resuelve del
temporal. (El proxy del sandbox exige NO_PROXY para localhost; el código es correcto.) 31 verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Funcionalidad de gestor de paquetes: rastrear qué hay instalado y poder quitarlo.
- hammer-core/installed.rs: `InstalledDb` (name→{version,hash,files}) load/save JSON; record
(upsert), remove, `owned_by_others` (refcount por ruta). Rutas absolutas ⇒ uninstall no
necesita el root. 4 tests.
- hammer-cli: run_apply ahora DEVUELVE los ficheros que CREA (hidratados + file_drop + init_rule;
config_edit modifica, no crea ⇒ no se registra ni se deshace). install los registra en la DB
(--db, default /var/lib/hammer/installed.json). `uninstall <nombre>` borra esos ficheros salvo
los que otro paquete instalado aporta (refcount) y quita la entrada. `installed` lista.
- Validado E2E REAL: install bwrap (con dep libcap) → registra 2 ficheros → `installed` los
lista → `uninstall bwrap` los borra (prefix vacío, DB vacía). 31 suites verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cierra el hueco de seguridad: hoy se firmaba cada .swm (autoría del paquete) pero NO el
catálogo ⇒ un atacante podía añadir/quitar/intercambiar entradas del index.json. Firmar el
release ancla qué paquetes/versiones/hashes existen.
- hammer-core/sign.rs: extraídos `KeyPair::sign_raw` + `verify_raw` genéricos (bytes canónicos
arbitrarios); Swm::{sign,verify_signature} ahora los reusan (DRY, sin cambio de comportamiento).
- hammer-core/repo.rs: `RepoIndex.signature` (Ed25519 sobre la lista de paquetes canónica,
excluye la propia firma) + `sign`/`verify_signature`. `upsert` INVALIDA la firma (cualquier
cambio al catálogo ⇒ re-firmar). 4 tests (sign→verify, survive save/load, upsert-invalida,
tamper→BadSig).
- hammer-cli: `repo sign --key` / `repo verify`; `install` VERIFICA el release antes de resolver
(BadSig ⇒ aborta "el índice fue manipulado"); `repo list` muestra si está firmado.
- Validado E2E host: sin firmar→firmar→trusted→install lo ve; MANIPULAR el índice sin re-firmar
⇒ install ABORTA; re-publicar invalida la firma. 31 suites verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cierra el hueco que pack/install advertían: un paquete con build-deps (bwrap→libcap,
openssh→zlib,openssl) ahora se instala por nombre reproduciéndose desde fuente CON sus deps.
Modelo: las build-deps viajan por NOMBRE en el source_patch y en la PackageEntry; install
resuelve el cierre transitivo desde el índice y reconstruye un catálogo de recetas que el lab
consulta al materializar deps en el sandbox.
- hammer-core: `Mutation::SourcePatch.deps` (Deps, serde-skip si vacío) + `from_recipe` lo
carga. `Deps::is_empty`. `RepoIndex`/`PackageEntry.deps` + `resolve_closure(name)` (DFS
topológico, deps antes que dependientes, detecta dep faltante y ciclo). 8 tests nuevos.
- hammer-build/swm_bridge: refactor — `recipe_from_source_patch` (síntesis pública, setea deps
+ base_dir=catálogo) + `catalog_dir_for` (dir determinista compartido). build_source_patch
lo reusa. synthesize_recipe ahora setea recipe.deps + base_dir al catálogo (no "/").
- hammer-cli: pack puebla PackageEntry.deps; install resuelve el cierre y escribe un {dep}.toml
por dep en el catalog_dir ANTES de aplicar el target (mismo dir determinista que usa
build_source_patch ⇒ el lab resuelve {dep}.toml por nombre). Warning de pack actualizado.
- VALIDADO E2E REAL contra ./store: `install bwrap` resuelve libcap del catálogo y reproduce
el artefacto CACHEADO EXACTO (b3:f89e716…) → hidrata bwrap (1.8MB ELF). El paquete con dep
hashea bit-idéntico al original. Resolución/diamante/faltante/ciclo unit-tested. 31 suites verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cierra el lazo "packié un .swm → lo instalo por nombre". El repo es el namespace que
le da identidad a los .swm (que en sí no la llevan).
- hammer-core/repo.rs: `RepoIndex` + `PackageEntry` (load/save index.json, find, upsert
idempotente por nombre que reporta el .swm huérfano). Índice JSON plano, ordenado,
diffeable, firmable a futuro como release. 4 tests.
- hammer-cli:
* `pack --repo DIR` PUBLICA (escribe <repo>/<name>-<version>.swm + upsert al índice con
distro_version/expected_hash/signed_by; retira el huérfano de una versión vieja).
* `install <nombre> [--repo] [--trust] [--base-ref] [--prefix] [--skip-source-patch]`
CONSUME: resuelve nombre→.swm, verifica firma (con --trust) ANTES de reproducir, delega
en el camino de apply (reproduce source_patch + hidrata). Nunca corre binario ajeno.
Nombre inexistente → error legible con los disponibles.
* `repo list` imprime el catálogo.
- Validado E2E en host: publicar ripgrep (firmado) + findutils, repo list, index.json limpio,
install ripgrep --trust → "firma: trusted (by alice)" → apply OK. 31 suites verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cierra la dirección forward que faltaba (SDD 06 §6 la marcaba "para más adelante"):
una receta que el sistema ya sabe construir se vuelve un paquete distribuible y
reproducible-desde-fuente, inversa de `hammer apply`.
- hammer-core: `Swm::from_recipe(recipe, target_bin, patch_text, expected, distro)`
(constructor puro: el caller lee los patches). `SwmBuild` gana `phases`+`zig_version`
y `SourcePatch` gana `strip_components` (Option/skip ⇒ .swm viejos parsean igual) para
reproducir con fidelidad el corpus real (22/34 recetas usan phases, 8 usan zig 0.13).
- hammer-build/swm_bridge: la dirección inversa (source_patch→Recipe→build) ahora traslada
phases/zig_version/strip_components a la receta efímera ⇒ apply rehace idéntico.
- hammer-cli: `hammer pack <recipe> [--target-bin] [--out] [--expected|--build] [--sign]`.
Concatena los patches inline; avisa si la receta declara deps (el source_patch aún no
las modela = pieza posterior). `export` también enriquece su source_patch.
- Validado en host: ripgrep (git+patch+install custom), openssl (tarball+zig 0.13+phases),
coreutils (multicall), findutils firmado → swm-verify "trusted". Tests core+bridge verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deja de ser un no-op "pendiente fase 5": apply::apply_init_rule escribe una
regla TOML por servicio (service/action/command); enable/start/restart la
escriben, disable/stop la retiran (idempotente), con guardas de nombre y acción.
CLI y orchestrator la aplican (rebaseando con prefix/overlay). Es el contrato
on-disk que el init (arje) lee para supervisar. 3 tests + doc del formato.
Co-Authored-By: Claude Opus 4.8 (1M context) <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>