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