commit 8bf1623044075274682e4351a0f8e120047c2bed Author: sergio Date: Sat Jun 6 19:06:04 2026 +0000 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..68d4f536 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..cab0b2bc --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..3df1b7a3 --- /dev/null +++ b/Cargo.toml @@ -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 "] +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" diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..caaf261c --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 00000000..42f49ea8 --- /dev/null +++ b/README.md @@ -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. diff --git a/crates/hammer-build/Cargo.toml b/crates/hammer-build/Cargo.toml new file mode 100644 index 00000000..7e87058e --- /dev/null +++ b/crates/hammer-build/Cargo.toml @@ -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 diff --git a/crates/hammer-build/src/lib.rs b/crates/hammer-build/src/lib.rs new file mode 100644 index 00000000..db836ecb --- /dev/null +++ b/crates/hammer-build/src/lib.rs @@ -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 { + let dep_hashes: Vec = 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 { + 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)" + ))) +} diff --git a/crates/hammer-build/src/sandbox.rs b/crates/hammer-build/src/sandbox.rs new file mode 100644 index 00000000..f62cceae --- /dev/null +++ b/crates/hammer-build/src/sandbox.rs @@ -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 { + 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 + } +} diff --git a/crates/hammer-cli/Cargo.toml b/crates/hammer-cli/Cargo.toml new file mode 100644 index 00000000..ec6e3fb9 --- /dev/null +++ b/crates/hammer-cli/Cargo.toml @@ -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 diff --git a/crates/hammer-cli/src/main.rs b/crates/hammer-cli/src/main.rs new file mode 100644 index 00000000..72466f84 --- /dev/null +++ b/crates/hammer-cli/src/main.rs @@ -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, + }, + /// [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(()) +} diff --git a/crates/hammer-core/Cargo.toml b/crates/hammer-core/Cargo.toml new file mode 100644 index 00000000..e86602a1 --- /dev/null +++ b/crates/hammer-core/Cargo.toml @@ -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 diff --git a/crates/hammer-core/src/hash.rs b/crates/hammer-core/src/hash.rs new file mode 100644 index 00000000..f912751d --- /dev/null +++ b/crates/hammer-core/src/hash.rs @@ -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) -> 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: `-`. + 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"); + } +} diff --git a/crates/hammer-core/src/lib.rs b/crates/hammer-core/src/lib.rs new file mode 100644 index 00000000..1454cfa1 --- /dev/null +++ b/crates/hammer-core/src/lib.rs @@ -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 = std::result::Result; diff --git a/crates/hammer-core/src/recipe.rs b/crates/hammer-core/src/recipe.rs new file mode 100644 index 00000000..53ede4d2 --- /dev/null +++ b/crates/hammer-core/src/recipe.rs @@ -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, +} + +#[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, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Deps { + #[serde(default)] + pub build: Vec, + #[serde(default)] + pub runtime: Vec, +} + +/// 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 { + // 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> { + let mut v: Vec> = 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 + } +} diff --git a/crates/hammer-core/src/store.rs b/crates/hammer-core/src/store.rs new file mode 100644 index 00000000..d953bd26 --- /dev/null +++ b/crates/hammer-core/src/store.rs @@ -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) -> crate::Result { + 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 { + Err(crate::Error::Store( + "sellado en el store pendiente (Fase 0)".into(), + )) + } +} diff --git a/crates/hammer-core/src/swm.rs b/crates/hammer-core/src/swm.rs new file mode 100644 index 00000000..bd6abb68 --- /dev/null +++ b/crates/hammer-core/src/swm.rs @@ -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, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub signature: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Base { + pub distro_version: String, + #[serde(default)] + pub pins: std::collections::BTreeMap, +} + +#[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, + #[serde(default, skip_serializing_if = "Option::is_none")] + patch_url: Option, + build: SwmBuild, + target_bin: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + expected_hash: Option, + }, + 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, + }, +} + +#[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, +} + +#[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 { + serde_yaml::from_str(s).map_err(|e| crate::Error::Serde(e.to_string())) + } + + pub fn to_yaml(&self) -> crate::Result { + 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")); + } +} diff --git a/crates/hammerd/Cargo.toml b/crates/hammerd/Cargo.toml new file mode 100644 index 00000000..eb8a5e22 --- /dev/null +++ b/crates/hammerd/Cargo.toml @@ -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 diff --git a/crates/hammerd/src/main.rs b/crates/hammerd/src/main.rs new file mode 100644 index 00000000..83960269 --- /dev/null +++ b/crates/hammerd/src/main.rs @@ -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(()) +} diff --git a/docs/00-vision.md b/docs/00-vision.md new file mode 100644 index 00000000..b3ce0d0b --- /dev/null +++ b/docs/00-vision.md @@ -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)). diff --git a/docs/01-architecture.md b/docs/01-architecture.md new file mode 100644 index 00000000..dea3c8c7 --- /dev/null +++ b/docs/01-architecture.md @@ -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/- │ │ │ └───┬───────────────┘ │ + │ └───────────────────────┘ │ │ │ │ + │ │ │ ┌───┴───────────────┐ │ + └──────────────────────────────────────────────┘ │ │ 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) + -/ 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)). diff --git a/docs/02-build-lab.md b/docs/02-build-lab.md new file mode 100644 index 00000000..49aa10f3 --- /dev/null +++ b/docs/02-build-lab.md @@ -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; +pub fn artifact_hash(recipe: &Recipe, store: &Store) -> Result; +``` + +CLI: `hammer build ` → imprime el hash del artefacto sellado. diff --git a/docs/03-hydration.md b/docs/03-hydration.md new file mode 100644 index 00000000..634f5dba --- /dev/null +++ b/docs/03-hydration.md @@ -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/-/ 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//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 ` — loader estándar del sistema. +2. `patchelf --set-rpath /lib:/usr/lib ` — 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; + pub fn path_of(&self, h: &ArtifactHash) -> PathBuf; + pub fn gc(&self, roots: &[ArtifactHash]) -> Result; +} + +// hammer-build +pub fn hydrate(h: &ArtifactHash, store: &Store, target_fhs: &Path, mode: LinkMode) + -> Result>; +``` + +CLI: `hammer hydrate [--into /]` — proyecta el artefacto al FHS (real o de overlay). diff --git a/docs/04-overlay.md b/docs/04-overlay.md new file mode 100644 index 00000000..952ded40 --- /dev/null +++ b/docs/04-overlay.md @@ -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//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 ``), 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; +pub fn overlay_commit(id: OverlayId, journal: &Journal) -> Result; +pub fn overlay_discard(id: OverlayId) -> Result<()>; +pub fn overlay_status() -> Result>; +``` + +CLI: `hammer try` · `hammer commit` · `hammer discard` · `hammer status`. diff --git a/docs/05-journal.md b/docs/05-journal.md new file mode 100644 index 00000000..55189f2c --- /dev/null +++ b/docs/05-journal.md @@ -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>; + pub fn export_swm(&self, base: &BaseRef) -> Result<(Swm, Vec)>; +} +``` + +CLI: `hammer journal` (ver/seguir el diario) · `hammer export [--base ] > my.swm`. diff --git a/docs/06-swm-format.md b/docs/06-swm-format.md new file mode 100644 index 00000000..32ca690c --- /dev/null +++ b/docs/06-swm-format.md @@ -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; + pub fn to_yaml(&self) -> Result; + pub fn verify_base(&self, local: &BaseRef) -> BaseCompat; + pub fn verify_signature(&self, trust: &TrustStore) -> SigStatus; +} +``` + +CLI: `hammer apply ` · `hammer export … > out.swm` · `hammer swm verify `. diff --git a/docs/07-agent-bus.md b/docs/07-agent-bus.md new file mode 100644 index 00000000..7ddcc80b --- /dev/null +++ b/docs/07-agent-bus.md @@ -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":""}` | 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. diff --git a/docs/08-ai-integration.md b/docs/08-ai-integration.md new file mode 100644 index 00000000..76525b76 --- /dev/null +++ b/docs/08-ai-integration.md @@ -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; + fn compile(&mut self, recipe: &Recipe) -> Result; + fn inject(&mut self, h: &ArtifactHash, target: &Path, overlay: OverlayId) -> Result<()>; + fn query(&mut self, q: Query) -> Result; + fn events(&mut self) -> impl Iterator; +} +``` diff --git a/docs/09-trust-model.md b/docs/09-trust-model.md new file mode 100644 index 00000000..7667a65c --- /dev/null +++ b/docs/09-trust-model.md @@ -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>; +``` diff --git a/docs/10-roadmap.md b/docs/10-roadmap.md new file mode 100644 index 00000000..a08be105 --- /dev/null +++ b/docs/10-roadmap.md @@ -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 0–6:** 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 ` → 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 --into /`. +- **Hecho cuando:** un artefacto del store aparece como `/bin/` 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`. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..61771976 --- /dev/null +++ b/docs/README.md @@ -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) | diff --git a/docs/adr/0001-rust-for-tooling.md b/docs/adr/0001-rust-for-tooling.md new file mode 100644 index 00000000..eebfd72a --- /dev/null +++ b/docs/adr/0001-rust-for-tooling.md @@ -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`). diff --git a/docs/adr/0002-alpine-first.md b/docs/adr/0002-alpine-first.md new file mode 100644 index 00000000..bd6d1f79 --- /dev/null +++ b/docs/adr/0002-alpine-first.md @@ -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 0–6). 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. diff --git a/docs/adr/0003-zig-cc-builder.md b/docs/adr/0003-zig-cc-builder.md new file mode 100644 index 00000000..db0da337 --- /dev/null +++ b/docs/adr/0003-zig-cc-builder.md @@ -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). diff --git a/docs/adr/0004-no-custom-nix.md b/docs/adr/0004-no-custom-nix.md new file mode 100644 index 00000000..c750c95b --- /dev/null +++ b/docs/adr/0004-no-custom-nix.md @@ -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 0–6 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. diff --git a/docs/adr/0005-hardlink-hydration.md b/docs/adr/0005-hardlink-hydration.md new file mode 100644 index 00000000..90a27cce --- /dev/null +++ b/docs/adr/0005-hardlink-hydration.md @@ -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. diff --git a/docs/adr/0006-pinned-commits.md b/docs/adr/0006-pinned-commits.md new file mode 100644 index 00000000..a8ab3957 --- /dev/null +++ b/docs/adr/0006-pinned-commits.md @@ -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 []` 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. diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..2f915aa0 --- /dev/null +++ b/rust-toolchain.toml @@ -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"]