Scaffold inicial: workspace Rust + SDDs completos

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>
This commit is contained in:
sergio
2026-06-06 19:06:04 +00:00
co-authored by Claude Opus 4.8
commit 8bf1623044
37 changed files with 2746 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
# Rust
/target
**/*.rs.bk
Cargo.lock.orig
# hammer local state (never commit the store or runtime state)
/store/
/var/
*.swm.local
# editor / OS
.DS_Store
*.swp
.idea/
.vscode/
Conversación co.txt
Generated
+592
View File
@@ -0,0 +1,592 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arrayref"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "blake3"
version = "1.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
"cpufeatures",
]
[[package]]
name = "cc"
version = "1.2.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clap"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "constant_time_eq"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "hammer-build"
version = "0.0.1"
dependencies = [
"anyhow",
"hammer-core",
"thiserror",
"tracing",
]
[[package]]
name = "hammer-cli"
version = "0.0.1"
dependencies = [
"anyhow",
"clap",
"hammer-build",
"hammer-core",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "hammer-core"
version = "0.0.1"
dependencies = [
"anyhow",
"blake3",
"serde",
"serde_json",
"serde_yaml",
"thiserror",
]
[[package]]
name = "hammerd"
version = "0.0.1"
dependencies = [
"anyhow",
"clap",
"hammer-core",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "log"
version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]]
name = "memchr"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
]
[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+38
View File
@@ -0,0 +1,38 @@
[workspace]
resolver = "2"
members = [
"crates/hammer-core",
"crates/hammer-build",
"crates/hammer-cli",
"crates/hammerd",
]
[workspace.package]
version = "0.0.1"
edition = "2021"
license = "MIT"
authors = ["sergio <gerencia@jlsoltech.com>"]
repository = "https://gitea.gioser.net/sergio/hammer"
[workspace.dependencies]
hammer-core = { path = "crates/hammer-core" }
hammer-build = { path = "crates/hammer-build" }
# shared third-party deps, pinned at the workspace level
anyhow = "1"
thiserror = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"
blake3 = "1"
clap = { version = "4", features = ["derive"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[profile.release]
# small, static-friendly binaries for the userland
opt-level = "z"
lto = true
codegen-units = 1
strip = true
panic = "abort"
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 sergio
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+67
View File
@@ -0,0 +1,67 @@
# hammer
> Una distribución Linux construida con un **laboratorio funcional y hermético** en el
> sótano, y una **terminal mutable, clásica y anárquica** en el piso de arriba — diseñada
> desde el suelo para que una **IA programadora** entienda, modifique y comparta el sistema.
`hammer` no es otro gestor de paquetes inmutable. Es una arquitectura de dos mundos:
- **El laboratorio** compila desde fuentes upstream (commits fijados), de forma
determinista y aislada (`bubblewrap` + `zig cc` + musl estático), y direcciona cada
artefacto por su hash (BLAKE3) en un *content-addressed store*.
- **El userland** es un Linux clásico, mutable, con FHS de verdad (`/bin`, `/lib`, `/etc`).
Los binarios se **hidratan** desde el store por *hardlinks*. Puedes pisar archivos en
caliente, romper cosas con un `rm -rf`, y revertir cuando quieras.
Entre ambos mundos viven las tres piezas que hacen a `hammer` distinto:
1. **Overlay de experimentación** — prueba cambios sobre el sistema real con red de
seguridad; `hammer try` / `commit` / `discard`.
2. **Diario de mutaciones** — un daemon `fanotify` registra todo lo que tú (o la IA)
cambiáis a mano. Vives imperativamente; el sistema genera el delta contra la base
limpia. **El diario es tu configuración — sin ser declarativa.**
3. **Manifiesto `.swm`** — compartes la *receta de la mutación* (parche de fuente + flags +
ediciones de config), no el binario cocinado. El receptor **reproduce y verifica**; no
confía en tu binario.
Encima de todo corre el **bus de agente** (`/run/agent.sock`): la IA habla un protocolo
pequeño y legible, dispara compilaciones, inyecta en el overlay, escucha fallos y reacciona.
Tú tienes la última palabra.
## Estado
Fase de arranque. Validamos la capa AI-nativa **sobre Alpine** (musl + FHS ya cocidos)
antes de bajar a distro propia. Ver [`docs/10-roadmap.md`](docs/10-roadmap.md).
## Documentación de diseño (SDD)
Toda la arquitectura está en [`docs/`](docs/). Empieza por
[`docs/00-vision.md`](docs/00-vision.md) y [`docs/01-architecture.md`](docs/01-architecture.md).
## Estructura
```
crates/
hammer-core tipos compartidos: Recipe, Swm, hashing CAS, store
hammer-build el laboratorio: sandbox + compilación + hidratación
hammer-cli el binario `hammer` (build, hydrate, try, commit, apply, export…)
hammerd daemon: bus de agente + diario de mutaciones
docs/ SDDs y ADRs
```
## Stack
| Capa | Decisión |
|---|---|
| Base de validación | Alpine (musl + FHS mutable) |
| Tooling / daemons | Rust (estático-musl) |
| Compilador del lab | `zig cc` por defecto, pluggable por receta |
| Sandbox de build | bubblewrap (namespaces) |
| Direccionamiento | BLAKE3 content-addressed store |
| Despliegue | Hidratación por hardlinks + patchelf |
## Filosofía
> La automatización es mi empleada en el sótano, pero en el piso de arriba mando yo.
Eficiencia matemática en la manufactura, libertad biológica en la ejecución.
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "hammer-build"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
description = "El laboratorio de hammer: sandbox de build (bubblewrap + zig cc) e hidratación."
[dependencies]
hammer-core.workspace = true
anyhow.workspace = true
thiserror.workspace = true
tracing.workspace = true
+55
View File
@@ -0,0 +1,55 @@
//! El laboratorio: compila una receta de forma hermética y la sella en el store; e hidrata
//! artefactos al FHS. Ver `docs/02-build-lab.md` y `docs/03-hydration.md`.
//!
//! Esqueleto de Fase 0. Las funciones públicas fijan el contrato; la implementación del
//! sandbox (bubblewrap) y de patchelf se irá rellenando.
use std::path::Path;
use hammer_core::{ArtifactHash, LinkMode, Recipe, Store};
pub mod sandbox;
/// Calcula el `ArtifactHash` de una receta, resolviendo recursivamente sus deps de build.
/// Ver `docs/02-build-lab.md` §2 y §5.
pub fn artifact_hash(recipe: &Recipe, store: &Store) -> hammer_core::Result<ArtifactHash> {
let dep_hashes: Vec<ArtifactHash> = Vec::new();
for dep_name in &recipe.deps.build {
// TODO(fase-0): cargar la receta de la dep y recursar. Por ahora se documenta el
// contrato; el grafo real se implementa con `hammer build`.
let _ = dep_name;
}
let inputs = recipe.hash_inputs(&dep_hashes);
let refs: Vec<&[u8]> = inputs.iter().map(|v| v.as_slice()).collect();
let _ = store;
Ok(ArtifactHash::of_inputs(&refs))
}
/// Compila una receta y devuelve el hash del artefacto sellado en el store.
/// Si el hash ya existe en el store, devuelve sin recompilar (caché).
pub fn build(recipe: &Recipe, store: &Store) -> hammer_core::Result<ArtifactHash> {
let h = artifact_hash(recipe, store)?;
if store.has(&h, &recipe.name) {
tracing::info!(hash = %h, name = %recipe.name, "caché: artefacto ya en el store");
return Ok(h);
}
// TODO(fase-0): resolver deps → make_sandbox → run_phases(patch/configure/compile/install)
// → store.seal(out, &h, &recipe.name).
tracing::warn!("build real pendiente (Fase 0): sandbox bubblewrap + zig cc");
Err(hammer_core::Error::Other(anyhow::anyhow!(
"build no implementado todavía; ver docs/10-roadmap.md (Fase 0)"
)))
}
/// Proyecta un artefacto del store al FHS (real o de overlay). Ver `docs/03-hydration.md`.
pub fn hydrate(
_h: &ArtifactHash,
_store: &Store,
_target_fhs: &Path,
_mode: LinkMode,
) -> hammer_core::Result<()> {
// TODO(fase-1): hardlink del store al FHS; patchelf (set-interpreter/set-rpath) si Dynamic.
Err(hammer_core::Error::Other(anyhow::anyhow!(
"hidratación no implementada todavía; ver docs/10-roadmap.md (Fase 1)"
)))
}
+41
View File
@@ -0,0 +1,41 @@
//! El sandbox hermético de build (bubblewrap). Ver `docs/02-build-lab.md` §3 y §4.
//!
//! Esqueleto de Fase 0. Define la forma del aislamiento que implementaremos: raíz tmpfs,
//! compilador read-only inyectado, fuentes/deps read-only, sin red, salida vía DESTDIR.
use std::path::PathBuf;
/// Descripción de un sandbox de build a montar con `bwrap`.
#[derive(Debug, Clone, Default)]
pub struct Sandbox {
/// Binds read-only: (origen_host, destino_sandbox).
pub ro_binds: Vec<(PathBuf, PathBuf)>,
/// Directorio de salida (DESTDIR) dentro del sandbox.
pub out: PathBuf,
/// Aislar red (hermeticidad: el build no descarga nada no declarado).
pub isolate_net: bool,
}
impl Sandbox {
/// Construye los argumentos para `bwrap`. TODO(fase-0): completar y ejecutar.
pub fn bwrap_args(&self) -> Vec<String> {
let mut args = vec![
"--unshare-all".into(),
"--tmpfs".into(),
"/".into(),
"--proc".into(),
"/proc".into(),
"--dev".into(),
"/dev".into(),
];
if !self.isolate_net {
args.push("--share-net".into());
}
for (src, dst) in &self.ro_binds {
args.push("--ro-bind".into());
args.push(src.display().to_string());
args.push(dst.display().to_string());
}
args
}
}
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "hammer-cli"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
description = "El binario `hammer`: orquesta build, hydrate, try/commit, apply/export y ctl."
[[bin]]
name = "hammer"
path = "src/main.rs"
[dependencies]
hammer-core.workspace = true
hammer-build.workspace = true
anyhow.workspace = true
clap.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
+99
View File
@@ -0,0 +1,99 @@
//! `hammer` — el punto de entrada de todo flujo manual. Ver `docs/`.
//!
//! Los subcomandos están mapeados a las fases del roadmap (`docs/10-roadmap.md`). Los que aún
//! no están implementados devuelven un error claro indicando su fase, para que el esqueleto
//! sea navegable desde el día uno.
use clap::{Parser, Subcommand};
const DEFAULT_STORE: &str = "/store";
#[derive(Parser)]
#[command(
name = "hammer",
version,
about = "Laboratorio funcional en el sótano, terminal mutable arriba.",
long_about = "hammer: distro AI-nativa. Ver docs/ para el diseño completo."
)]
struct Cli {
/// Ruta del content-addressed store.
#[arg(long, default_value = DEFAULT_STORE, global = true)]
store: String,
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
/// [Fase 0] Compila una receta y la sella en el store.
Build {
/// Ruta a la receta TOML.
recipe: String,
},
/// [Fase 1] Proyecta un artefacto del store al FHS (real o de overlay).
Hydrate {
hash: String,
#[arg(long, default_value = "/")]
into: String,
},
/// [Fase 2] Monta un overlay de experimentación sobre los directorios del sistema.
Try,
/// [Fase 2] Fusiona el overlay activo al FHS real (registra en el diario).
Commit,
/// [Fase 2] Descarta el overlay activo; vuelve al estado base.
Discard,
/// [Fase 2] Muestra el estado de overlays activos.
Status,
/// [Fase 3] Ver/seguir el diario de mutaciones.
Journal,
/// [Fase 4] Aplica un manifiesto .swm (reproduce y lo deja en un overlay).
Apply { file: String },
/// [Fase 4] Exporta el delta del sistema como manifiesto .swm.
Export {
#[arg(long)]
base: Option<String>,
},
/// [Fase 5] Envía un comando al init (proxy a /run/init.control).
Ctl { line: String },
}
fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info".into()),
)
.init();
let cli = Cli::parse();
let store = hammer_core::Store::open(&cli.store)?;
match cli.cmd {
Cmd::Build { recipe } => {
let text = std::fs::read_to_string(&recipe)?;
let recipe = hammer_core::Recipe::from_toml(&text)?;
let hash = hammer_build::build(&recipe, &store)?;
println!("{hash}");
}
Cmd::Hydrate { hash, into } => {
println!("[fase 1 pendiente] hydrate {hash} into {into}");
}
Cmd::Try | Cmd::Commit | Cmd::Discard | Cmd::Status => {
println!("[fase 2 pendiente] overlay — ver docs/04-overlay.md");
}
Cmd::Journal => {
println!("[fase 3 pendiente] diario — ver docs/05-journal.md");
}
Cmd::Apply { file } => {
println!("[fase 4 pendiente] apply {file} — ver docs/06-swm-format.md");
}
Cmd::Export { base } => {
println!("[fase 4 pendiente] export base={base:?} — ver docs/05-journal.md");
}
Cmd::Ctl { line } => {
println!("[fase 5 pendiente] ctl {line:?} — ver docs/07-agent-bus.md");
}
}
Ok(())
}
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "hammer-core"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
description = "Tipos compartidos de hammer: Recipe, Swm, hashing CAS y el store."
[dependencies]
anyhow.workspace = true
thiserror.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
blake3.workspace = true
+66
View File
@@ -0,0 +1,66 @@
//! Direccionamiento por contenido (CAS). Ver `docs/02-build-lab.md` §2.
//!
//! El `ArtifactHash` identifica un artefacto por TODO lo que influye en su salida:
//! commit fuente + parches + flags/compilador/target + hashes de dependencias.
use serde::{Deserialize, Serialize};
/// Hash BLAKE3 de un artefacto, con prefijo legible `b3:`.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ArtifactHash(String);
impl ArtifactHash {
/// Construye desde bytes ya hasheados (representación hex).
pub fn from_hex(hex: impl Into<String>) -> Self {
ArtifactHash(format!("b3:{}", hex.into()))
}
/// Hashea un conjunto ordenado de entradas. El llamador es responsable de pasar las
/// entradas en orden canónico y estable (ver `docs/02-build-lab.md` §2).
pub fn of_inputs(inputs: &[&[u8]]) -> Self {
let mut hasher = blake3::Hasher::new();
for chunk in inputs {
// length-prefijado para evitar colisiones por concatenación ambigua.
hasher.update(&(chunk.len() as u64).to_le_bytes());
hasher.update(chunk);
}
ArtifactHash(format!("b3:{}", hasher.finalize().to_hex()))
}
/// Forma corta para directorios del store: `<hash-sin-prefijo>-<name>`.
pub fn store_dir_name(&self, name: &str) -> String {
let bare = self.0.strip_prefix("b3:").unwrap_or(&self.0);
format!("{bare}-{name}")
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for ArtifactHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deterministic_and_order_sensitive() {
let a = ArtifactHash::of_inputs(&[b"grep", b"abc123", b"--static"]);
let b = ArtifactHash::of_inputs(&[b"grep", b"abc123", b"--static"]);
assert_eq!(a, b, "misma entrada ⇒ mismo hash");
let c = ArtifactHash::of_inputs(&[b"abc123", b"grep", b"--static"]);
assert_ne!(a, c, "orden distinto ⇒ hash distinto");
}
#[test]
fn store_dir_name_strips_prefix() {
let h = ArtifactHash::from_hex("deadbeef");
assert_eq!(h.store_dir_name("grep"), "deadbeef-grep");
}
}
+32
View File
@@ -0,0 +1,32 @@
//! Tipos núcleo de hammer, compartidos por el lab, la CLI y el daemon.
//!
//! Ver `docs/01-architecture.md` y siguientes. Esto es el esqueleto de Fase 0: los tipos y
//! contratos están definidos; la lógica pesada (sandbox, fanotify, bus) vive en los otros
//! crates y se irá rellenando por fase.
pub mod hash;
pub mod recipe;
pub mod store;
pub mod swm;
pub use hash::ArtifactHash;
pub use recipe::{Compiler, LinkMode, Recipe};
pub use store::Store;
pub use swm::Swm;
/// Error común del ecosistema hammer.
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("serialización: {0}")]
Serde(String),
#[error("receta inválida: {0}")]
Recipe(String),
#[error("store: {0}")]
Store(String),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
+109
View File
@@ -0,0 +1,109 @@
//! La `Recipe`: descripción pura de un build. Ver `docs/02-build-lab.md` §1.
use serde::{Deserialize, Serialize};
use crate::hash::ArtifactHash;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Recipe {
pub name: String,
pub version: String,
pub source: Source,
pub build: Build,
#[serde(default)]
pub deps: Deps,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Source {
pub repo: String,
/// Commit FIJADO. Nunca "HEAD". Ver ADR 0006.
pub commit: String,
#[serde(default)]
pub patches: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Build {
#[serde(default)]
pub compiler: Compiler,
#[serde(default = "default_target")]
pub target: String,
#[serde(default)]
pub link: LinkMode,
#[serde(default)]
pub flags: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Deps {
#[serde(default)]
pub build: Vec<String>,
#[serde(default)]
pub runtime: Vec<String>,
}
/// Compilador del lab, POR RECETA (no global). `zig-cc` por defecto; escotilla a clang/gcc
/// para paquetes con gcc-ismos. Ver ADR 0003.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Compiler {
#[default]
ZigCc,
Clang,
Gcc,
}
impl Compiler {
pub fn as_str(&self) -> &'static str {
match self {
Compiler::ZigCc => "zig-cc",
Compiler::Clang => "clang",
Compiler::Gcc => "gcc",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LinkMode {
#[default]
Static,
Dynamic,
}
fn default_target() -> String {
"x86_64-linux-musl".to_string()
}
impl Recipe {
pub fn from_toml(_s: &str) -> crate::Result<Recipe> {
// TODO(fase-0): parsear TOML. Por ahora el parsing real lo añadiremos al implementar
// `hammer build`; el contrato (esta struct) ya está fijado.
Err(crate::Error::Recipe(
"parsing de receta TOML pendiente (Fase 0)".into(),
))
}
/// Las entradas canónicas que alimentan el `ArtifactHash` de esta receta.
/// NOTA: el hash final también incorpora los hashes de las deps de build (recursivo),
/// que el lab resuelve antes de llamar aquí. Ver `docs/02-build-lab.md` §2 y §5.
pub fn hash_inputs(&self, dep_hashes: &[ArtifactHash]) -> Vec<Vec<u8>> {
let mut v: Vec<Vec<u8>> = vec![
self.source.commit.as_bytes().to_vec(),
self.build.compiler.as_str().as_bytes().to_vec(),
self.build.target.as_bytes().to_vec(),
format!("{:?}", self.build.link).into_bytes(),
];
for p in &self.source.patches {
v.push(p.as_bytes().to_vec());
}
for f in &self.build.flags {
v.push(f.as_bytes().to_vec());
}
for d in dep_hashes {
v.push(d.as_str().as_bytes().to_vec());
}
v
}
}
+49
View File
@@ -0,0 +1,49 @@
//! El content-addressed store. Ver `docs/03-hydration.md` §1.
//!
//! Inmutable, append-only, direccionado por `ArtifactHash`. Esqueleto de Fase 0: la API está
//! fijada; el sellado real (mover el árbol de salida del sandbox e idealmente hacerlo de sólo
//! lectura) se completa al implementar el builder.
use std::path::{Path, PathBuf};
use crate::hash::ArtifactHash;
pub struct Store {
root: PathBuf,
}
impl Store {
/// Abre (o prepara) un store en `root` (p. ej. `/store`).
pub fn open(root: impl Into<PathBuf>) -> crate::Result<Self> {
let root = root.into();
std::fs::create_dir_all(&root)?;
Ok(Store { root })
}
pub fn root(&self) -> &Path {
&self.root
}
/// Ruta del artefacto en el store para un hash + nombre legible.
pub fn path_of(&self, h: &ArtifactHash, name: &str) -> PathBuf {
self.root.join(h.store_dir_name(name))
}
/// ¿Ya existe el artefacto? (caché del lab; ver `docs/02-build-lab.md` §5).
pub fn has(&self, h: &ArtifactHash, name: &str) -> bool {
self.path_of(h, name).is_dir()
}
/// Sella el árbol de salida de un build en el store bajo su hash.
/// TODO(fase-0): mover `out_dir` a la ruta del store y marcarlo read-only.
pub fn seal(
&self,
_out_dir: &Path,
_h: &ArtifactHash,
_name: &str,
) -> crate::Result<PathBuf> {
Err(crate::Error::Store(
"sellado en el store pendiente (Fase 0)".into(),
))
}
}
+121
View File
@@ -0,0 +1,121 @@
//! El manifiesto `.swm` (Software Mutación). Ver `docs/06-swm-format.md`.
//!
//! Es la unidad de intercambio: receta de transformación sobre fuente pública + ediciones de
//! config. NUNCA transporta binarios cocidos (salvo `FileDrop` con hash declarado).
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Swm {
pub swm_version: u32,
pub base: Base,
pub mutations: Vec<Mutation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<Signature>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Base {
pub distro_version: String,
#[serde(default)]
pub pins: std::collections::BTreeMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Mutation {
SourcePatch {
repo: String,
commit: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
patch: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
patch_url: Option<String>,
build: SwmBuild,
target_bin: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
expected_hash: Option<String>,
},
ConfigEdit {
file: String,
inline_diff: String,
},
InitRule {
action: String,
service: String,
command: String,
},
FileDrop {
path: String,
/// hash BLAKE3 del contenido, declarado para verificación.
content_hash: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
content_url: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SwmBuild {
#[serde(default = "default_compiler")]
pub compiler: String,
#[serde(default = "default_target")]
pub target: String,
#[serde(default = "default_link")]
pub link: String,
#[serde(default)]
pub flags: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Signature {
pub by: String,
pub alg: String,
pub sig: String,
}
fn default_compiler() -> String {
"zig-cc".into()
}
fn default_target() -> String {
"x86_64-linux-musl".into()
}
fn default_link() -> String {
"static".into()
}
impl Swm {
pub fn from_yaml(s: &str) -> crate::Result<Swm> {
serde_yaml::from_str(s).map_err(|e| crate::Error::Serde(e.to_string()))
}
pub fn to_yaml(&self) -> crate::Result<String> {
serde_yaml::to_string(self).map_err(|e| crate::Error::Serde(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_yaml() {
let yaml = r#"
swm_version: 1
base:
distro_version: "2026-06-06"
pins:
grep: "a1b2c3d"
mutations:
- type: config_edit
file: "/etc/network.conf"
inline_diff: |
- DHCP=yes
+ IP=192.168.1.100
"#;
let swm = Swm::from_yaml(yaml).expect("parse");
assert_eq!(swm.swm_version, 1);
assert_eq!(swm.mutations.len(), 1);
let back = swm.to_yaml().expect("serialize");
assert!(back.contains("config_edit"));
}
}
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "hammerd"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
description = "Daemon de hammer: bus de agente (/run/agent.sock) y diario de mutaciones (fanotify)."
[[bin]]
name = "hammerd"
path = "src/main.rs"
[dependencies]
hammer-core.workspace = true
anyhow.workspace = true
clap.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
+44
View File
@@ -0,0 +1,44 @@
//! `hammerd` — daemon de hammer. Dos responsabilidades:
//! 1. Bus de agente: /run/agent.sock (JSON-líneas, SO_PEERCRED). Ver `docs/07-agent-bus.md`.
//! 2. Diario de mutaciones: fanotify sobre /bin,/sbin,/lib,/etc. Ver `docs/05-journal.md`.
//!
//! Esqueleto de arranque. Las dos subsistemas (Fases 3 y 5) se implementarán por separado;
//! aquí queda el binario navegable y el cableado básico de logging/argumentos.
use clap::Parser;
#[derive(Parser)]
#[command(name = "hammerd", version, about = "Daemon de hammer: bus de agente + diario.")]
struct Args {
/// Socket del bus de agente.
#[arg(long, default_value = "/run/agent.sock")]
agent_sock: String,
/// FIFO de control humano del init.
#[arg(long, default_value = "/run/init.control")]
init_control: String,
/// Directorio del diario de mutaciones.
#[arg(long, default_value = "/var/lib/hammer/journal")]
journal: String,
}
fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info".into()),
)
.init();
let args = Args::parse();
tracing::info!(
agent_sock = %args.agent_sock,
init_control = %args.init_control,
journal = %args.journal,
"hammerd: arranque (esqueleto)"
);
// TODO(fase-3): iniciar el watcher fanotify → diario.
// TODO(fase-5): servir el bus de agente en agent_sock (JSON-líneas, SO_PEERCRED).
tracing::warn!("subsistemas pendientes: diario (Fase 3) y bus de agente (Fase 5)");
Ok(())
}
+77
View File
@@ -0,0 +1,77 @@
# SDD 00 — Visión y filosofía
## 1. El problema
El panorama de distribuciones Linux te obliga a elegir entre dos extremos malos:
- **Inmutables / declarativas** (NixOS, Guix, Fedora Silverblue): determinismo y
reproducibilidad reales, pero a costa de tu agencia. El sistema operativo *es* el grafo.
Pierdes la estructura familiar de directorios, pierdes el control directo en la terminal,
y vives editando un archivo de configuración abstracto en lugar de habitar el sistema.
Para una herramienta de automatización (una IA), además, la complejidad está *oculta tras
capas de abstracción* que la obligan a simular a un humano.
- **Tradicionales / imperativas** (Arch, Gentoo, Slackware): control y mutabilidad totales,
pero con el tiempo se pudren — el gestor de paquetes pierde el rastro de lo que el usuario
hizo a mano, se acumulan archivos huérfanos, y reproducir tu sistema en otra máquina es
imposible sin documentar cada acción manualmente.
Ninguno de los dos sirve bien para el escenario que viene: **un usuario con una IA
programadora que entiende, modifica y comparte su sistema en cualquier contexto.**
## 2. La tesis
Se puede tener lo mejor de ambos separando dos mundos que hoy están fusionados:
> **La automatización funcional es mi empleada en el sótano. En el piso de arriba mando yo.**
- **El sótano (el laboratorio):** compilación determinista, hermética, direccionada por
contenido. Aquí reina la teoría de grafos y el hashing. Cero efectos secundarios.
- **El piso de arriba (el userland):** un Linux clásico, mutable, con `/bin`, `/lib`, `/etc`
reales. Aquí reina el usuario. Un `rm -rf` rompe cosas de verdad. La terminal es un espacio
vivo de interacción directa, no la interfaz de lectura de un archivo de config.
El puente entre ambos es la **hidratación**: los artefactos del store se proyectan al FHS
real por hardlinks, normalizados para no arrastrar rutas del store.
## 3. Por qué esto es AI-nativo (el verdadero diferenciador)
La distro en sí no es la innovación — ensamblar componentes conocidos (musl, busybox, un
kernel) es trabajo conocido. **La innovación es el substrato de baja entropía** que resulta:
1. **Compilación determinista** → la IA pide cambios de fuente, recompila en el lab aislado,
y el binario es predecible. Vuelve atrás sin dramas.
2. **El sistema ejecutable es texto plano + binarios atómicos** → la IA no parsea bases de
datos binarias de configuración (registro de Windows, journal de systemd). Lee/escribe
archivos e inspecciona binarios con `readelf`/`objdump`.
3. **Mutabilidad = agencia real** → la IA experimenta en un overlay y promueve cambios. Si se
equivoca, desmontas la capa. Sin rollbacks complejos ni snapshots pesados.
4. **musl mantiene el código C legible** para el contexto de un LLM (glibc es un laberinto de
macros prehistóricas).
5. **El enlazado estático** garantiza que una herramienta mutada por la IA corre sin romper
enlaces de librerías en otros programas.
Mientras la industria diseña sistemas para *proteger al sistema del administrador*
(restringiendo, aislando, inmutabilizando) — y al hacerlo también ciega a las herramientas de
IA — `hammer` hace lo contrario: **mantiene las tuberías expuestas y de baja entropía**, y
trata al usuario (y a su IA) como un operador consciente, no como un peligro.
## 4. Principios de diseño
- **Determinismo en la fábrica, libertad en la ejecución.** No negociar ninguno de los dos.
- **Texto plano y binarios atómicos por encima de bases de datos opacas.** Si algo del sistema
no se puede `cat`, `grep` o `readelf`, es una bandera roja.
- **Verificar, no confiar.** Se comparten recetas reproducibles, nunca binarios cocidos.
- **La IA propone; el humano dispone.** Toda acción de la IA es reversible y queda registrada.
- **No reinventar lo resuelto.** Usamos motores y compiladores existentes como herramientas;
el valor está en el pegamento AI-nativo, no en reescribir Nix o un toolchain.
- **El diario es la verdad.** Vives imperativamente; el sistema deriva el estado declarativo
*a posteriori*. Nunca al revés.
## 5. Anti-objetivos (lo que `hammer` NO es)
- No es un gestor de paquetes inmutable ni un daemon que restaura estado.
- No reescribe un motor de build desde cero (ver [ADR 0004](adr/0004-no-custom-nix.md)).
- No compila contra `HEAD` vivo a ciegas (ver [ADR 0006](adr/0006-pinned-commits.md)).
- No oculta complejidad tras abstracciones que cieguen al usuario o a la IA.
- No promete estabilidad de ABL de musl como magia (ver [SDD 03](03-hydration.md)).
+99
View File
@@ -0,0 +1,99 @@
# SDD 01 — Arquitectura general
## 1. El modelo de dos mundos
```
EL SÓTANO (laboratorio) EL PISO DE ARRIBA (userland)
determinista · hermético mutable · clásico · FHS real
┌──────────────────────────────────────────────┐ ┌─────────────────────────────────────────┐
│ │ │ │
│ [ Upstreams: git, commits FIJADOS ] │ │ /bin /sbin /lib /etc (FHS clásico) │
│ │ │ │ ▲ │
│ ▼ │ │ │ hardlinks normalizados │
│ ┌───────────────────────┐ │ │ │ │
│ │ hammer-build (lab) │ │ │ ┌───┴──────────────┐ │
│ │ bubblewrap + zig cc │ compila musl │ │ │ HIDRATACIÓN │ patchelf / copy │
│ │ recetas + grafo deps │ estático │ │ └───┬──────────────┘ │
│ └──────────┬────────────┘ │ │ │ │
│ ▼ │ │ │ │
│ ┌───────────────────────┐ │ │ ┌───┴───────────────┐ │
│ │ CAS store (BLAKE3) │ ────────────────────┼───┼─▶│ overlay try/commit │ red de seguridad │
│ │ /store/<hash>-<name> │ │ │ └───┬───────────────┘ │
│ └───────────────────────┘ │ │ │ │
│ │ │ ┌───┴───────────────┐ │
└──────────────────────────────────────────────┘ │ │ diario fanotify │ delta vs base │
│ └───┬───────────────┘ │
┌───────────────────────────────────────────────┐ │ │
│ BUS DE AGENTE /run/agent.sock (JSON-líneas) │◀┘ /run/init.control (FIFO humano) │
│ COMPILE · INJECT · QUERY · eventos │ │
└───────────────────────────┬────────────────────┘ │
│ └─────────────────────────────────────┘
[ IA programadora ] intención NL → .swm → overlay → test → aprobar
```
**Regla de oro:** los dos mundos nunca se mezclan. El lab jamás escribe directamente en el
FHS real; siempre pasa por el store y luego por hidratación. El userland jamás compila contra
el host; siempre delega al lab.
## 2. Componentes y su responsabilidad
| Componente | Crate | Responsabilidad única |
|---|---|---|
| **Laboratorio** | `hammer-build` | Compilar una receta de forma hermética → artefacto en el store |
| **Store CAS** | `hammer-core` | Guardar/recuperar artefactos por hash BLAKE3; garbage collection |
| **Hidratación** | `hammer-build` | Proyectar artefactos del store al FHS real (hardlink + patchelf) |
| **Overlay** | `hammer-cli` | Montar/fusionar/descartar capa de experimentación en caliente |
| **Diario** | `hammerd` | Vigilar mutaciones (fanotify), log append-only, exportar `.swm` |
| **Bus de agente** | `hammerd` | Socket de control para IA/scripts; eventos del sistema |
| **CLI** | `hammer-cli` | El binario `hammer` que orquesta todo lo anterior |
| **Tipos compartidos** | `hammer-core` | `Recipe`, `Swm`, `Hash`, `Store`, formatos serializables |
## 3. Flujo de datos canónico (de la intención al sistema vivo)
1. **Intención** — el humano (o la IA) expresa un cambio: *"que grep ignore binarios por
defecto y use regex Perl"*.
2. **Receta** — se traduce a una `Recipe`: repo + commit fijado + patch + flags + compilador.
3. **Build**`hammer-build` levanta el sandbox, compila con `zig cc`, produce un binario
musl estático, lo deposita en el store bajo `BLAKE3(...)`.
4. **Overlay** — el artefacto se hidrata, pero **en una capa overlay temporal**, no en el
sistema real. El humano/IA prueba en caliente.
5. **Validación** — corre el test harness; los eventos van por el bus (`BUILD_READY`,
`CRASHED`). Si falla, se descarta el overlay; el sistema base intacto.
6. **Promoción** — si pasa, se fusiona al FHS real. El diario registra la mutación.
7. **Compartir**`hammer export` empaqueta el delta como `.swm`: base-hash + patch + config.
Otro usuario lo `apply`-ea: su IA reproduce y verifica localmente; nunca ejecuta tu binario.
## 4. Estado en disco (layout)
```
/store/ el content-addressed store (inmutable, append-only)
<blake3>-<name>/ un artefacto: árbol de archivos resultante de un build
/var/lib/hammer/
recipes/ recetas conocidas (caché de definiciones)
pins.toml lockfile: nombre → commit fijado del upstream
journal/ diario de mutaciones append-only
overlays/ upperdirs de overlays activos
/run/
init.control FIFO de control humano (texto crudo)
agent.sock socket de control de agente (JSON-líneas, SO_PEERCRED)
hammerd.pid
```
> Nota: en la fase Alpine, `/store` y `/var/lib/hammer` viven dentro del rootfs de Alpine.
> En la distro propia se decidirá su partición/montaje. Ver [SDD 10](10-roadmap.md).
## 5. Límites del sistema (qué corre dónde)
- **`hammer-build`** corre privilegiado lo justo para crear namespaces (vía `bubblewrap`,
que usa user-namespaces sin root real cuando es posible).
- **`hammerd`** corre como servicio bajo el init; expone el bus y vigila el diario.
- **`hammer` (CLI)** corre como el usuario; es el punto de entrada de todo flujo manual.
- **La IA** es un cliente del bus como cualquier otro proceso — sin privilegios especiales
más allá de los que el humano le conceda explícitamente (ver [SDD 08](08-ai-integration.md)).
## 6. Decisiones transversales
- Reproducibilidad ⇒ commits fijados, no HEAD ([ADR 0006](adr/0006-pinned-commits.md)).
- No reescribir el motor de build ([ADR 0004](adr/0004-no-custom-nix.md)).
- Validar el concepto sobre Alpine primero ([ADR 0002](adr/0002-alpine-first.md)).
+135
View File
@@ -0,0 +1,135 @@
# SDD 02 — El laboratorio de build
El laboratorio es la fábrica de artefactos: toma una **receta** (qué compilar y cómo) y
produce un **árbol de archivos** depositado en el store, direccionado por su hash. Es la única
parte del sistema que se permite ser funcional/determinista, porque es donde paga.
## 1. Receta (`Recipe`)
Una receta es una descripción declarativa y pura de *un* build. Es el átomo del grafo.
```toml
# ejemplo: recipes/grep.toml
name = "grep"
version = "3.11"
[source]
repo = "git://git.savannah.gnu.org/grep.git"
commit = "a1b2c3d4e5f6..." # FIJADO. Nunca "HEAD". Ver ADR 0006.
patches = ["patches/grep-perl-default.patch"]
[build]
compiler = "zig-cc" # por defecto; escotilla: "clang" | "gcc"
target = "x86_64-linux-musl"
link = "static" # "static" | "dynamic"
flags = ["--enable-perl-regexp"]
# fases override opcionales; si se omiten, se usan las heurísticas estándar
# configure = "..." compile = "..." install = "..."
[deps]
build = ["pcre2", "musl"] # dependencias de compilación (otras recetas)
runtime = [] # vacío si link = static
```
El **compilador es por receta** (no global): `zig-cc` por defecto por hermeticidad y musl
incluido, con escotilla a `clang`/`gcc` para paquetes con gcc-ismos. Ver
[ADR 0003](adr/0003-zig-cc-builder.md).
## 2. El identificador de contenido (CAS)
Cada artefacto se direcciona por un hash que captura **todo** lo que influye en su salida:
```
artifact_hash = BLAKE3(
source_commit // commit git fijado
⧺ canonical(patches) // contenido de los parches, no su ruta
⧺ canonical(build_flags) // flags + compilador + target + link
⧺ Σ dep.artifact_hash // hashes de las dependencias de build (recursivo)
)
```
Consecuencia: **entrada idéntica ⇒ hash idéntico ⇒ salida idéntica**. Esto es lo que permite:
- **Caché:** ¿el hash ya existe en el store? Se reusa, no se recompila.
- **Avance ordenado del grafo:** si un upstream avanza su commit fijado, sólo cambian los
hashes de ese nodo y sus dependientes; el resto del grafo se reusa.
- **Verificación de `.swm`:** dos máquinas con la misma receta producen el mismo hash; así se
verifica un cambio compartido sin confiar en binarios (ver [SDD 09](09-trust-model.md)).
> BLAKE3 por velocidad y paralelismo; el hash no es un secreto, es un identificador.
## 3. El sandbox de build
Cada build corre en su propio entorno aislado, montado con **bubblewrap** (`bwrap`), que usa
namespaces de Linux (mount, pid, net, user) sin requerir root real cuando el kernel lo permite.
Receta del aislamiento:
1. **Raíz tmpfs.** Se monta un `/` temporal vacío en tmpfs. Nada del host es visible salvo lo
que montemos explícitamente.
2. **Compilador inyectado.** `zig` (o el compilador de la receta) se monta read-only en el
`PATH` del sandbox. En el bootstrap es el único compilador presente — así se rompe el
cordón umbilical con el host.
3. **Fuentes read-only.** El árbol del repo (en el commit fijado) se monta read-only.
4. **Dependencias read-only.** Los artefactos de las deps de build, ya en el store, se montan
read-only en rutas conocidas.
5. **Sin red.** El namespace de red se aísla: el `fetch` ocurre *fuera* del sandbox (ver §4),
de modo que el build en sí no puede descargar nada no declarado → hermeticidad real.
6. **Salida vía `DESTDIR`.** El build instala en `DESTDIR=/out`; ese árbol es el artefacto que
se extrae y se sella en el store.
## 4. Fases del build
Inspiradas en BitBake (`do_fetch`/`do_unpack`/`do_configure`/`do_compile`/`do_install`) pero
mínimas. Sólo `fetch` toca la red, y ocurre **antes** de entrar al sandbox:
| Fase | ¿En sandbox? | Qué hace |
|---|---|---|
| `fetch` | No (red permitida) | Clona/actualiza el repo al commit fijado; verifica hash |
| `patch` | Sí | Aplica `source.patches` sobre el árbol read-only (en copia tmpfs) |
| `configure` | Sí | Detecta sistema de build (autotools/cmake/meson/make) y configura |
| `compile` | Sí | Compila con el compilador de la receta hacia `target`/`link` |
| `install` | Sí | `make install DESTDIR=/out` (o equivalente) |
| `seal` | No | Calcula el hash final del árbol `/out`, lo deposita en el store |
La detección de sistema de build en `configure` es heurística (presencia de `configure.ac`,
`CMakeLists.txt`, `meson.build`, `Makefile`) y se puede sobreescribir por receta.
## 5. El grafo de dependencias
Las recetas forman un DAG por sus `deps.build`. El algoritmo de build:
```
build(recipe):
h = artifact_hash(recipe) # requiere resolver deps primero (recursivo)
if store.has(h): return h # caché
for dep in recipe.deps.build:
build(dep) # post-orden: deps antes que el nodo
sandbox = make_sandbox(recipe, deps)
run_phases(sandbox) # patch → configure → compile → install
return store.seal(sandbox.out, h)
```
Builds de nodos independientes se paralelizan (sin dependencia mutua en el DAG). El cap de
concurrencia se ajusta a los cores disponibles.
## 6. Bootstrap (romper el cordón umbilical)
El mayor error es usar las herramientas del host para compilar el sistema final. `zig cc`
reduce drásticamente este problema: un solo binario provee compilador C/C++ + musl + headers,
hermético, cross-compilando al target. El Stage 0 tradicional (cross-compiler mínimo) queda
en gran parte resuelto por `zig`.
Para la fase Alpine no necesitamos bootstrap completo: Alpine ya provee musl y un toolchain;
usamos `zig cc` para los builds de hammer y validamos el flujo. El bootstrap from-scratch real
es trabajo del track de distro propia ([SDD 10](10-roadmap.md), [ADR 0003](adr/0003-zig-cc-builder.md)).
## 7. Interfaz (qué expone el crate)
```rust
// hammer-build
pub fn build(recipe: &Recipe, store: &Store) -> Result<ArtifactHash>;
pub fn artifact_hash(recipe: &Recipe, store: &Store) -> Result<ArtifactHash>;
```
CLI: `hammer build <recipe.toml>` → imprime el hash del artefacto sellado.
+104
View File
@@ -0,0 +1,104 @@
# SDD 03 — Hidratación y store
La hidratación es el puente del sótano al piso de arriba: cómo un artefacto del store
content-addressed termina siendo un `/bin/grep` normal, mutable, en un FHS clásico — sin
arrastrar la ruta del store grabada a fuego, y sin el infierno de symlinks de Nix.
## 1. El store
```
/store/<blake3>-<name>/ un artefacto sellado e inmutable
bin/grep
share/...
```
- **Inmutable y append-only.** Nada modifica un artefacto sellado. Reconstruir produce un
hash nuevo, una entrada nueva.
- **Direccionado por contenido.** El prefijo es el `artifact_hash` (ver [SDD 02](02-build-lab.md)).
- **Deduplicado.** Dos recetas que producen el mismo árbol comparten entrada.
- **GC por alcanzabilidad.** Un artefacto es basura si ningún hardlink del FHS ni ningún pin
lo referencia. `hammer gc` libera lo inalcanzable.
## 2. El problema que resolvemos
Un binario compilado en un entorno funcional lleva grabado:
- el **intérprete ELF** (el loader de la libc): p. ej. `/store/<hash>/lib/ld-musl-x86_64.so.1`,
- el **RPATH** (dónde busca las `.so` dinámicas): rutas dentro del store.
Si copias ese binario tal cual a `/bin`, falla: busca la libc en una ruta del store. Hay que
**normalizar** las rutas al esquema clásico antes de que toque el FHS real.
## 3. Estrategia primaria: enlazado estático (la vía limpia)
Con musl, el camino de oro es **enlazar todo estáticamente** (`link = "static"`). El resultado
es un único binario autosuficiente, sin `.so`, sin intérprete externo, sin RPATH que importe.
- **Hidratar = hardlink directo** del binario del store a `/bin`/`/sbin`.
- **Estructura familiar:** `/bin` real lleno de ejecutables normales; cero symlinks crípticos.
- **Control total:** ¿reemplazar `ls` por una versión experimental compilada a mano? Copias el
binario encima y listo (ver §6, copy-on-write).
Esta es la estrategia por defecto.
## 4. Estrategia secundaria: purga de RPATH con `patchelf` (la vía dinámica)
Cuando un paquete requiere enlazado dinámico (plugins, tooling específico), el lab compila
contra el store, y la hidratación **reescribe el binario** antes de inyectarlo:
1. `patchelf --set-interpreter /lib/ld-musl-x86_64.so.1 <bin>` — loader estándar del sistema.
2. `patchelf --set-rpath /lib:/usr/lib <bin>` — rutas clásicas.
3. Hardlink/copia del binario normalizado y de sus `.so` a `/lib`.
`patchelf` (irónicamente, creado por el equipo de Nix) es la herramienta exacta para esto.
## 5. El core dinámico curado (matiz honesto sobre el ABI de musl)
La idea de "actualizar el sustrato atómicamente y que los binarios no se enteren" **sólo es
segura si se versiona explícitamente.** musl **no garantiza estabilidad de ABI entre
versiones**; confiar ciegamente en ello rompería binarios.
Política de `hammer`:
- El grueso del sistema se enlaza **estático** → inmune al problema.
- Un subconjunto hiper-reducido de librerías core de actualización frecuente por seguridad
(p. ej. `openssl`, `zlib`) puede vivir **dinámico** en una ruta controlada `/lib/core/`,
**versionada** (`/lib/core/libssl.so.3`).
- Las actualizaciones de ese core respetan el `soname`/versión. Si una actualización cambia el
ABI, **es un artefacto nuevo con soname nuevo**, no un reemplazo silencioso.
- Para plugins reales, preferir `dlopen` versionado sobre fe en el ABI.
Resultado: el superpoder de "reemplazar el suelo" existe, pero acotado y honesto, sin la
promesa mágica que rompería en producción.
## 6. Mutabilidad: copy-on-write sobre hardlinks
El FHS está hidratado por **hardlinks** al store (no symlinks → rutas reales; no copias →
dedup). El store es inmutable; el FHS es mutable. ¿Cómo conviven?
- **Leer/ejecutar:** el hardlink apunta al inode del store. Cero copia.
- **Escribir/pisar en caliente:** al reemplazar `/bin/grep`, **se rompe el hardlink** (se
escribe un inode nuevo). El artefacto del store queda intacto → base de rollback.
- **Rollback:** re-hidratar desde el store restaura el hardlink original.
Esto da exactamente lo buscado: "un `rm -rf` rompe cosas de verdad", pero siempre hay una base
limpia reconstruible debajo.
## 7. Interfaz
```rust
// hammer-core
pub struct Store { root: PathBuf }
impl Store {
pub fn has(&self, h: &ArtifactHash) -> bool;
pub fn seal(&self, out_dir: &Path, h: ArtifactHash) -> Result<PathBuf>;
pub fn path_of(&self, h: &ArtifactHash) -> PathBuf;
pub fn gc(&self, roots: &[ArtifactHash]) -> Result<GcReport>;
}
// hammer-build
pub fn hydrate(h: &ArtifactHash, store: &Store, target_fhs: &Path, mode: LinkMode)
-> Result<Vec<HydratedFile>>;
```
CLI: `hammer hydrate <hash> [--into /]` — proyecta el artefacto al FHS (real o de overlay).
+78
View File
@@ -0,0 +1,78 @@
# SDD 04 — Overlay de experimentación
El overlay es la red de seguridad que convierte la mutabilidad total en algo que también es
seguro: la capacidad de **cometer errores sin perder el control imperativo**. Es donde la IA
prueba sus cambios antes de tocar el sistema real, y donde tú experimentas a mano sin miedo.
## 1. El mecanismo
Usa `overlayfs` nativo del kernel, gestionado dinámicamente desde la terminal:
```
montaje overlay sobre /bin (y /etc, /lib…)
upperdir → /var/lib/hammer/overlays/<id>/upper (tmpfs o dir oculto, MUTABLE)
lowerdir → el /bin real (read-only de facto bajo el overlay)
merged → /bin (lo que ve el sistema)
```
Mientras el overlay está montado, el sistema y tú veis `/bin` como **el sistema real
modificado en caliente**. Pero toda escritura cae en el `upperdir`; el `/bin` real (lowerdir)
nunca se toca.
## 2. El ciclo de vida
```
hammer try # monta un overlay sobre los directorios del sistema
... experimentas / la IA inyecta binarios y edita configs ...
... pruebas en caliente: ejecutas, rompes, observas ...
hammer commit # fusiona upperdir → FHS real (rsync inteligente) y registra en el diario
— o —
hammer discard # desmonta y borra el upperdir: el sistema vuelve al estado base, instantáneo
```
- **`try`** → estado base limpio + capa mutable encima. Riesgo cero.
- **`discard`** → desmonta el overlay; el sistema vuelve a su base **instantáneamente**. No hay
que deshacer cambios uno por uno.
- **`commit`** → fusiona la capa al FHS real y anota cada archivo tocado en el diario
([SDD 05](05-journal.md)). A partir de aquí el cambio es permanente y rastreable.
## 3. Por qué overlay y no contenedor
Un contenedor (docker/chroot) te aísla de tus dispositivos y procesos reales — pierdes el
acceso al sistema vivo. El overlay hace lo contrario: **modificas la "realidad" del sistema
operativo** (mismos PIDs, mismos `/dev`, misma red) pero con una red de seguridad. Es la
diferencia entre "probar en una maqueta" y "probar en la casa real con un seguro de vida".
## 4. Composición con el resto del sistema
- **La IA** (ver [SDD 08](08-ai-integration.md)) opera **siempre** dentro de un overlay: clona
el repo, compila en el lab, hidrata en el `upperdir`, edita configs en el `upper`. Tú validas
en caliente. La IA nunca escribe en el FHS real hasta que tú haces `commit`.
- **Múltiples overlays** pueden coexistir (distintos `<id>`), p. ej. un experimento por
hipótesis. Cada uno es independiente.
- **El diario** sólo registra en `commit` — el ruido del experimento no contamina la historia.
## 5. Estados y reglas
| Estado | Significado | Transiciones |
|---|---|---|
| `none` | sin overlay activo | `try``active` |
| `active` | overlay montado, capa mutable encima | `commit``none` (+diario), `discard``none` |
Reglas:
- `commit`/`discard` sin overlay activo: no-op con aviso.
- Un `try` anidado crea un overlay nuevo apilado; se desmontan en orden LIFO.
- Si el sistema se apaga con overlays activos, al arranque `hammerd` los reporta y deja que el
humano decida (no auto-commit, no auto-discard — eso sería declarativo).
## 6. Interfaz
```rust
// hammer-cli (con helpers de hammer-core)
pub fn overlay_try(targets: &[PathBuf]) -> Result<OverlayId>;
pub fn overlay_commit(id: OverlayId, journal: &Journal) -> Result<CommitReport>;
pub fn overlay_discard(id: OverlayId) -> Result<()>;
pub fn overlay_status() -> Result<Vec<OverlayState>>;
```
CLI: `hammer try` · `hammer commit` · `hammer discard` · `hammer status`.
+86
View File
@@ -0,0 +1,86 @@
# SDD 05 — Diario de mutaciones
Este es el insight central de `hammer`: **el diario es tu configuración del sistema — sin ser
declarativa.** No declaras tu sistema en un archivo de texto antes de vivirlo; vives el sistema
imperativamente, y el propio sistema deriva, *a posteriori*, un mapa de tu desviación respecto
a la base limpia construida por el laboratorio.
Esto rompe la falsa dicotomía declarativo vs imperativo, y resuelve dos problemas a la vez:
- El de Arch/Gentoo: con los meses se pudren en archivos huérfanos porque el gestor pierde el
rastro de lo que el usuario hizo a mano.
- El de Nix: para evitar lo anterior, te ata las manos (todo debe declararse antes).
## 1. El mecanismo
Un daemon ultraligero (`hammerd`) usa **`fanotify`** a nivel de kernel para registrar
**exclusivamente las mutaciones** sobre los directorios del sistema (`/bin`, `/sbin`, `/lib`,
`/etc`). No bloquea ninguna acción. Te deja destruir, mover y crear archivos a tu antojo. Pero
anota, en silencio, un diario de modificaciones.
> `fanotify` sobre `inotify`: cobertura a nivel de superbloque/mount, eventos de modificación
> con info de PID/UID, y menor coste que vigilar recursivamente cada subdirectorio.
## 2. Qué registra (y qué no)
**Registra** mutaciones del FHS gestionado:
- creación / reemplazo / borrado de archivos en `/bin`, `/sbin`, `/lib`, `/etc`,
- qué los originó (PID, UID, y si fue una hidratación de `hammer`, el `artifact_hash`),
- ediciones de archivos de config en `/etc`.
**No registra:**
- actividad efímera en `/tmp`, `/run`, `/proc`, `/sys`, `/dev`,
- lecturas/ejecuciones (sólo mutaciones),
- nada dentro de un overlay activo (el ruido del experimento no entra; sólo el `commit`).
## 3. Formato del diario
Log **append-only de texto plano** (JSON-líneas), legible con `cat`/`grep`/`jq`. Cada línea es
un evento atómico. (Los timestamps los provee el daemon; el formato no impone reloj de
construcción.)
```jsonl
{"ts":"2026-06-06T18:40:01Z","op":"replace","path":"/bin/grep","by":{"uid":1000,"pid":4821},"artifact":"b3:9f2a…","note":"hydrate grep@perl-default"}
{"ts":"2026-06-06T18:41:12Z","op":"edit","path":"/etc/network.conf","by":{"uid":1000,"pid":4990},"diff_hash":"b3:7c1d…"}
{"ts":"2026-06-06T18:42:03Z","op":"manual-replace","path":"/bin/ls","by":{"uid":1000,"pid":5102},"artifact":null,"note":"usuario pisó ls a mano"}
```
`artifact: null` + `op: manual-replace` ⇒ el usuario reemplazó algo a mano fuera del flujo de
`hammer`. Eso no es un error; es información. El diario distingue "mutación trazable" (vino de
una receta/artefacto conocido) de "mutación opaca" (pisada manual), sin prohibir ninguna.
## 4. Exportar: del diario al `.swm`
`hammer export` lee el diario, lo recorta al delta relevante respecto a una **base** conocida,
y produce un manifiesto `.swm` ([SDD 06](06-swm-format.md)):
- **base** = el conjunto de commits fijados + versión de distro de los que partiste.
- **mutations** = las mutaciones trazables convertidas a recetas (`source_patch`) y las
ediciones de config (`config_edit`), más reglas de init si aplican.
- Las mutaciones **opacas** (pisadas manuales sin artefacto) se reportan como advertencia: no
se pueden compartir como receta reproducible. `hammer` te dice exactamente cuáles y por qué.
Así, **exportar el diario = exportar tu distro**, pero sólo la parte que es reproducible y
verificable. Lo que pisaste a ciegas, lo sabes y decides qué hacer con ello.
## 5. Replicar en otra máquina
No copias una config abstracta: exportas el diario de tus acciones humanas sobre el metal. La
otra máquina, partiendo de la misma base, **reproduce** las recetas y aplica las ediciones.
Tu sistema se replica desde su historia, no desde una declaración.
## 6. Interfaz
```rust
// hammerd / hammer-core
pub struct Journal { path: PathBuf }
impl Journal {
pub fn record(&self, ev: MutationEvent) -> Result<()>;
pub fn since(&self, base: &BaseRef) -> Result<Vec<MutationEvent>>;
pub fn export_swm(&self, base: &BaseRef) -> Result<(Swm, Vec<OpaqueWarning>)>;
}
```
CLI: `hammer journal` (ver/seguir el diario) · `hammer export [--base <ref>] > my.swm`.
+113
View File
@@ -0,0 +1,113 @@
# SDD 06 — Formato `.swm` (Software Mutación)
El `.swm` es la unidad de intercambio de `hammer`. **No es un paquete binario** (`.deb`,
`.rpm`, ni un `PKGBUILD` que compila quién-sabe-qué). Es un **manifiesto de la mutación**: la
receta de transformación sobre código fuente público, más las ediciones de configuración. El
receptor reproduce y verifica localmente; **nunca ejecuta tu binario** (ver
[SDD 09](09-trust-model.md)).
## 1. Esquema
```yaml
# my-grep-tweak.swm
swm_version: 1
base:
distro_version: "2026-06-06" # versión del set base de hammer
pins: # commits fijados de los upstreams relevantes
kernel: "a1b2c3d…"
musl: "e4f5a6b…"
# base_hash deriva de pins+distro_version; el receptor verifica compatibilidad
mutations:
- type: source_patch # recompilar una herramienta desde su fuente parcheada
repo: "git://git.savannah.gnu.org/grep.git"
commit: "a1b2c3d…" # commit fijado sobre el que aplica el patch
patch: | # parche .patch de git, embebido o por URL (patch_url)
--- a/src/main.c
+++ b/src/main.c
@@ ...
build:
compiler: "zig-cc"
target: "x86_64-linux-musl"
link: "static"
flags: ["--enable-perl-regexp"]
target_bin: "/bin/grep"
- type: config_edit # editar un archivo de config en texto plano
file: "/etc/network.conf"
inline_diff: |
- DHCP=yes
+ IP=192.168.1.100
- type: init_rule # añadir/ajustar una regla del init (ver SDD 07)
action: "watchdog"
service: "web"
command: "/bin/myweb -p 8080"
signature: # opcional pero recomendado (ver SDD 09)
by: "sergio"
alg: "ed25519"
sig: "base64…"
```
## 2. Tipos de mutación
| `type` | Qué describe | Cómo lo aplica el receptor |
|---|---|---|
| `source_patch` | recompilar una herramienta desde fuente parcheada | clona repo@commit → aplica patch → `hammer build` → hidrata en overlay |
| `config_edit` | edición de un archivo de config | aplica el `inline_diff` (3-way) sobre el archivo objetivo |
| `init_rule` | regla del bus de init | inyecta la regla vía `/run/init.control` / receta de servicio |
| `file_drop` | depositar un archivo de datos no compilable | escribe el archivo (con su hash declarado) en la ruta |
Cada `source_patch` es, en esencia, una `Recipe` ([SDD 02](02-build-lab.md)) serializada para
viajar. El `build` lleva el **compilador por mutación** (no global).
## 3. Aplicación (`hammer apply`)
```
hammer apply my-grep-tweak.swm
```
1. **Verifica la base.** ¿Los `pins`/`distro_version` son compatibles con el sistema local?
Si no, avisa qué difiere y se detiene (o `--force` bajo tu responsabilidad).
2. **Verifica la firma** (si existe) contra las claves de confianza locales.
3. **Reproduce.** Para cada `source_patch`: clona el repo al commit, aplica el patch, compila
en el lab local → obtiene un `artifact_hash`.
4. **Aísla.** Hidrata todo en un **overlay temporal** ([SDD 04](04-overlay.md)); aplica
`config_edit`/`init_rule`/`file_drop` en esa capa. **No toca el sistema base.**
5. **Valida.** Corres/pruebas en caliente. Si bien → `hammer commit`. Si mal → `hammer discard`.
Nunca se ejecuta un binario ajeno: sólo se transforma código fuente público con una receta que
tu propio laboratorio compila.
## 4. Determinismo y verificación cruzada
Como el lab es determinista ([SDD 02](02-build-lab.md)), el `artifact_hash` que produce tu
máquina debe coincidir con el del autor (si declara `expected_hash`). Coincidencia ⇒ obtuviste
exactamente el mismo binario, sin haberlo descargado. Discrepancia ⇒ algo difiere (toolchain,
pin, patch) y `hammer` te dice qué. Esto es "verificar, no confiar" en la práctica.
## 5. No-objetivos del formato
- **No** transporta binarios cocidos (salvo `file_drop` para datos no compilables, con hash).
- **No** es Turing-completo: es declarativo *para el intercambio*, aunque el sistema que
describe se viva imperativamente. (Lo declarativo aquí es honesto: describe una
transformación, no aprisiona tu sistema.)
- **No** asume una arquitectura: `target` viaja en cada `source_patch`.
## 6. Interfaz
```rust
// hammer-core
#[derive(Serialize, Deserialize)]
pub struct Swm { /* swm_version, base, mutations, signature */ }
impl Swm {
pub fn from_yaml(s: &str) -> Result<Swm>;
pub fn to_yaml(&self) -> Result<String>;
pub fn verify_base(&self, local: &BaseRef) -> BaseCompat;
pub fn verify_signature(&self, trust: &TrustStore) -> SigStatus;
}
```
CLI: `hammer apply <file.swm>` · `hammer export … > out.swm` · `hammer swm verify <file.swm>`.
+98
View File
@@ -0,0 +1,98 @@
# SDD 07 — Bus de init y de agente
El sistema se controla por **tuberías UNIX puras**, no por parseo de archivos `.service`
complejos. Hay dos canales, con propósitos distintos:
1. **`/run/init.control`** — FIFO de control **humano**, texto crudo. Ergonomía máxima.
2. **`/run/agent.sock`** — socket de control de **agente** (IA, scripts), JSON-líneas con
framing y autenticación. Parseable y fiable.
## 1. El init por eventos UNIX
Tu init no lee configuraciones pesadas: expone una API por FIFO. Cualquier proceso —un script,
un binario, o tú tecleando— controla el estado del sistema enviando bytes crudos:
```sh
echo "restart network" > /run/init.control
echo "start web" > /run/init.control
echo "stop web" > /run/init.control
```
Los servicios no se declaran en archivos pesados; se **inyectan dinámicamente**. El estado del
sistema es un flujo vivo y maleable que manipulas con herramientas estándar (`cat`, `echo`,
`grep`). Los servicios viven como directorios con un script `run` ejecutable (estilo s6/runit),
no como unidades declarativas opacas:
```
/etc/service/web/run # script ejecutable; el init lo supervisa
/etc/service/network/run
```
## 2. Por qué un segundo canal para la IA
El FIFO crudo es perfecto para humanos pero malo como API de máquina: no tiene framing fiable,
ni forma de devolver eventos correlacionados, ni de saber **quién** habla. La IA necesita:
- enviar comandos estructurados y recibir **eventos** correlacionados (éxito/fallo de un build,
crash de un servicio),
- **autenticación** del peer (que no cualquier proceso dispare builds o inyecte binarios),
- un protocolo pequeño, legible y versionado.
Por eso `/run/agent.sock` es un socket UNIX con:
- **framing** = JSON-líneas (un objeto JSON por línea, `\n`-terminado),
- **auth** = `SO_PEERCRED` (UID/GID/PID del peer verificados por el kernel) + capacidades que
el humano concede explícitamente (ver [SDD 08](08-ai-integration.md)),
- **versión** en el handshake.
## 3. Protocolo del bus de agente
### Handshake
```json
{"t":"hello","ver":1,"client":"claude-agent"}
{"t":"welcome","ver":1,"caps":["compile","query"]} // caps según lo que el humano concedió
```
### Comandos (cliente → hammerd)
| Comando | Forma | Efecto |
|---|---|---|
| `COMPILE` | `{"t":"compile","recipe":{…}}` o `{"repo","commit","patch","build"}` | dispara un build en el lab |
| `INJECT` | `{"t":"inject","artifact":"b3:…","target":"/bin/grep","overlay":"<id>"}` | hidrata en un overlay (o real si hay cap) |
| `QUERY` | `{"t":"query","what":"file","path":"/etc/network.conf"}` | estado de un archivo/servicio/artefacto |
| `INIT` | `{"t":"init","cmd":"start web"}` | proxy autenticado al control del init |
### Eventos (hammerd → cliente)
| Evento | Forma | Significado |
|---|---|---|
| `BUILD_READY` | `{"t":"build_ready","artifact":"b3:…","recipe":"grep"}` | build terminó OK |
| `BUILD_FAILED` | `{"t":"build_failed","recipe":"grep","log_tail":"…"}` | build falló, con cola de log |
| `CRASHED` | `{"t":"crashed","service":"web","code":127}` | un servicio supervisado murió |
| `MODIFIED` | `{"t":"modified","path":"/bin/grep","by":{…}}` | el diario registró una mutación |
Ejemplo del bucle de autorreparación: la IA modifica el binario del servidor web, este falla
al arrancar; `hammerd` emite `{"t":"crashed","service":"web","code":127}`; la IA, escuchando,
lee el log (texto plano en `/var/log`), corrige la fuente, recompila, y reenvía `start web`.
Para el humano, el sistema "se autorreparó" — pero cada paso quedó en el bus y en el diario.
## 4. Seguridad del bus
- **Sin auth, sin comandos.** Conexión que no completa `hello` válido se cierra.
- **Capacidades mínimas.** Por defecto un agente sólo obtiene `query`. `compile`, `inject` (a
overlay) e `inject-real` (al FHS real) se conceden explícita y separadamente.
- **`inject-real` es especial.** Inyectar al sistema real (no a overlay) requiere una capacidad
aparte y, por política, confirmación humana. El flujo recomendado de la IA es siempre overlay
+ `commit` humano.
- **Todo comando con efecto se registra** (en el diario y/o un audit log del bus).
## 5. Interfaz
```rust
// hammerd
pub async fn serve_agent_bus(sock: &Path, caps_policy: CapsPolicy) -> Result<()>;
pub fn init_control_send(line: &str) -> Result<()>; // escribe en /run/init.control
```
CLI de conveniencia: `hammer ctl "start web"` (humano) · la IA usa el socket directamente.
+113
View File
@@ -0,0 +1,113 @@
# SDD 08 — Integración de la IA
La IA es un **artesano hiperveloz en el taller**; el humano es **el dueño que decide si el
mueble entra a la casa o se va a la basura.** Esta sección define el bucle agéntico, sus
garantías de seguridad, y cómo una intención en lenguaje natural se vuelve un cambio real.
## 1. La terminal como runtime de la IA
La IA no es un chat en una ventana que te dice qué teclear. Opera directamente sobre el sistema
a través del **bus de agente** ([SDD 07](07-agent-bus.md)) y de un **overlay**
([SDD 04](04-overlay.md)). Su entorno de trabajo es el sistema real, con red de seguridad.
## 2. El bucle agéntico
```
intención (NL)
│ "que grep ignore binarios por defecto y use regex Perl,
│ y que la red use IP estática 192.168.1.100"
┌─────────────┐
│ PLAN │ la IA traduce la intención a un .swm: qué herramienta,
│ │ qué patch de fuente, qué flags, qué config editar
└──────┬──────┘
┌─────────────┐
│ BUILD │ COMPILE por el bus → el lab clona repo@commit, aplica patch,
│ (lab) │ compila estático con zig cc → artifact_hash
└──────┬──────┘
┌─────────────┐
│ TRY │ hammer try → overlay temporal; INJECT del artefacto y de las
│ (overlay) │ ediciones de config en la capa. El sistema real intacto.
└──────┬──────┘
┌─────────────┐
│ VERIFY │ corre el test harness; escucha BUILD_FAILED / CRASHED por el bus.
│ │ si falla → corrige fuente → recompila (vuelve a BUILD).
└──────┬──────┘
┌─────────────┐
│ PROPOSE │ la IA avisa: "listo, probé esto, aquí el diff y el resultado".
│ │ presenta el .swm y la evidencia.
└──────┬──────┘
┌─────────────┐
│ HUMANO │ tú decides: hammer commit (promueve + diario) o hammer discard.
│ decide │ La IA NUNCA promueve al FHS real por su cuenta (ver §4).
└─────────────┘
```
## 3. Por qué esta arquitectura se presta (y otras no)
- **Determinismo del lab** → la IA puede recompilar y volver atrás sin dramas; el resultado es
predecible.
- **Texto plano + binarios atómicos** → la IA lee/escribe archivos e inspecciona binarios con
`readelf`/`objdump`; no parsea bases de datos opacas.
- **musl legible** → el código C de la libc base es comprensible en el contexto de un LLM
(glibc es un laberinto de macros).
- **Enlazado estático** → una herramienta mutada corre sin romper enlaces de otros programas.
- **Mutabilidad + overlay** → agencia real con red de seguridad; sin rollbacks pesados.
En distros inmutables/declarativas, la IA tendría que lidiar con generadores de config y
políticas read-only; en tradicionales, no tendría provenance. `hammer` mantiene las **tuberías
expuestas y de baja entropía**, que es justo lo que un agente necesita.
## 4. Garantías de seguridad (no negociables)
1. **La IA trabaja en overlay por defecto.** Sin capacidad `inject-real`, no puede tocar el
FHS base. El flujo es siempre overlay → `commit` humano.
2. **Capacidades mínimas y explícitas.** El humano concede `query`/`compile`/`inject` por
separado vía la política del bus ([SDD 07](07-agent-bus.md)).
3. **Toda acción es reversible y registrada.** `discard` revierte el overlay; el `commit`
entra al diario firmado ([SDD 05](05-journal.md)).
4. **Recetas, no binarios.** La IA produce `.swm` (fuente + flags + config), no binarios
opacos. Lo que comparte es verificable por terceros ([SDD 06](06-swm-format.md)).
5. **El humano tiene la última palabra.** Ningún cambio se vuelve permanente sin `commit`.
## 5. De la intención al `.swm`: el rol del modelo
El traductor intención→`.swm` es un LLM. Detalles de modelo/API (Claude, herramientas, prompts)
se documentarán aparte cuando se implemente la Fase 6; aquí sólo fijamos el contrato:
- **Entrada:** intención en NL + contexto del sistema (qué herramientas hay, qué pins, estado
de servicios — todo consultable por `QUERY`).
- **Salida:** un `.swm` válido ([SDD 06](06-swm-format.md)) + un plan de verificación.
- **El modelo no ejecuta nada directamente:** emite comandos por el bus, que `hammerd` valida
contra las capacidades concedidas.
## 6. Lenguaje de consulta/transformación (visión, Fase posterior)
Para que la IA refiera partes del sistema sin rutas absolutas frágiles, un mini-lenguaje de
consulta sobre el grafo del sistema:
```
find /service/web -where "depends_on(libssl)" -> replace_with my_tls
```
El daemon traduce esa consulta estructurada a acciones reales del bus. La terminal sigue siendo
el lugar de validación final. Esto es visión, no Fase 0 — se diseñará cuando el bucle básico
esté probado.
## 7. Interfaz
```rust
// cliente del bus (puede vivir en un crate aparte hammer-agent en Fase 6)
pub trait AgentClient {
fn hello(&mut self) -> Result<Caps>;
fn compile(&mut self, recipe: &Recipe) -> Result<ArtifactHash>;
fn inject(&mut self, h: &ArtifactHash, target: &Path, overlay: OverlayId) -> Result<()>;
fn query(&mut self, q: Query) -> Result<QueryResult>;
fn events(&mut self) -> impl Iterator<Item = BusEvent>;
}
```
+96
View File
@@ -0,0 +1,96 @@
# SDD 09 — Modelo de confianza
La premisa de compartir en `hammer` es **"verificar, no confiar"**: nunca ejecutas un binario
ajeno; reproduces una receta sobre código fuente público y compruebas que obtienes el mismo
resultado. Esto sólo funciona si el build es determinista y si el intercambio está firmado y,
opcionalmente, anclado a un log de transparencia.
## 1. La cadena de confianza
```
código fuente público (repo@commit fijado)
│ + patch declarado en el .swm
build determinista en TU laboratorio local
│ → artifact_hash reproducible
¿coincide con el expected_hash del autor?
├─ sí → obtuviste exactamente su binario, sin descargarlo. Confianza derivada del código, no del autor.
└─ no → algo difiere (pin, patch, toolchain). hammer te dice qué. No promueves.
```
El binario del autor **nunca viaja**. Viaja la *receta*. La confianza no está en "el binario de
sergio es bueno", sino en "el código fuente público + esta transformación produce esto, y lo
verifiqué yo mismo".
## 2. Determinismo como cimiento
Todo esto descansa en que dos máquinas con la misma entrada produzcan el mismo
`artifact_hash` ([SDD 02](02-build-lab.md)). Requisitos prácticos de reproducibilidad:
- **Toolchain fijado.** El `zig`/compilador y su versión forman parte del hash de entrada.
- **Commits fijados.** Nada de `HEAD` ([ADR 0006](adr/0006-pinned-commits.md)).
- **Sandbox hermético.** Sin red durante el build, sin fugas del host
([SDD 02](02-build-lab.md) §3).
- **Eliminación de no-determinismo.** Timestamps embebidos, rutas absolutas, orden de archivos,
paralelismo no determinista → se normalizan (p. ej. `SOURCE_DATE_EPOCH`, orden estable).
Cuando un build no reproduce, es un **bug a corregir**, no una excepción a tolerar. La
reproducibilidad bit-a-bit es objetivo, no aspiración.
## 3. Firma del `.swm`
Un `.swm` puede ir firmado (Ed25519). La firma cubre el contenido del manifiesto (base +
mutations). Sirve para:
- **Autoría:** saber quién publicó la receta.
- **Integridad:** que no se alteró en tránsito.
La firma **no** sustituye la verificación reproducible — un autor firmado podría declarar un
`expected_hash` que no corresponde a su patch; por eso `hammer apply` **siempre** reproduce y
compara, firme o no.
## 4. Log de transparencia (visión, opt-in)
Para compartir en comunidad sin un repositorio central de binarios de confianza, los `.swm`
publicados pueden anclarse a un **log de transparencia append-only** (estilo Sigstore/Rekor):
- cada `.swm` publicado deja una entrada inmutable (hash del manifiesto + firma + timestamp),
- cualquiera puede auditar la historia: qué se publicó, por quién, cuándo,
- detecta sustituciones silenciosas y "split-view".
Esto es opt-in y de fase posterior; el modelo base (reproducir + firmar) ya da la garantía
fuerte: **no ejecutas binarios ajenos**.
## 5. TrustStore local
Claves públicas en las que el usuario confía para *autoría* (no para ejecución):
```
/var/lib/hammer/trust/
sergio.ed25519.pub
comunidad-foo.ed25519.pub
```
`hammer apply` reporta el estado de firma (`trusted` / `unknown-key` / `bad-sig` / `unsigned`)
pero la decisión de promover sigue dependiendo de la **verificación reproducible** + el
`commit` humano.
## 6. Resumen de garantías
| Amenaza | Mitigación |
|---|---|
| Ejecutar binario ajeno malicioso | No se ejecutan binarios ajenos; se reproduce desde fuente |
| Receta alterada en tránsito | Firma Ed25519 del `.swm` |
| Build no reproducible / backdoor en toolchain | Toolchain fijado en el hash + determinismo verificado |
| Sustitución silenciosa en la comunidad | Log de transparencia (opt-in) |
| Promoción accidental de algo malo | Overlay + `commit` humano explícito |
## 7. Interfaz
```rust
// hammer-core
pub fn sign_swm(swm: &Swm, key: &Ed25519PrivateKey) -> Signature;
pub fn verify_swm(swm: &Swm, trust: &TrustStore) -> SigStatus;
pub fn reproduce_and_compare(swm: &Swm, store: &Store) -> Result<Vec<HashMatch>>;
```
+80
View File
@@ -0,0 +1,80 @@
# 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 ▶ *empezamos aquí*
- [ ] `hammer-core`: tipos `Recipe`, `ArtifactHash`, `Store` (BLAKE3, layout `/store`).
- [ ] `hammer-build`: sandbox con `bubblewrap` + `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.
- **Hecho cuando:** una receta compila reproduciblemente y queda en el store.
### Fase 1 — Hidratación
- [ ] `hydrate(hash, target, mode)`: hardlink a FHS, `patchelf` para el caso dinámico.
- [ ] `hammer hydrate <hash> --into /`.
- **Hecho cuando:** un artefacto del store aparece como `/bin/<x>` real y ejecutable.
### 🎯 Primer entregable (Fase 0 + 1)
**Compilar `grep` estático desde su repo con un patch, hidratarlo a `/bin/grep`, y que corra**,
todo dentro de una VM/LXC Alpine. Esto prueba el corazón "fábrica funcional → FHS mutable".
### Fase 2 — Overlay de experimentación
- [ ] `try` / `commit` / `discard` / `status` sobre `overlayfs`.
- **Hecho cuando:** puedes romper `/bin` en un overlay y revertir con `discard`.
### Fase 3 — Diario de mutaciones
- [ ] `hammerd` con `fanotify` sobre `/bin`,`/sbin`,`/lib`,`/etc`.
- [ ] Log JSON-líneas append-only; `hammer journal`.
- **Hecho cuando:** toda mutación trazable/opaca queda registrada y consultable.
### Fase 4 — Formato y flujo `.swm`
- [ ] `Swm` (de/serialización YAML), `hammer export`, `hammer apply` (con overlay), `verify`.
- **Hecho cuando:** exportas un cambio, lo aplicas en otra máquina y reproduce idéntico.
### Fase 5 — Bus de agente
- [ ] `/run/init.control` (FIFO humano) + `/run/agent.sock` (JSON-líneas, `SO_PEERCRED`).
- [ ] Comandos `COMPILE`/`INJECT`/`QUERY`/`INIT`; eventos `BUILD_*`/`CRASHED`/`MODIFIED`.
- **Hecho cuando:** un cliente externo dispara un build y recibe el evento de fin por el socket.
### Fase 6 — Integración de la IA
- [ ] Cliente de agente; traductor intención NL → `.swm`; bucle plan→build→try→verify→propose.
- **Hecho cuando:** una intención en lenguaje natural produce un cambio probado en overlay,
presentado para `commit` humano.
---
## Track posterior — distro propia
- Reemplazar el init de Alpine por **tu init** (bus por pipes nativo).
- **Bootstrap from-scratch** con `zig`/`musl-cross-make` (Stage 0/1/2 → imagen destino).
- 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).
- Mini-lenguaje de consulta del sistema para la IA ([SDD 08](08-ai-integration.md) §6).
---
## Estado actual
- ✅ Repo y workspace Rust inicializados.
- ✅ SDDs y ADRs redactados.
- ✅ Esqueletos de crates que compilan (`hammer build` stub).
- ⏭️ Siguiente: implementar el sandbox de build real (Fase 0) y montar la VM/LXC Alpine.
## 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`.
+33
View File
@@ -0,0 +1,33 @@
# Documentación de diseño de hammer
Estos documentos son la fuente de verdad del diseño. El código los implementa; cuando haya
discrepancia, o se corrige el código o se actualiza el SDD con un commit que explique por qué.
## Software Design Documents (SDD)
| # | Documento | Qué cubre |
|---|---|---|
| 00 | [Visión y filosofía](00-vision.md) | Por qué existe, qué problema resuelve, la actitud de ingeniería |
| 01 | [Arquitectura general](01-architecture.md) | El modelo de dos mundos, componentes, flujo de datos |
| 02 | [El laboratorio de build](02-build-lab.md) | Sandbox, `zig cc`, recetas, CAS, grafo de dependencias |
| 03 | [Hidratación y store](03-hydration.md) | Store content-addressed, hardlinks a FHS, patchelf, rollback |
| 04 | [Overlay de experimentación](04-overlay.md) | overlayfs en caliente, try/commit/discard |
| 05 | [Diario de mutaciones](05-journal.md) | fanotify, log append-only, config-sin-ser-declarativa |
| 06 | [Formato `.swm`](06-swm-format.md) | Manifiesto de mutación compartible, esquema, firma |
| 07 | [Bus de init y de agente](07-agent-bus.md) | `/run/init.control`, `/run/agent.sock`, protocolo |
| 08 | [Integración de la IA](08-ai-integration.md) | El bucle agéntico, seguridad, intención → `.swm` |
| 09 | [Modelo de confianza](09-trust-model.md) | Reproducibilidad, verificar-no-confiar, log de transparencia |
| 10 | [Roadmap](10-roadmap.md) | Fases, MVP, primer entregable |
## Architecture Decision Records (ADR)
Decisiones tomadas, con su contexto y consecuencias. Ver [`adr/`](adr/).
| # | Decisión |
|---|---|
| 0001 | [Rust para el tooling y daemons](adr/0001-rust-for-tooling.md) |
| 0002 | [Validar sobre Alpine antes de la distro propia](adr/0002-alpine-first.md) |
| 0003 | [`zig cc` como compilador por defecto del lab](adr/0003-zig-cc-builder.md) |
| 0004 | [No escribir nuestro propio Nix](adr/0004-no-custom-nix.md) |
| 0005 | [Hidratación por hardlinks](adr/0005-hardlink-hydration.md) |
| 0006 | [Commits fijados, no HEAD vivo](adr/0006-pinned-commits.md) |
+30
View File
@@ -0,0 +1,30 @@
# ADR 0001 — Rust para el tooling y los daemons
- **Estado:** aceptada
- **Fecha:** 2026-06-06
## Contexto
Necesitamos un lenguaje para `hammerd`, las CLIs y el daemon del diario. Candidatos: Rust, Go,
Zig. El sistema manipula ELF, sockets UNIX, `fanotify`, `overlayfs`, namespaces, y produce
binarios que deben correr en un userland musl, posiblemente estáticos.
## Decisión
**Rust** para todo el tooling y los daemons.
## Razones
- **Binarios estáticos-musl** de primera (`x86_64-unknown-linux-musl`), justo nuestro target.
- Ecosistema fuerte para lo que hacemos: parsing/edición ELF, sockets, `nix`/`rustix` para
syscalls (`fanotify`, namespaces), `blake3`, `serde`.
- Seguridad de memoria sin GC; importante en un daemon de larga vida con privilegios.
- El autor lo eligió explícitamente.
## Consecuencias
- Curva de entrada más alta que Go; asumida.
- Compilación del compilador del lab es **ortogonal**: escribimos `hammer` en Rust, el lab
invoca `zig cc` para compilar C/C++ upstream. Sin conflicto (ver [ADR 0003](0003-zig-cc-builder.md)).
- Perfil `release` ya configurado para binarios pequeños (`opt-level=z`, `lto`, `strip`,
`panic=abort`).
+34
View File
@@ -0,0 +1,34 @@
# ADR 0002 — Validar sobre Alpine antes de la distro propia
- **Estado:** aceptada
- **Fecha:** 2026-06-06
## Contexto
El instinto es construir la distro desde el bootstrap (toolchain, init, userland) y luego poner
la capa AI-nativa encima. Pero el bootstrap from-scratch es el trabajo más largo y arriesgado
del proyecto (meses), y **no es donde está la innovación**. La innovación es la capa AI-nativa:
build determinista + mutable + diario + `.swm`.
## Decisión
Montar y validar toda la capa de `hammer` **sobre Alpine Linux** (Fases 06). Sólo después,
una vez probado el concepto, bajar a la distro propia (track posterior).
## Razones
- Alpine ya es **musl + busybox + FHS mutable + minimalista** — exactamente nuestra filosofía,
ya cocida y mantenida.
- Permite validar el bucle disruptivo (overlay, diario, `.swm`, bus de IA) **en semanas**, no
meses.
- Todo el tooling (Rust) y el lab (`zig cc`) se reusan sin retrabajo cuando bajemos a la distro
propia: cambia el *suelo*, no las herramientas.
## Consecuencias
- En la fase Alpine, `/store` y `/var/lib/hammer` viven dentro del rootfs de Alpine; el init es
el de Alpine (OpenRC), no el propio. El bus por pipes nativo del init propio es track posterior.
- No probamos el bootstrap completo en esta fase; `zig cc` cubre la compilación sin necesitar
bootstrap (ver [ADR 0003](0003-zig-cc-builder.md)).
- Riesgo: alguna fricción musl/static en Alpine que no represente la distro propia. Aceptable —
Alpine es musl real, la fricción es representativa.
+36
View File
@@ -0,0 +1,36 @@
# ADR 0003 — `zig cc` como compilador por defecto del laboratorio
- **Estado:** aceptada
- **Fecha:** 2026-06-06
## Contexto
El lab compila C/C++ upstream de forma hermética hacia un target musl, idealmente estático, y
debe evitar contaminación del host. La opción clásica es construir un cross-toolchain (Stage 0)
a mano: largo y frágil.
## Decisión
Usar **`zig cc` como compilador por defecto** del lab, con el compilador **configurable por
receta** (escotilla a `clang`/`gcc`). Esto es ortogonal al lenguaje del tooling, que es Rust
([ADR 0001](0001-rust-for-tooling.md)): Rust orquesta, `zig cc` aprieta el tornillo.
## Razones
- **Mata el bootstrap:** un solo binario (~45 MB) provee compilador C/C++ + **musl** + headers,
para múltiples arquitecturas. Resuelve gran parte del Stage 0/1.
- **Hermético por defecto:** trae sus propios headers de libc, no chupa `/usr/include` del
host. Justo nuestro requisito de no-contaminación.
- **Cross-compile trivial:** `-target x86_64-linux-musl`, aarch64, etc. Clave para el track de
distro propia y otras arquitecturas.
- **Determinista:** ayuda directamente al hashing CAS y a la reproducibilidad
([SDD 09](../09-trust-model.md)).
## Consecuencias / riesgos
- `zig cc` es clang por debajo: algunos paquetes con **gcc-ismos** (extensiones GNU, flags
específicos) o `configure` de autotools mal detectado **fallarán**. Por eso el compilador es
**por receta**, no global: escotilla a `clang`/`gcc` cuando haga falta.
- No todo upstream compila limpio con clang/musl independientemente del compilador; es fricción
inherente a musl/static, no de `zig`.
- La versión de `zig` forma parte del hash de entrada del build (reproducibilidad).
+34
View File
@@ -0,0 +1,34 @@
# ADR 0004 — No escribir nuestro propio Nix
- **Estado:** aceptada
- **Fecha:** 2026-06-06
## Contexto
Es tentador escribir desde cero un motor completo de grafo de dependencias con caché
content-addressed, resolución, y aislamiento — "nuestro propio Nix". El concepto de hashing de
derivaciones es el núcleo de nuestra fábrica.
## Decisión
**No** construir un motor de build genérico tipo Nix desde cero. Implementamos lo **mínimo**
que necesita `hammer`: un builder con recetas explícitas, sandbox `bubblewrap`, hashing CAS
BLAKE3 de entradas conocidas, y un DAG simple de dependencias declaradas. Nada de un lenguaje
funcional, evaluación perezosa, ni resolución general de paquetes.
## Razones
- Un motor de grafo hermético con caché correcta es, por sí solo, un proyecto de **años**.
No es donde está el valor de `hammer`.
- Nuestro valor es el **pegamento AI-nativo** (overlay + diario + `.swm` + bus), no reescribir
teoría de build ya resuelta.
- El alcance mínimo (recetas explícitas + CAS + DAG) cubre las Fases 06 sin esa complejidad.
## Consecuencias
- No tendremos resolución automática de dependencias estilo distro completa al principio; las
recetas declaran sus `deps` explícitamente. Suficiente para el MVP.
- Si en el futuro hiciera falta más potencia de grafo, se evaluará **usar Nix como backend de
build** (no reescribirlo) y sólo hidratar su salida a FHS. Decisión diferida.
- Mantiene el código del lab pequeño, auditable y comprensible — coherente con la filosofía de
baja entropía.
+37
View File
@@ -0,0 +1,37 @@
# ADR 0005 — Hidratación por hardlinks
- **Estado:** aceptada
- **Fecha:** 2026-06-06
## Contexto
Hay que llevar artefactos del store content-addressed al FHS real. Tres opciones:
1. **Symlinks al store** (estilo Nix): rutas crípticas, `/bin` lleno de enlaces a un almacén
oculto. Es justo el "pueblo fantasma" que rechazamos.
2. **Copia ciega** al FHS: rutas reales, pero se pierde el dedup y la relación con el store
(rollback más torpe, más disco).
3. **Hardlinks** del store al FHS.
## Decisión
**Hidratación por hardlinks** como modo por defecto (con `patchelf` para normalizar el caso
dinámico antes del enlace).
## Razones
- **Rutas reales:** `/bin/grep` es un archivo de verdad, no un symlink a `/store/...`.
Estructura familiar, control total en la terminal.
- **Dedup:** el hardlink comparte el inode del store; cero copia mientras no se modifique.
- **Mutabilidad con red de seguridad:** pisar `/bin/grep` en caliente **rompe el hardlink**
(copy-on-write manual); el artefacto del store queda intacto como base de rollback.
- **Rollback simple:** re-hidratar restaura el hardlink original desde el store.
## Consecuencias
- Hardlinks requieren que store y FHS estén en el **mismo filesystem**. En la fase Alpine viven
ambos en el rootfs → OK. En la distro propia se tendrá en cuenta al particionar
([SDD 10](../10-roadmap.md)).
- El caso dinámico necesita `patchelf` (set-interpreter + set-rpath) **antes** del hardlink
([SDD 03](../03-hydration.md) §4).
- El GC del store debe contar hardlinks/alcanzabilidad para no borrar artefactos vivos.
+38
View File
@@ -0,0 +1,38 @@
# ADR 0006 — Commits fijados, no `HEAD` vivo
- **Estado:** aceptada
- **Fecha:** 2026-06-06
## Contexto
La idea original habla de "repositorios vivos" apuntando al `HEAD` de los upstreams. Romántico,
pero `HEAD` se rompe a diario y destruye el determinismo que justifica todo el laboratorio
funcional. Sin entrada estable no hay hash estable, no hay caché fiable, y no hay `.swm`
verificable.
## Decisión
El lab compila **siempre contra commits fijados** (un lockfile, `pins.toml`). "Repositorio
vivo" significa **rastreo automatizado del upstream con snapshots fijados**, no `HEAD` ciego.
## Razones
- **Reproducibilidad:** entrada idéntica ⇒ hash idéntico ⇒ salida idéntica
([SDD 02](../02-build-lab.md), [SDD 09](../09-trust-model.md)).
- **Avance ordenado del grafo:** al subir un pin, sólo cambian ese nodo y sus dependientes;
sabes exactamente qué commit causó qué fallo.
- **`.swm` verificable:** dos máquinas con el mismo pin+patch producen el mismo binario; eso es
lo que permite "verificar, no confiar".
## Mecanismo de "vivo"
- `pins.toml` mapea `nombre → commit` por upstream.
- `hammer update [<pkg>]` consulta el upstream, propone subir el pin al nuevo commit, y
reconstruye sólo lo afectado. El humano (o la IA) decide cuándo avanzar.
- Análogo al `flake.lock` de Nix, pero imperativo y bajo tu control.
## Consecuencias
- No hay sorpresas por `HEAD` cambiando bajo tus pies.
- Mantener los pins al día es una acción explícita (deseable: lo controlas tú/la IA).
- El pin (commit) forma parte del `artifact_hash`; cambiarlo invalida la caché de ese nodo.
+6
View File
@@ -0,0 +1,6 @@
[toolchain]
# Pin a channel for reproducible local builds. Bump deliberately.
channel = "stable"
components = ["rustfmt", "clippy"]
# musl target for static userland binaries (install with: rustup target add x86_64-unknown-linux-musl)
targets = ["x86_64-unknown-linux-musl"]