takana etapa 5d: comentarios de crates, CLAUDE.md y el skill de granja

205 lineas en 73 ficheros de crates, mas la prosa de CLAUDE.md y del skill,
que se me habian quedado afuera de los barridos anteriores (no eran ni recetas
ni docs/ ni scripts/).

EL BARRIDO ANCHO ESTUVO A UN COMMIT DE ROMPER EL CORPUS ENTERO.

El primer intento reescribia los .rs completos, no solo los comentarios. Entre
las lineas de codigo que tocaba estaban SIETE etiquetas de separacion de
dominio, que son ENTRADA DE HASH:

  b"hammer-tree-v1"        <- el prefijo de ArtifactHash::of_tree (hash.rs:60)
  b"hammer-seed-v1"           la funcion que hashea TODOS los artefactos:
  b"hammer-stage1-rootfs-v2"  cambiarla mueve los 4750 hashes del store
  b"hammer-product-rootfs-v3"
  b"hammer-product-attested-v2"
  b"hammer-builder-rootfs-v1"
  b"hammer-attest-dev-rootkey-0001!!"  <- clave raiz de atestacion, [u8;32]

Revertido y rehecho solo sobre comentarios, esquivando ademas las cadenas
crudas de Rust (r#"..."#) porque el SYSTEM_PROMPT del traductor tiene lineas
que empiezan como comentario.

Controles: las 7 etiquetas siguen ahi, el diff toca CERO lineas de codigo,
605 tests en verde y el hash de zlib sigue en b3:dc363f26.

La leccion es la misma de toda esta etapa: un literal que parece prosa puede
ser entrada de hash, y la unica forma de saberlo es mirar donde se usa.
This commit is contained in:
Sergio
2026-09-09 19:38:33 +00:00
parent 1b7e6f947b
commit b818f5249f
61 changed files with 211 additions and 211 deletions
+2 -2
View File
@@ -3,7 +3,7 @@ name: granja
description: Cómo se compila en este proyecto — dónde está el worker, cómo se enchufa, y por qué NO se levantan cajas de pago. Usar al empezar cualquier trabajo de build, cosecha o estado de la granja.
---
# La granja de hammer
# La granja de takana
## Lo primero, porque cuesta dinero equivocarse
@@ -32,7 +32,7 @@ worker vivo» aunque el worker esté compilando. Comprobar el fichero antes de c
## Reglas que no son estilo
1. **Todo `hammer build` bajo `flock -o work/.farm-build.lock`.** El `-o` no es opcional: sin él un
1. **Todo `takana build` bajo `flock -o work/.farm-build.lock`.** El `-o` no es opcional: sin él un
proceso fugado hereda el fd y deja la granja sin compilar, en silencio. Ver regla 1 de CLAUDE.md.
2. **Un lock tomado sin build a la vista se diagnostica con `fuser -v work/.farm-build.lock`**, que
nombra al fugado. Pasó dos veces el mismo día.
+4 -4
View File
@@ -4,7 +4,7 @@ Este repo lo trabajan **varios agentes a la vez** (hoy: frente granja/store y fr
aquí abajo no son preferencias de estilo: son las dos formas conocidas de que un agente destruya el
trabajo de otro sin enterarse. El resto del diseño está en `docs/`.
## 1. Todo `hammer build` va envuelto en `flock`
## 1. Todo `takana build` va envuelto en `flock`
```sh
flock work/.farm-build.lock ./target/release/takana --store ./store build <receta>
@@ -16,7 +16,7 @@ Para una tanda, tomar el lock una sola vez y no por receta:
flock work/.farm-build.lock bash -c 'for r in ...; do ./target/release/takana --store ./store build "$r"; done'
```
**Por qué.** `hammer build` comparte `work/sources/<dep>-<sha>` entre todas las recetas. Dos builds
**Por qué.** `takana build` comparte `work/sources/<dep>-<sha>` entre todas las recetas. Dos builds
concurrentes que compartan una dependencia se pisan el árbol de fuentes: uno hace fetch y lo borra
mientras el otro lo usa, y **el árbol queda roto para siempre** — reintentar no lo arregla. Medido a
escala: al invalidar libdrm, ~93 de 205 recetas KDE murieron con `/src/.zwrap/cc is not a full path
@@ -43,8 +43,8 @@ el estilo `exec 9>` + `flock 9`, que es vulnerable igual** (haría falta `9>&-`
conocida, no barrida.
`scripts/farm/farm-worker-loop.sh` y `campana-deuda.sh` ya toman **ese mismo fichero de lock**, así
que usarlo nos serializa con la granja además de entre nosotros. **No está dentro de `hammer build`
a propósito**: esos scripts lo toman por fuera y hammer se bloquearía contra ellos.
que usarlo nos serializa con la granja además de entre nosotros. **No está dentro de `takana build`
a propósito**: esos scripts lo toman por fuera y takana se bloquearía contra ellos.
## 1 bis. El worker es el LXC PRESTADO, y es GRATIS: no se levanta nada en Hetzner
+1 -1
View File
@@ -5,7 +5,7 @@ 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)."
description = "Daemon de takana: bus de agente (/run/agent.sock) y diario de mutaciones (fanotify)."
[[bin]]
name = "hammerd"
+4 -4
View File
@@ -1,14 +1,14 @@
//! Adaptador de transporte arje-bus → bus de agente de hammer (último tramo de B.2).
//! Adaptador de transporte arje-bus → bus de agente de takana (último tramo de B.2).
//!
//! arje (el init/PID 1) supervisa cada Ente y, al morir, difunde `BusEvent` por su bus
//! (`arje-bus`: `BusRequest::Subscribe` + `BusPayload::Event`). Este módulo se **suscribe** a
//! ese stream y traduce cada evento al vocabulario de hammer (`crashes::Lifecycle` →
//! ese stream y traduce cada evento al vocabulario de takana (`crashes::Lifecycle` →
//! `Event::Crashed`), publicándolo en el [`EventBus`] → `/run/agent.sock` → la capa de IA.
//!
//! ## Por qué un mirror y no una dependencia de `arje-bus`
//!
//! `arje-bus` arrastra el grafo de crates de arje (arje-card → card-core → …). Acoplarlo aquí
//! rompería el build hermético/standalone de hammer. En su lugar **releemos el frame postcard**:
//! rompería el build hermético/standalone de takana. En su lugar **releemos el frame postcard**:
//! el bus usa frames `u32-BE-len + postcard(BusMessage)`, y reproducimos sólo el subconjunto que
//! un suscriptor necesita. El layout está verificado byte-a-byte contra `arje-bus` real (mismas
//! versiones de `ulid` 1.2 y `postcard` 1.1; `ulid` serializa como string). Si arje reordena las
@@ -90,7 +90,7 @@ enum LifecycleStatus {
}
impl BusEvent {
/// Traduce el evento de arje a la señal normalizada de hammer. El `label` del Ente es el
/// Traduce el evento de arje a la señal normalizada de takana. El `label` del Ente es el
/// `service`; una muerte por señal usa la convención de shell `128 + signum` para que el
/// código sea siempre ≠ 0. `None` = evento consumido sin señal (Parked/Refloored: son
/// vaivén del piso gráfico, no ciclo de vida que le importe a la capa de IA).
+1 -1
View File
@@ -32,7 +32,7 @@ pub fn ensure_fifo(path: &Path) -> std::io::Result<PathBuf> {
Err(e) => return Err(e),
}
// `nix::unistd::mkfifo` con permisos 0o660: owner + group escriben/leen, world nada.
// El humano normalmente está en el grupo del daemon (p. ej. `hammer`); la IA agente
// El humano normalmente está en el grupo del daemon (p. ej. `takana`); la IA agente
// entra por el socket.
use nix::sys::stat::Mode;
nix::unistd::mkfifo(path, Mode::S_IRUSR | Mode::S_IWUSR | Mode::S_IRGRP | Mode::S_IWGRP)
+1 -1
View File
@@ -8,7 +8,7 @@
//!
//! Este módulo es el **sink** de ese flujo (B.2 "exponer el CRASHED a la capa de IA"):
//! traduce la señal de ciclo de vida normalizada al vocabulario del bus de agente de
//! hammer (`Event::Crashed`) y la bombea al [`EventBus`] → toda conexión del
//! takana (`Event::Crashed`) y la bombea al [`EventBus`] → toda conexión del
//! `/run/agent.sock` (la capa de IA) la recibe.
//!
//! ## Frontera de acoplamiento
+4 -4
View File
@@ -1,5 +1,5 @@
//! `hammerd` — daemon de hammer. Tres responsabilidades:
//! 1. Diario de mutaciones: fanotify sobre /bin,/sbin,/lib,/etc → `hammer-journal`.
//! `hammerd` — daemon de takana. Tres responsabilidades:
//! 1. Diario de mutaciones: fanotify sobre /bin,/sbin,/lib,/etc → `takana-journal`.
//! 2. FIFO de control humano: `/run/init.control` (texto crudo, una línea por comando).
//! 3. Bus de agente: `/run/agent.sock` (JSON-líneas, SO_PEERCRED). Ver `docs/07-agent-bus.md`.
//!
@@ -20,7 +20,7 @@ mod events;
mod watcher;
#[derive(Parser)]
#[command(name = "hammerd", version, about = "Daemon de hammer: bus de agente + diario.")]
#[command(name = "hammerd", version, about = "Daemon de takana: bus de agente + diario.")]
struct Args {
/// Socket del bus de agente.
#[arg(long, default_value = "/run/agent.sock")]
@@ -32,7 +32,7 @@ struct Args {
#[arg(long, default_value = "/var/lib/hammer/journal")]
journal: String,
/// Directorio raíz de overlays (para que el watcher pueda filtrar eventos bajo overlays
/// activos). Default coincide con `hammer try`.
/// activos). Default coincide con `takana try`.
#[arg(long, default_value = takana_overlay::DEFAULT_STATE_ROOT)]
overlay_state_root: String,
/// Directorios extra a vigilar; si vacío, usa los defaults del FHS (SDD 05 §2).
+2 -2
View File
@@ -11,7 +11,7 @@
//! * Cada evento se materializa como `MutationEvent` con `source = External` y la `op`
//! se deriva (`classify_op`): si el fd resuelve a un path borrado (`" (deleted)"`) es
//! `Delete`; si el diario ya conocía ese path (con un evento previo no-`Delete`) es
//! `Edit`; en otro caso es `Create` (primera mutación que hammer observa sobre el path).
//! `Edit`; en otro caso es `Create` (primera mutación que takana observa sobre el path).
//! Nota: `FAN_CLOSE_WRITE` no distingue un `rename`-sobre-destino de una reescritura in
//! situ, ni reporta el nombre en eventos de directorio — la atribución plena de
//! `Create`/`Move`/`Delete` vía `FAN_REPORT_DFID_NAME` (con `open_by_handle_at` y
@@ -203,7 +203,7 @@ impl Watcher {
/// * `deleted` (el fd resuelve a `" (deleted)"`) ⇒ `Delete`.
/// * el diario ya tiene un evento previo no-`Delete` para el path ⇒ `Edit` (el archivo
/// ya era conocido y se reescribió).
/// * en otro caso ⇒ `Create` — la primera mutación que hammer observa sobre el path, o
/// * en otro caso ⇒ `Create` — la primera mutación que takana observa sobre el path, o
/// una resurrección tras un `Delete` previo.
///
/// No es una verdad absoluta del FS (un archivo preexistente al daemon, editado por
+1 -1
View File
@@ -8,7 +8,7 @@
//! capacidad devuelve `Error{code:"no_cap"}`, lo que prueba el dispatcher + el gate.
// El binario no expone una API pública; importamos sus módulos privados desde
// `path = ...`. `proto` se movió a hammer-core en Fase 6.
// `path = ...`. `proto` se movió a takana-core en Fase 6.
#[path = "../src/events.rs"]
mod events;
#[path = "../src/control.rs"]
+2 -2
View File
@@ -1,9 +1,9 @@
//! netup — configuración de red mínima Rust-nativa para el userland de hammer (Etapa C).
//! netup — configuración de red mínima Rust-nativa para el userland de takana (Etapa C).
//!
//! Reemplaza el `ip` de busybox + la IP estática hardcodeada del driver de rebuild por un binario
//! propio: levanta la interfaz, negocia un lease DHCPv4 y aplica IP/ruta/DNS vía netlink — todo
//! síncrono, sin tokio, dependiendo sólo de libc (ABI estable del kernel). Como hammerd/arje, es
//! código hammer-propio: no hay un cliente DHCP Rust "maduro" para adoptar al estilo ripgrep.
//! código takana-propio: no hay un cliente DHCP Rust "maduro" para adoptar al estilo ripgrep.
//!
//! Uso: `netup [interfaz]` (sin argumento, autodetecta la primera interfaz no-loopback).
+9 -9
View File
@@ -8,12 +8,12 @@
//! base → swm.verify_base(local) → debe ser Ok
//! build → para cada source_patch: AgentClient.compile() vía el bus (o saltar si --no-bus)
//! try → takana_overlay::try_overlay() (o usar --prefix)
//! apply → hammer-core::apply para config_edit/file_drop + hydrate para source_patch
//! apply → takana-core::apply para config_edit/file_drop + hydrate para source_patch
//! verify → spot-checks post-condición sobre disco
//! propose → devuelve Proposal con overlay_id + log de checks
//! ```
//!
//! La IA NUNCA ejecuta `hammer commit`. El humano lo hace tras revisar el `Proposal`.
//! La IA NUNCA ejecuta `takana commit`. El humano lo hace tras revisar el `Proposal`.
use std::path::{Path, PathBuf};
use std::time::Duration;
@@ -101,7 +101,7 @@ impl<T: IntentTranslator> Orchestrator<T> {
};
// 5. APPLY — recorremos las mutaciones en orden. En source_patch, dispatch al bus
// o skip; en config_edit/file_drop, primitivas de hammer-core::apply.
// o skip; en config_edit/file_drop, primitivas de takana-core::apply.
let mut applied = AppliedCounts::default();
let mut checks: Vec<VerifyCheck> = Vec::new();
let mut skipped_source_patches = 0usize;
@@ -356,10 +356,10 @@ fn wait_for_crash(policy: &RepairPolicy) -> Option<CrashInfo> {
None
}
/// Stub: `hammer-build` no es dependencia directa de `hammer-agent` para no arrastrar el
/// Stub: `takana-build` no es dependencia directa de `takana-agent` para no arrastrar el
/// lab a clientes que sólo quieran hablar con el bus. Implementamos la hidratación local
/// con `std::fs::hard_link` directamente porque es trivial. Si el árbol del artefacto crece
/// (symlinks, perms más finos), promovemos a `hammer-build::run_hydrate` añadiendo la dep
/// (symlinks, perms más finos), promovemos a `takana-build::run_hydrate` añadiendo la dep
/// de manera condicional.
fn hammer_build_run_hydrate(artifact_dir: &Path, target_fhs: &Path) -> Result<usize> {
let mut files = 0usize;
@@ -383,7 +383,7 @@ fn hammer_build_run_hydrate(artifact_dir: &Path, target_fhs: &Path) -> Result<us
}
let _ = std::fs::remove_file(&dst);
std::fs::hard_link(&src, &dst).or_else(|_| {
// Cross-fs ⇒ caemos a copia. No optimizamos: hammer-agent espera
// Cross-fs ⇒ caemos a copia. No optimizamos: takana-agent espera
// mismo FS; sólo damos el fallback como cinturón de seguridad.
std::fs::copy(&src, &dst).map(|_| ())
})?;
@@ -470,8 +470,8 @@ impl VerifyCheck {
}
}
/// El resultado del bucle. El humano lo lee para decidir `hammer commit <overlay_id>` (si
/// está en modo Overlay) o `hammer discard`.
/// El resultado del bucle. El humano lo lee para decidir `takana commit <overlay_id>` (si
/// está en modo Overlay) o `takana discard`.
#[derive(Debug, Clone, serde::Serialize)]
pub struct Proposal {
pub intent: String,
@@ -496,7 +496,7 @@ pub struct RepairAttempt {
pub attempt: u32,
pub intent: String,
/// `Some` cuando el ciclo abrió un overlay; preservado para que el humano pueda hacer
/// `hammer discard <id>` sobre intentos intermedios si lo desea.
/// `takana discard <id>` sobre intentos intermedios si lo desea.
pub overlay_id: Option<String>,
/// Si tras este intento llegó un `Crashed`, el servicio y código. `None` ⇒ estabilizado.
pub triggered_crash: Option<CrashInfo>,
+1 -1
View File
@@ -5,7 +5,7 @@ edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
description = "Bootstrap from-scratch de hammer: Stage 0/1/2 hacia el auto-alojamiento (SDD 11)."
description = "Bootstrap from-scratch de takana: Stage 0/1/2 hacia el auto-alojamiento (SDD 11)."
[dependencies]
takana-core.workspace = true
+36 -36
View File
@@ -1,4 +1,4 @@
//! Bootstrap from-scratch de hammer — el track posterior del [SDD 11](../../docs/11-bootstrap.md).
//! Bootstrap from-scratch de takana — el track posterior del [SDD 11](../../docs/11-bootstrap.md).
//!
//! Esta primera entrega cubre el **Stage 0**: ingerir un toolchain semilla ya construido
//! (no se compila; ver [ADR 0008](../../docs/adr/0008-bootstrap-stages.md)) como una *fuente
@@ -175,7 +175,7 @@ const STAGE1_COMPONENTS: &[&str] = &["musl", "busybox", "hammerd", "arje-zero"];
/// un `Card` `Virtual` cuyos `genesis` son `hammerd` (servicio `Native` con `Restart` — el
/// `CRASHED` real) y una `console-getty` supervisada. La forma replica una seed real de arje
/// (`seeds/arje-qemu.card.json`) para que `Card::validate()` la acepte; los ULID son fijos para que
/// el `RootfsHash` sea reproducible. Template autocontenido (opción A del SDD): hammer no depende de
/// el `RootfsHash` sea reproducible. Template autocontenido (opción A del SDD): takana no depende de
/// `card-core` como librería; el pin a arje-zero + el boot en VM cubren el drift de schema.
const STAGE1_SEED_CARD: &str = r#"{
"schema_version": 1,
@@ -447,13 +447,13 @@ const SSHD_CONFIG: &str = "Port 22\nListenAddress 0.0.0.0\nPermitRootLogin prohi
// ── Atestación de integridad al arranque (I4 / Etapa D, mitad de HAMMER) ─────────────────────────
//
// Por la regla de oro del [plan arje↔hammer] (`PLAN-ATESTACION-Y-HAMMER.md` §B.1): hammer es dueño del
// Por la regla de oro del [plan arje↔takana] (`PLAN-ATESTACION-Y-HAMMER.md` §B.1): takana es dueño del
// **`expected_hash` + firma + TrustStore**; arje es dueño del **gate al boot** (computa `blake3` de cada
// binario crítico ANTES de incarnar y, si no casa, `halt`). Esta es la mitad de hammer: PRODUCIR el
// binario crítico ANTES de incarnar y, si no casa, `halt`). Esta es la mitad de takana: PRODUCIR el
// manifiesto de hashes esperados (`/ente/attest.json`) que el gate A2 de arje verifica — "el
// `expected_hash` de un `.swm` ES el BLAKE3 que arje atesta". El hash es `blake3` CRUDO del fichero
// (`ArtifactHash::of_file`), el mismo que computa `arje-cas::blake3_of`. La firma con la rootkey del
// seed (A1, `arje-packager`) es una capa posterior; el manifiesto ya es verificable hoy por `hammer
// seed (A1, `arje-packager`) es una capa posterior; el manifiesto ya es verificable hoy por `takana
// attest` (auto-validación: "reproducir, no confiar"). Es un fichero APARTE de la seed card ⇒ no toca
// el schema de `card-core` y el producto bootea igual con el arje-zero actual (que aún ignora A2).
@@ -475,7 +475,7 @@ pub struct AttestEntry {
pub b3: String,
}
/// Manifiesto de atestación que hammer emite en `/ente/attest.json`. `algo` fijo a `blake3` (crudo).
/// Manifiesto de atestación que takana emite en `/ente/attest.json`. `algo` fijo a `blake3` (crudo).
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct AttestManifest {
pub schema_version: u32,
@@ -514,7 +514,7 @@ fn build_attestation(root: &Path) -> Result<AttestManifest> {
/// Verifica un árbol FHS (`rootfs_dir`, p.ej. un product-rootfs hidratado) contra su propio
/// `/ente/attest.json`: recomputa el BLAKE3 de cada binario listado y lo compara. Es lo que el gate de
/// arje hará al boot, hecho hoy por hammer (auto-validación de integridad del producto).
/// arje hará al boot, hecho hoy por takana (auto-validación de integridad del producto).
pub fn verify_attestation(rootfs_dir: &Path) -> Result<AttestReport> {
let manifest_path = rootfs_dir.join("ente/attest.json");
let raw = std::fs::read_to_string(&manifest_path).map_err(|e| {
@@ -541,7 +541,7 @@ pub fn verify_attestation(rootfs_dir: &Path) -> Result<AttestReport> {
}
/// Seed de producto = la seed base con los cards de servicio APENDADOS al `genesis`. Composición vía
/// `serde_json` (hammer sigue autocontenido: no depende de `card-core`). Determinista ⇒ el
/// `serde_json` (takana sigue autocontenido: no depende de `card-core`). Determinista ⇒ el
/// `product-rootfs` es reproducible. El núcleo (hammerd + getty) se preserva tal cual.
fn product_seed_card() -> Result<String> {
let mut seed: serde_json::Value = serde_json::from_str(STAGE1_SEED_CARD)
@@ -773,7 +773,7 @@ fn seal_product_rootfs(
/// Reproduce un conjunto de componentes DESDE UN REPO FIRMADO, devolviendo sus `(nombre,hash)` sellados
/// — el MISMO shape que [`build_components`], así [`assemble_product_rootfs`] los hidrata igual. Por cada
/// componente: resuelve su cierre de deps del índice, puebla el catálogo con los `{dep}.toml` (para que
/// el lab materialice build-deps al reproducir, igual que `hammer install`), y REPRODUCE su `source_patch`
/// el lab materialice build-deps al reproducir, igual que `takana install`), y REPRODUCE su `source_patch`
/// con `build_source_patch` (que sella y CHEQUEA el `expected_hash` anclado en el índice). Verifica la
/// firma del release ANTES de tocar nada: el producto NO se ensambla desde un repo no confiado.
pub fn install_components_from_repo(
@@ -844,7 +844,7 @@ fn load_swm_file(path: &Path) -> Result<takana_core::Swm> {
.map_err(|e| Error::Other(format!("no pude parsear {}: {e}", path.display())))
}
/// Primera mutación `SourcePatch` de un `.swm` (un paquete hammer es un único source_patch).
/// Primera mutación `SourcePatch` de un `.swm` (un paquete takana es un único source_patch).
fn source_patch_of(swm: &takana_core::Swm) -> Option<&takana_core::Mutation> {
swm.mutations
.iter()
@@ -854,18 +854,18 @@ fn source_patch_of(swm: &takana_core::Swm) -> Option<&takana_core::Mutation> {
// ── Producto ATESTADO: la capa FIRMADA que el gate de arje verifica al boot (I4 / Etapa D) ───────
//
// `product()` sella el `product-rootfs` con el arje-zero del núcleo + un `/ente/attest.json` de
// AUTO-verificación (esquema paralelo que `hammer attest` valida, pero que el gate de arje NO lee).
// AUTO-verificación (esquema paralelo que `takana attest` valida, pero que el gate de arje NO lee).
// El producto ATESTADO cierra el lazo canónico: hidrata el arje-zero CON gate (`arje-zero-attest`),
// FIRMA la seed con `arje-packager` (una concesión Ed25519 por binario crítico, sobre su BLAKE3, bajo
// una rootkey) y la sella como `product-attested-rootfs`. Booteado, el gate (política `halt`) recomputa
// el BLAKE3 de cada exec crítico y aborta antes de incarnar si algo no casa la concesión firmada.
//
// Esto reemplaza el spike `scripts/attest-boot-test.sh` (que overlayeaba a mano): ahora hammer-bootstrap
// Esto reemplaza el spike `scripts/attest-boot-test.sh` (que overlayeaba a mano): ahora takana-bootstrap
// produce el producto atestado de forma estructurada. Reusa la cripto canónica (arje-attest/agora vía el
// packager) — cero criptografía nueva en hammer. La rootkey por defecto es determinista ⇒ las firmas
// packager) — cero criptografía nueva en takana. La rootkey por defecto es determinista ⇒ las firmas
// Ed25519 (RFC 8032, nonce derivado del mensaje) son reproducibles bit a bit.
/// Rootkey de desarrollo (32 bytes) con la que hammer firma la atestación por defecto. Determinista ⇒
/// Rootkey de desarrollo (32 bytes) con la que takana firma la atestación por defecto. Determinista ⇒
/// firmas reproducibles ⇒ `product-attested-rootfs` reproducible. El endurecimiento soberano (rootkey
/// fuera del árbol, `/etc/arje/rootkey.pub` / `ARJE_ATTEST_ROOTKEY_FILE`) es una capa posterior
/// (roadmap Etapa D, hardening #3); por ahora se inyecta como override de [`AttestConfig::rootkey`].
@@ -945,7 +945,7 @@ fn critical_bins_from_seed(seed: &serde_json::Value, staging: &Path) -> Vec<(Str
/// Ensambla el producto ATESTADO en `staging`: hidrata el `product-rootfs` base, PISA `/usr/bin/arje-zero`
/// con el init CON gate, fija `attest_policy` en la seed, la FIRMA con el packager (una concesión por
/// binario crítico sobre su BLAKE3), ancla la pubkey en `/etc/arje/rootkey.pub` (ancla soberana externa,
/// hardening #3) y regenera `/ente/attest.json` (la auto-verificación de hammer, ahora coherente con el
/// hardening #3) y regenera `/ente/attest.json` (la auto-verificación de takana, ahora coherente con el
/// arje-zero nuevo). El packager corre en el host (estático musl, sin loader, como busybox/bwrap). La
/// firma se valida E2E en QEMU (`scripts/attest-boot-test.sh`).
fn assemble_attested_product_rootfs(
@@ -1030,7 +1030,7 @@ fn assemble_attested_product_rootfs(
// `attest_rootkey` declarada en el seed ⇒ un seed reescrito con OTRA rootkey cae en AutorNoConfiable
// (las concesiones no las firmó el ancla). Cierra el hueco que el WARN "sin ancla soberana externa"
// señala. La pubkey se LEE del seed ya firmado (`attest_rootkey`, lo que el packager computó de
// nuestra rootkey privada) ⇒ cero criptografía nueva en hammer.
// nuestra rootkey privada) ⇒ cero criptografía nueva en takana.
let signed_raw = std::fs::read_to_string(&seed_path)
.map_err(|e| Error::Other(format!("releer seed firmado: {e}")))?;
let signed: serde_json::Value = serde_json::from_str(&signed_raw)
@@ -1050,7 +1050,7 @@ fn assemble_attested_product_rootfs(
std::fs::write(staging.join("etc/arje/rootkey.pub"), &pubkey)?;
// 6) regenerar /ente/attest.json: el arje-zero cambió ⇒ su BLAKE3 también, así que el manifiesto de
// auto-verificación de hammer (`hammer attest`) debe casar el árbol nuevo. El producto base lo
// auto-verificación de takana (`takana attest`) debe casar el árbol nuevo. El producto base lo
// trae hidratado (hardlink read-only): romperlo antes de reescribir.
let attest = build_attestation(staging)?;
let attest_json = serde_json::to_string_pretty(&attest)
@@ -1141,7 +1141,7 @@ pub fn product_attested(
// **sólo** las herramientas de Stage 1, y se compara el **content-hash** (`of_tree`, no el hash
// input-addressed del store) de stage1 vs stage1'. Iguales ⇒ el sistema se compila a sí mismo bit
// a bit (auto-alojado y reproducible). El rebuild **dentro del rootfs** (bwrap/chroot/VM) corre en
// la VM destino; lo que vive en hammer es la **referencia** (content-hash de stage1) y la
// la VM destino; lo que vive en takana es la **referencia** (content-hash de stage1) y la
// comparación. Ver `docs/runbooks/stage1-vm-boot.md`.
/// Veredicto de la verificación de auto-alojamiento de Stage 2.
@@ -1264,12 +1264,12 @@ pub fn all(
//
// El Stage 1 que booteamos es un *runtime* (musl+busybox+hammerd+arje-zero): no trae compilador, no
// puede reconstruirse. El **builder rootfs** es Stage 1 **+ el toolchain adentro** — la imagen que,
// booteada en la VM, corre `hammer bootstrap stage1` dentro de sí misma y produce `stage1'`. Compara
// booteada en la VM, corre `takana bootstrap stage1` dentro de sí misma y produce `stage1'`. Compara
// su content-hash con la referencia anclada por `stage2`: iguales ⇒ auto-alojamiento bit a bit.
//
// Variante (a) **pragmática** (SDD 11 §7.2): el toolchain entra desde Alpine (no construido por
// hammer). Demuestra el *mecanismo* y cierra la reproducibilidad end-to-end; no es aún el
// auto-alojamiento *puro* (variante b: el toolchain construido por hammer desde fuente, incremental).
// takana). Demuestra el *mecanismo* y cierra la reproducibilidad end-to-end; no es aún el
// auto-alojamiento *puro* (variante b: el toolchain construido por takana desde fuente, incremental).
//
// El builder NO se sella en el store (su /toolchain Alpine no es content-addressed y abultaría): se
// ensambla en un `out_dir` que se empaqueta como initramfs (ver runbook §8c). Su identidad sí es
@@ -1284,7 +1284,7 @@ pub fn all(
/// El toolchain es Alpine **dinámico**: sus binarios (bwrap/git/curl/cargo/make) no corren desde el
/// userland Stage 1, que es **estático** (no hay loader en `/lib`). El driver instala un *shim* del
/// loader musl (`cp` a `/lib`) + `LD_LIBRARY_PATH`/`PATH` al toolchain, y a partir de ahí esas
/// herramientas son ejecutables; el `hammer` estático vive en `/usr/bin/hammer`. El sandbox de build
/// herramientas son ejecutables; el `takana` estático vive en `/usr/bin/hammer`. El sandbox de build
/// (bwrap) anida en `/toolchain`. (El shim sólo se usa para *invocar* el toolchain desde fuera del
/// sandbox: fetch git/tarball; dentro del sandbox el loader está en su sitio.)
const REBUILD_DRIVER: &str = r#"#!/bin/sh
@@ -1349,13 +1349,13 @@ pub struct BuilderSpec {
/// Stage 1 rootfs ya sellado (nombre store `stage1-rootfs`) que el builder extiende — la base
/// runtime sobre la que se hidrata el toolchain.
pub stage1_rootfs: RootfsHash,
/// Semilla zig sellada (Stage 0). Se replica en el `/store` del builder para que el `hammer`
/// Semilla zig sellada (Stage 0). Se replica en el `/store` del builder para que el `takana`
/// de adentro resuelva el toolchain por hash, igual que afuera.
pub seed_hash: ArtifactHash,
pub seed_kind: SeedKind,
/// Recetas a embeber en `/etc/hammer/recipes` (las que `stage1` rehidrata el rebuild).
pub recipes_dir: PathBuf,
/// Binario `hammer` estático → `/usr/bin/hammer`. Variante (a): el del host; (b): el que hammer
/// Binario `takana` estático → `/usr/bin/hammer`. Variante (a): el del host; (b): el que takana
/// construye desde fuente.
pub hammer_bin: PathBuf,
/// Toolchain del sandbox de build → `/toolchain` (variante a: el rootfs Alpine de `.dev-fs`).
@@ -1369,20 +1369,20 @@ pub struct BuilderSpec {
/// Caché de fuentes (`work/repos`, `work/tarballs`) → `/work`, para un rebuild **offline** y
/// determinista en la VM. `None` ⇒ el rebuild fetchea por red (la seed card trae `networking: full`).
pub work_cache: Option<PathBuf>,
/// Piezas del toolchain **construidas por hammer desde fuente** (variante b, SDD 11 §7.2b) que se
/// Piezas del toolchain **construidas por takana desde fuente** (variante b, SDD 11 §7.2b) que se
/// montan sobre el path Alpine en `/toolchain`. Vacío ⇒ variante (a) pura-Alpine. Cada swap entra
/// al hash lógico del builder: la procedencia deja de ser "todo Alpine" y se vuelve auditable.
pub swaps: Vec<ToolchainSwap>,
}
/// Una herramienta del toolchain reemplazada por su build hammer desde fuente (SDD 11 §7.2b). El
/// Una herramienta del toolchain reemplazada por su build takana desde fuente (SDD 11 §7.2b). El
/// binario sellado se monta sobre `rel_path` dentro de `/toolchain`, pisando la versión Alpine; su
/// hash sellado se ancla en el hash lógico del builder para que el swap sea reproducible y auditable.
#[derive(Debug, Clone)]
pub struct ToolchainSwap {
/// Nombre del componente/receta (p. ej. `make`); también el nombre store del artefacto sellado.
pub name: String,
/// `of_tree` del build hammer (el artefacto sellado del que sale el binario).
/// `of_tree` del build takana (el artefacto sellado del que sale el binario).
pub artifact: ArtifactHash,
/// Path relativo del binario dentro del artefacto **y** dentro de `/toolchain` (p. ej.
/// `usr/bin/make`). El mismo en origen y destino: el layout del artefacto imita al de Alpine.
@@ -1500,7 +1500,7 @@ fn link_or_copy_tree(src: &Path, dst: &Path, skip: Option<&Path>) -> Result<()>
/// Ensambla el árbol del builder en `staging` (la pieza testeable, sin VM ni Alpine real): hidrata el
/// Stage 1 rootfs como base, monta el toolchain en `/toolchain`, replica la semilla en `/store`,
/// instala `hammer` + recetas + el driver `rebuild-stage1` y, opcionalmente, la caché de fuentes.
/// instala `takana` + recetas + el driver `rebuild-stage1` y, opcionalmente, la caché de fuentes.
fn assemble_builder(spec: &BuilderSpec, store: &Store, staging: &Path) -> Result<()> {
// 1) Base: el Stage 1 rootfs sellado (read-only en el store ⇒ hardlinks seguros).
let stage1_dir = store.path_of(&spec.stage1_rootfs, "stage1-rootfs");
@@ -1527,7 +1527,7 @@ fn assemble_builder(spec: &BuilderSpec, store: &Store, staging: &Path) -> Result
}
link_or_copy_tree(&spec.toolchain_src, &staging.join("toolchain"), None)?;
// 3b) Swaps de variante (b): piezas construidas por hammer desde fuente pisan su versión Alpine
// 3b) Swaps de variante (b): piezas construidas por takana desde fuente pisan su versión Alpine
// en /toolchain. Un `rel_path` puede apuntar a un BINARIO (estático musl ⇒ corre sin el shim
// del loader; p.ej. make, busybox) o a un DIRECTORIO (un árbol de headers; p.ej. los
// `usr/include/{linux,asm,…}` de linux-headers). En el caso directorio se reemplaza el árbol
@@ -1560,7 +1560,7 @@ fn assemble_builder(spec: &BuilderSpec, store: &Store, staging: &Path) -> Result
}
}
// 4) Semilla → /store/<hash>-seed-<kind>, tal cual está sellada afuera, para que el `hammer` de
// 4) Semilla → /store/<hash>-seed-<kind>, tal cual está sellada afuera, para que el `takana` de
// adentro resuelva el toolchain por hash sin re-ingerirla.
let seed_name = format!("seed-{}", spec.seed_kind.as_str());
let seed_dir = store.path_of(&spec.seed_hash, &seed_name);
@@ -1577,7 +1577,7 @@ fn assemble_builder(spec: &BuilderSpec, store: &Store, staging: &Path) -> Result
None,
)?;
// 5) Binario hammer → /usr/bin/hammer (+x).
// 5) Binario takana → /usr/bin/hammer (+x).
let hammer_dst = staging.join("usr/bin/hammer");
if std::fs::hard_link(&spec.hammer_bin, &hammer_dst).is_err() {
std::fs::copy(&spec.hammer_bin, &hammer_dst)?;
@@ -2226,7 +2226,7 @@ mod tests {
#[test]
fn assemble_attested_swaps_gate_signs_seed_and_regenerates_attest() {
// Hermético: un `arje-packager` SINTÉTICO (script python) simula la firma — añade `attest`/
// `attest_rootkey` al seed-out. Valida el CABLEADO de hammer (pisar el init, romper hardlinks,
// `attest_rootkey` al seed-out. Valida el CABLEADO de takana (pisar el init, romper hardlinks,
// fijar attest_policy, derivar los --bin de la seed, regenerar /ente/attest.json), sin la cripto
// real (eso lo cubre E2E `scripts/attest-boot-test.sh` en QEMU).
use std::os::unix::fs::PermissionsExt;
@@ -2440,7 +2440,7 @@ open(os.path.join(os.path.dirname(out),"..","..","bins-seen.txt"),"w").write("\n
std::fs::write(w.join("bin/zig"), b"#!/bin/sh\necho zig\n").unwrap();
});
// Recetas, binario hammer y toolchain (Alpine sintético), fuera del store.
// Recetas, binario takana y toolchain (Alpine sintético), fuera del store.
let recipes = dir.join("recipes");
std::fs::create_dir_all(&recipes).unwrap();
std::fs::write(recipes.join("musl.toml"), b"name = 'musl'\n").unwrap();
@@ -2484,7 +2484,7 @@ open(os.path.join(os.path.dirname(out),"..","..","bins-seen.txt"),"w").write("\n
std::fs::read_link(out.join("sbin/init")).unwrap(),
PathBuf::from("/usr/bin/arje-zero"),
);
// Toolchain en /toolchain, semilla en /store, hammer + recetas + driver.
// Toolchain en /toolchain, semilla en /store, takana + recetas + driver.
assert!(out.join("toolchain/usr/bin/make").is_file(), "toolchain montado");
assert!(
out.join("store").join(spec.seed_hash.store_dir_name("seed-zig")).join("bin/zig").is_file(),
@@ -2494,7 +2494,7 @@ open(os.path.join(os.path.dirname(out),"..","..","bins-seen.txt"),"w").write("\n
assert!(out.join("etc/hammer/recipes/musl.toml").is_file(), "recetas embebidas");
assert!(out.join("usr/bin/rebuild-stage1").is_file(), "driver de rebuild");
// El driver y hammer son ejecutables.
// El driver y takana son ejecutables.
use std::os::unix::fs::PermissionsExt;
let m = std::fs::metadata(out.join("usr/bin/rebuild-stage1")).unwrap();
assert_eq!(m.permissions().mode() & 0o111, 0o111, "driver +x");
@@ -2559,7 +2559,7 @@ open(os.path.join(os.path.dirname(out),"..","..","bins-seen.txt"),"w").write("\n
let out = tmp.path().join("builder-out");
let report = builder_rootfs(&spec, &store, &out).unwrap();
// El make de /toolchain es ahora el de hammer, no el de Alpine.
// El make de /toolchain es ahora el de takana, no el de Alpine.
let got = std::fs::read(out.join("toolchain/usr/bin/make")).unwrap();
assert_eq!(got, b"\x7fELF-hammer-make-static", "el swap pisó el make de Alpine");
+1 -1
View File
@@ -5,7 +5,7 @@ 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."
description = "El laboratorio de takana: sandbox de build (bubblewrap + zig cc) e hidratación."
[dependencies]
takana-core.workspace = true
+1 -1
View File
@@ -616,7 +616,7 @@ fn fetch_tarball(
///
/// # Por qué NO avisa cuando cae al mirror
///
/// Tentación evidente y **equivocada**: si `hammer build` avisara en cada descarga, el aviso sería
/// Tentación evidente y **equivocada**: si `takana build` avisara en cada descarga, el aviso sería
/// ruido en 561 recetas y nadie lo leería. Peor: haría creer que el bit-rot está vigilado *acá*,
/// cuando construir y vigilar upstream son dos trabajos distintos. La vigilancia vive en
/// `scripts/fuentes/fuentes-vigia.sh`, la corre el latido y escribe `docs/state/fuentes-vigia.json`.
+2 -2
View File
@@ -3,7 +3,7 @@
//! Estrategia primaria (`LinkMode::Static`): hardlink directo. Cero copia, cero RPATH que
//! reescribir. El FHS queda con archivos reales (no symlinks) compartiendo inode con el store.
//! Pisar un archivo del FHS rompe el hardlink (CoW al kernel) sin tocar el artefacto del store
//! — base de rollback (`hammer hydrate <hash>` lo restaura).
//! — base de rollback (`takana hydrate <hash>` lo restaura).
//!
//! Estrategia secundaria (`LinkMode::Dynamic`): los ELF que necesitan normalización de
//! interpreter/RPATH se **copian** (no se hardlinkean: `patchelf` reescribe el binario y un
@@ -65,7 +65,7 @@ pub fn hydrate(
// `is_dir()` a secas acepta un directorio VACÍO: hydrate proyectaría 0 ficheros y devolvería
// un HydrateReport de éxito. Es el mismo agujero que ya se tapó en `Store::has` (2026-08-10) y
// en `Store::seal` (2026-08-29): un vacío no es un artefacto, es un nombre. Acá duele especial
// porque el escritorio se hidrata así (`hammer hydrate <hash> --into`), y un FHS proyectado
// porque el escritorio se hidrata así (`takana hydrate <hash> --into`), y un FHS proyectado
// desde nada sale "OK" y falla mucho más tarde, sin rastro de cuál artefacto faltaba.
let vacio = std::fs::read_dir(artifact_dir)
.map(|mut e| e.next().is_none())
+9 -9
View File
@@ -239,9 +239,9 @@ pub fn build(
if matches!(detect_build_system(&src_tree), BuildSys::Cargo)
|| eff_recipe.source.cargo_vendor == Some(true)
{
// El source se copia DENTRO del repo hammer (que es un workspace Cargo) ⇒ si el Cargo.toml
// El source se copia DENTRO del repo takana (que es un workspace Cargo) ⇒ si el Cargo.toml
// de la fuente NO declara su propio `[workspace]`, cargo cree que pertenece al workspace de
// hammer y `cargo vendor` aborta. Inyectamos un `[workspace]` vacío para aislarla. SÓLO si
// takana y `cargo vendor` aborta. Inyectamos un `[workspace]` vacío para aislarla. SÓLO si
// no lo tiene ya (las recetas del corpus que lo parchean a mano siguen funcionando sin
// duplicar la tabla). Esto desbloquea genéricamente los imports Rust (Etapa G) sin un patch
// por receta. Ver memoria 'cargo-recipe crate suelto'.
@@ -312,7 +312,7 @@ pub fn build(
env.push(("CBUILD".into(), SANDBOX_NATIVE_TARGET.to_string()));
env.push(("CHOST".into(), SANDBOX_NATIVE_TARGET.to_string()));
// *-sys C-FFI (Etapa G): si la receta declara `openssl` como dep, apuntá `openssl-sys` a la
// openssl hammer-built materializada en /usr (estática, musl). Sin esto openssl-sys busca en el
// openssl takana-built materializada en /usr (estática, musl). Sin esto openssl-sys busca en el
// host y aborta ("Could not find directory of OpenSSL installation"). Condicional al dep ⇒ no
// rompe recetas que usen openssl vendored. Destraba la frontera cargo-* (reqwest/git tools).
if recipe.deps.build.iter().any(|d| d == "openssl") {
@@ -401,7 +401,7 @@ pub fn build(
}
// Sidecar de provenance: escribimos la receta serializada DENTRO de out_dir antes del
// seal, para que quede congelada en el árbol read-only del store. `hammer export` la
// seal, para que quede congelada en el árbol read-only del store. `takana export` la
// lee de vuelta para emitir `source_patch` con la receta original en vez de un
// `file_drop` opaco. Ver `docs/04-overlay.md` §provenance (Fase 4).
let sidecar_path = out_dir.join(takana_core::store::RECIPE_SIDECAR_REL);
@@ -494,7 +494,7 @@ pub fn run_evidence(
env: Vec::new(),
};
// 3. Ejecutar cada check y evaluarlo (la comparación exit/hash es pura, vive en hammer-core).
// 3. Ejecutar cada check y evaluarlo (la comparación exit/hash es pura, vive en takana-core).
let mut results = Vec::with_capacity(recipe.evidence.checks.len());
for c in &recipe.evidence.checks {
tracing::info!(kind = c.kind.as_str(), cmd = %c.cmd, "evidence: check");
@@ -612,7 +612,7 @@ fn inject_cargo_package_selector(recipe: &mut Recipe, src: &Path) {
}
/// Garantiza que el `Cargo.toml` raíz de la fuente declare un `[workspace]` propio, para que cargo
/// NO la considere parte del workspace del repo hammer (que la contiene en `work/sources/`). Append
/// NO la considere parte del workspace del repo takana (que la contiene en `work/sources/`). Append
/// idempotente: si ya hay `[workspace]` (crate suelto que es su propio workspace, o una receta que
/// lo parchea), no toca nada. Caso virtual-manifest (root ya es `[workspace]`) tampoco se toca.
fn ensure_cargo_workspace_isolation(src: &Path) -> takana_core::Result<()> {
@@ -856,7 +856,7 @@ fn resolve_phases(recipe: &Recipe, src: &Path) -> takana_core::Result<Phases> {
// Hermético y reproducible: `--offline` (deps vendoreadas en la fuente) +
// `--locked` (Cargo.lock fijo, coherente con ADR 0006). El link va por un
// wrapper `zig cc` — cargo no acepta un linker de dos palabras, y zig provee
// el `libgcc_s` que el link dinámico musl pide (ver M2 del plan arje↔hammer).
// el `libgcc_s` que el link dinámico musl pide (ver M2 del plan arje↔takana).
if recipe.build.target == SANDBOX_NATIVE_TARGET {
// NATIVO: el sandbox del lab YA es x86_64 musl, así que no cruzamos. El rust del
// sandbox sólo trae su std nativo (`x86_64-alpine-linux-musl`), no
@@ -894,7 +894,7 @@ fn resolve_phases(recipe: &Recipe, src: &Path) -> takana_core::Result<Phases> {
// sea un ERROR ("option `-C link-self-contained` is not supported on this
// target") y NO usa self-contained — el baseline 9adefb82 linkea con zig sin
// chocar. Así que: añadir el flag SÓLO si el host del rustc NO es `-alpine-`
// (i.e. un rust vanilla como el hammer-rust del frente self-host); para Alpine,
// (i.e. un rust vanilla como el takana-rust del frente self-host); para Alpine,
// omitirlo. RUSTFLAGS alcanza a todas las unidades; los `bin_flags` (cargo rustc
// --) sólo a la top.
// CC para los build-scripts (cc-rs compila el C de deps como libgit2-sys,
@@ -924,7 +924,7 @@ fn resolve_phases(recipe: &Recipe, src: &Path) -> takana_core::Result<Phases> {
// CROSS real (otro target que zig sí entiende): `--target` + linker por env
// per-triple. Hermético y reproducible: `--offline` (deps vendoreadas) +
// `--locked` (Cargo.lock fijo, ADR 0006). zig provee el `libgcc_s` que el link
// dinámico musl pide (ver M2 del plan arje↔hammer).
// dinámico musl pide (ver M2 del plan arje↔takana).
let setup = "printf '#!/bin/sh\\nexec zig cc -mcpu=baseline \"$@\"\\n' > \"$PWD/.hammer-zig-cc\" && \
chmod +x \"$PWD/.hammer-zig-cc\" && ";
let triple = rustc_triple(&recipe.build.target);
+5 -5
View File
@@ -39,7 +39,7 @@ impl std::fmt::Display for BuildFailure {
impl std::error::Error for BuildFailure {}
impl BuildFailure {
/// Recupera un `BuildFailure` de un error de hammer-core, si lo lleva dentro. El bus lo
/// Recupera un `BuildFailure` de un error de takana-core, si lo lleva dentro. El bus lo
/// usa para separar `reason` de `log_tail`.
pub fn from_error(e: &takana_core::Error) -> Option<&BuildFailure> {
match e {
@@ -457,7 +457,7 @@ impl Sandbox {
// default (16) el codegen paralelo de rustc reparte el crate en N unidades y la salida
// varía build-a-build incluso en la MISMA máquina y con entradas idénticas — Stage 2 lo
// cazó: `arje-zero` (cuyo workspace tawasuyu no fija perfil) divergía ~10 KB repartidos
// por .text/.rodata/.eh_frame, mientras `hammerd` (workspace hammer que SÍ pinea
// por .text/.rodata/.eh_frame, mientras `hammerd` (workspace takana que SÍ pinea
// codegen-units=1) reproducía bit-a-bit. El lab lo impone en vez de confiar en que cada
// repo upstream lo declare (SDD 09 §2). Inerte para builds no-Cargo (musl/busybox lo
// ignoran). Ver runbook §8c / SDD 11.
@@ -501,7 +501,7 @@ impl Sandbox {
/// El tee va SIEMPRE a stderr (tanto el stdout como el stderr del build): stdout del proceso
/// queda reservado para el resultado legible-por-máquina (el content-hash que imprime la CLI,
/// p. ej. `bootstrap stage1`). Antes el stdout del build se teeaba al stdout del padre y se
/// colaba en capturas tipo `PRIME=$(hammer … bootstrap stage1)`; con builds reales (no cacheados,
/// colaba en capturas tipo `PRIME=$(takana … bootstrap stage1)`; con builds reales (no cacheados,
/// p. ej. el rebuild in-VM) eso son miles de líneas de `configure`/`make` ⇒ el argumento crece y
/// la fase de verificación siguiente (`--rootfs "$PRIME"`) revienta el exec con E2BIG
/// (Argument list too long). Va a stderr, junto a los logs de tracing. Ver runbook §8c.
@@ -555,7 +555,7 @@ pub const CXX_WRAPPER: &str = "hammer-zig-cxx";
/// ── POR QUÉ (medido 2026-08-28) ─────────────────────────────────────────────────────────────
/// Construir `zlib` en dos máquinas dio el MISMO `ArtifactHash` y **bytes distintos**:
/// `usr/lib/libz.a` divergía sólo en la cabecera `ar` del miembro `/` (la tabla de símbolos),
/// con la hora de pared del build. `hammer why-differs` lo nombró exacto.
/// con la hora de pared del build. `takana why-differs` lo nombró exacto.
///
/// La causa NO es `zig ar` (llvm-ar sella mtime 0) ni el binutils del rootfs Alpine (que Alpine
/// compila con `--enable-deterministic-archives`): es **nuestro propio `recipes/binutils.toml`**,
@@ -577,7 +577,7 @@ pub const CXX_WRAPPER: &str = "hammer-zig-cxx";
pub const AR_WRAPPER: &str = "ar";
pub const RANLIB_WRAPPER: &str = "ranlib";
/// Materializa en el rootfs dos wrappers de linker-driver (`hammer-zig-cc`/`-cxx`) que envuelven a
/// Materializa en el rootfs dos wrappers de linker-driver (`takana-zig-cc`/`-cxx`) que envuelven a
/// `zig cc`/`zig c++`. Su ÚNICO efecto es QUITAR `-static` cuando el link REQUIERE enlace dinámico.
///
/// El harness inyecta `LDFLAGS=-static` para las recetas `link=static` (binario monolítico que pide
+2 -2
View File
@@ -3,8 +3,8 @@
//! Convierte la mutación serializada en una `Recipe` efímera + un archivo de patch
//! temporal, y delega en `build` para sellar el artefacto en el store. Luego se hidrata.
//!
//! Este módulo vive en `hammer-build` (no en `hammer-core`) porque depende del store y del
//! sandbox. `hammer-core` debe permanecer puro (sin red ni mounts) para que el watcher y los
//! Este módulo vive en `takana-build` (no en `takana-core`) porque depende del store y del
//! sandbox. `takana-core` debe permanecer puro (sin red ni mounts) para que el watcher y los
//! tests no arrastren toda la cadena del lab.
use std::path::{Path, PathBuf};
@@ -3,7 +3,7 @@
//! EL FALLO (medido 2026-08-30, antes de `no_escapa`): hidratar es `target_fhs.join(rel)` y
//! escribir por ruta. Un artefacto que trae `usr/share/pkg → /algún/lado` deja ese symlink puesto
//! —los symlinks se replican literales, y así debe ser— y la hidratación SIGUIENTE escribía
//! `usr/share/pkg/archivo` **fuera del root**, devolviendo `Ok(1)`. Con `hammer hydrate --into`
//! `usr/share/pkg/archivo` **fuera del root**, devolviendo `Ok(1)`. Con `takana hydrate --into`
//! sobre una imagen, eso es escribir en el sistema anfitrión diciendo que todo fue bien.
//!
//! El corpus no lo disparaba (de 160 symlinks absolutos ninguno apunta a un directorio, y de
+3 -3
View File
@@ -1,12 +1,12 @@
//! Guardián de SDD 25 §7-H4: **ningún camino de spawn de hammer usa `pre_exec`**.
//! Guardián de SDD 25 §7-H4: **ningún camino de spawn de takana usa `pre_exec`**.
//!
//! POR QUÉ. `std::process::Command` de Rust usa `posix_spawn` cuando puede, y `posix_spawn` es
//! PLANO respecto al tamaño del padre: a 256 MB tocados, `fork+exec` costó 3 949 µs contra 231 µs
//! (SDD 25 T1, **17×**). En cuanto se le pone un `pre_exec`, Rust apaga ese camino y cae a
//! `fork+exec`, que copia el espacio de direcciones del padre. hammer lanza builds desde un
//! `fork+exec`, que copia el espacio de direcciones del padre. takana lanza builds desde un
//! proceso que puede tener cientos de MB mapeados.
//!
//! Hoy `hammer-build::sandbox` lanza `bwrap` sin `pre_exec` — y hasta ahora eso era SUERTE, no una
//! Hoy `takana-build::sandbox` lanza `bwrap` sin `pre_exec` — y hasta ahora eso era SUERTE, no una
//! regla: nadie lo comprobaba. Esto lo vuelve una regla.
//!
//! CUÁNDO SE PUEDE APAGAR. Si algún día hace falta de verdad (un `setsid`, un `unshare` que bwrap
+3 -3
View File
@@ -7,10 +7,10 @@ authors.workspace = true
repository.workspace = true
description = "El binario `takana`: orquesta build, hydrate, try/commit, apply/export y ctl."
# ADR 0016 — renombre hammer→takana, etapa 2. Los DOS binarios se emiten a
# ADR 0016 — renombre takana→takana, etapa 2. Los DOS binarios se emiten a
# propósito, en vez de un symlink: la siembra de la granja excluye `/target`, así
# que un symlink hecho en el hub no existiría en el worker, y `cargo clean` lo
# borra. Dos `[[bin]]` sobreviven a las dos cosas. `hammer` se retira en la etapa 6.
# borra. Dos `[[bin]]` sobreviven a las dos cosas. `takana` se retira en la etapa 6.
[[bin]]
name = "takana"
path = "src/main.rs"
@@ -21,7 +21,7 @@ path = "src/main.rs"
[features]
default = []
# Activa el traductor LLM real (Claude API) detrás del flag `hammer ai --llm`.
# Activa el traductor LLM real (Claude API) detrás del flag `takana ai --llm`.
# Sin esta feature, `--llm` falla con un mensaje claro pidiendo recompilar.
llm-claude = ["takana-agent/llm-claude"]
+6 -6
View File
@@ -1,4 +1,4 @@
//! Importador `Alpine APKBUILD → receta hammer` (Etapa G, fuente #2).
//! Importador `Alpine APKBUILD → receta takana` (Etapa G, fuente #2).
//!
//! **Por qué Alpine y no sólo nix:** Alpine ya porta miles de paquetes a **musl** (y a menudo
//! estático), CON los parches de portabilidad. Eso es justo lo que un import de nixpkgs PIERDE (los
@@ -8,12 +8,12 @@
//! Este importador PARSEA el APKBUILD (no lo ejecuta — es shell; sourcearlo correría código ajeno)
//! y extrae: pkgname/pkgver, la URL de la fuente, **los `.patch` (los carga a `source.patches`)**,
//! makedepends → deps, y `build()/package()` → fases (best-effort: son shell de abuild con CHOST/
//! --shared/etc. ⇒ necesitan adaptación al lab estático de hammer). Es un PUNTO DE PARTIDA, pero
//! --shared/etc. ⇒ necesitan adaptación al lab estático de takana). Es un PUNTO DE PARTIDA, pero
//! uno que YA incluye el trabajo de musl — mucho más cerca de compilar que un import crudo.
use std::collections::BTreeMap;
/// Convierte el texto de un APKBUILD en el TOML de una receta hammer.
/// Convierte el texto de un APKBUILD en el TOML de una receta takana.
pub fn recipe_from_apkbuild(text: &str) -> Result<String, String> {
let vars = parse_vars(text);
let get = |k: &str| vars.get(k).cloned().unwrap_or_default();
@@ -61,7 +61,7 @@ pub fn recipe_from_apkbuild(text: &str) -> Result<String, String> {
};
// build-deps: SÓLO `makedepends`. El `depends` de abuild es RUNTIME (p.ej. gzip depends="less"
// para `zless`) — no hace falta para compilar y rompería `hammer build` (busca recipes/less.toml).
// para `zless`) — no hace falta para compilar y rompería `takana build` (busca recipes/less.toml).
// Lo emitimos como comentario de provenance, no como `[deps] build`. (checkdepends ni se lee: no
// corremos la suite de tests del paquete.)
let self_prefix = format!("{pkgname}-");
@@ -265,10 +265,10 @@ fn func_body(text: &str, name: &str) -> Option<String> {
}
}
/// Traduce las variables de abuild a su equivalente en el lab de hammer. Substituciones SEGURAS
/// Traduce las variables de abuild a su equivalente en el lab de takana. Substituciones SEGURAS
/// (inequívocas): `$pkgdir`→`/out` (el DESTDIR del lab), `$pkgname`→nombre, `$pkgver`→versión.
/// NO toca `$CBUILD/$CHOST/$srcdir/$builddir` ni `--shared`. `$CBUILD/$CHOST` los RESUELVE el lab en
/// runtime (env con el triple nativo saneado, Etapa G Fase 3 — `hammer-build`); `$srcdir/$builddir` y
/// runtime (env con el triple nativo saneado, Etapa G Fase 3 — `takana-build`); `$srcdir/$builddir` y
/// `--shared` quedan para el humano. `make DESTDIR="$pkgdir"` → `make DESTDIR="/out"`.
///
/// Además ENVUELVE el cuerpo en una función shell: abuild ejecuta `build()/package()` COMO funciones,
+6 -6
View File
@@ -1,4 +1,4 @@
//! `hammer kernel` — el armador de kernel (SDD 22).
//! `takana kernel` — el armador de kernel (SDD 22).
//!
//! Superficie en inglés (Regla 7.bis), mensajes en castellano. El contrato con las UIs es JSON
//! estable, mismo patrón que `/run/hammer/boot-graph.json`.
@@ -123,7 +123,7 @@ pub enum KernelCmd {
///
/// Sin esto la UI miente: un `-e FOO` cuya dependencia no se cumple se pierde en silencio.
DiffBack {
/// El JSON que emitió `hammer kernel plan --out`.
/// El JSON que emitió `takana kernel plan --out`.
#[arg(long)]
plan: PathBuf,
/// El `.config` resultante (el que la receta instala en `/out/boot/config-<versión>`).
@@ -137,7 +137,7 @@ pub enum KernelCmd {
/// No corre sin `--objective`: la regla aplicada global rechazaría recetas sanas (el kernel de
/// QEMU apaga USB, HID e INPUT a propósito). Sale distinto de cero si bloquea.
Gate {
/// El JSON que emitió `hammer kernel plan --out`. Con `--config` es opcional: sólo sirve
/// El JSON que emitió `takana kernel plan --out`. Con `--config` es opcional: sólo sirve
/// para atribuir cada pérdida a su bundle.
#[arg(long)]
plan: Option<PathBuf>,
@@ -152,7 +152,7 @@ pub enum KernelCmd {
/// Id de objetivo del catálogo (`qemu-serial`, `servidor`, `metal-escritorio`, `portatil`).
#[arg(long)]
objective: String,
/// Hardware de la máquina DESTINO, capturado con `hammer kernel hw --out`. Sin esto se
/// Hardware de la máquina DESTINO, capturado con `takana kernel hw --out`. Sin esto se
/// mira el de esta máquina, que no siempre es la misma.
#[arg(long)]
devices: Option<PathBuf>,
@@ -1339,7 +1339,7 @@ fn artifact_of(path: &Path) -> Option<String> {
}
/// Dónde viven las recetas de kernel cuando nadie pasa `--recipes`. La segunda es la de las
/// recetas DERIVADAS que emite `hammer kernel plan` (p. ej. `linux-gioser`).
/// recetas DERIVADAS que emite `takana kernel plan` (p. ej. `linux-gioser`).
const RECIPE_DEFAULTS: [&str; 2] = ["recipes", "docs/state/kernel-plans"];
/// Nombre del directorio del store (`<64 hex>-<nombre>`) que contiene esta ruta.
@@ -1395,7 +1395,7 @@ fn receta_de(artifact: &str, dirs: &[PathBuf]) -> Option<takana_core::Recipe> {
/// Le presta un catálogo a una receta que vive fuera de uno.
///
/// Las recetas DERIVADAS (`hammer kernel plan --recipe-out`) se guardan a propósito fuera de
/// Las recetas DERIVADAS (`takana kernel plan --recipe-out`) se guardan a propósito fuera de
/// `recipes/`: dejarlas ahí las mete en el grafo compartido como deuda. Pero sus `deps.build` son
/// las del catálogo padre, así que desde su directorio no resuelven y el hash no se puede calcular
/// — y sin hash no hay vigencia que comparar. Acá se les apunta el `base_dir` al primer directorio
+17 -17
View File
@@ -1,4 +1,4 @@
//! `hammer` — el punto de entrada de todo flujo manual. Ver `docs/`.
//! `takana` — 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
@@ -310,8 +310,8 @@ enum Cmd {
db: PathBuf,
},
/// [Etapa G] Importa una receta DESDE nixpkgs: toma el JSON normalizado que produce
/// `scripts/nix-import.sh` (vía `nix eval`) y emite una receta hammer (`.toml`). Trae la
/// RECETA (source+hash+deps+flags), nunca el binario del cache de nix — hammer reconstruye.
/// `scripts/nix-import.sh` (vía `nix eval`) y emite una receta takana (`.toml`). Trae la
/// RECETA (source+hash+deps+flags), nunca el binario del cache de nix — takana reconstruye.
/// Es un PUNTO DE PARTIDA: el build (zig/musl) suele necesitar adaptación por paquete.
ImportNix {
/// JSON normalizado del paquete nix. `-` o ausente ⇒ lee de stdin.
@@ -323,7 +323,7 @@ enum Cmd {
},
/// [Etapa G] Importa una receta DESDE un APKBUILD de Alpine (aports). Crucial: carga los
/// `.patch` de musl de Alpine — lo que un import de nix pierde — así está MUCHO más cerca de
/// compilar en el lab estático de hammer. `scripts/alpine-import.sh <pkg>` baja el APKBUILD.
/// compilar en el lab estático de takana. `scripts/alpine-import.sh <pkg>` baja el APKBUILD.
ImportAlpine {
/// El APKBUILD (texto). `-` o ausente ⇒ stdin.
#[arg(default_value = "-")]
@@ -372,7 +372,7 @@ enum Cmd {
/// Compile por el bus. Si no, source_patch se omite con warning.
#[arg(long)]
bus: Option<PathBuf>,
/// Raíz de overlays para `try`. Default coincide con `hammer try`.
/// Raíz de overlays para `try`. Default coincide con `takana try`.
#[arg(long)]
state_root: Option<PathBuf>,
/// Si está, activa el bucle de auto-reparación: tras aplicar, espera en el bus
@@ -398,10 +398,10 @@ enum Cmd {
/// [Fase 6] Evalúa una expresión del mini-lenguaje (SDD 08 §6) contra el sistema local.
/// Forma: `kind:value`. Kinds soportados: `bin`, `file`, `pin`, `service`, `depends`.
/// Ejemplos:
/// hammer query bin:grep
/// hammer query file:/etc/hosts
/// hammer query depends:/usr/bin/curl
/// hammer query pin:musl --base-ref base.json
/// takana query bin:grep
/// takana query file:/etc/hosts
/// takana query depends:/usr/bin/curl
/// takana query pin:musl --base-ref base.json
Query {
/// La expresión a evaluar.
expr: String,
@@ -509,8 +509,8 @@ enum BootCmd {
///
/// EN PRODUCCIÓN NO SE USA (PLAN-KIKIN §4.1/§9, 2026-07-16): mirada real NUNCA sale — sostiene
/// el DRM master del greeter al escritorio para no re-modesetear (parpadeo). El camino real es:
/// arje-zero publica el grafo al arrancar (`hammer boot graph`), mirada escribe boot-select y
/// dispara el reboot por el bus de arje, y arje-zero corre `hammer boot activate --from-select`
/// arje-zero publica el grafo al arrancar (`takana boot graph`), mirada escribe boot-select y
/// dispara el reboot por el bus de arje, y arje-zero corre `takana boot activate --from-select`
/// en su secuencia de apagado.
Menu {
/// Comando del compositor a lanzar (se parte por espacios). El primer token es el binario.
@@ -693,7 +693,7 @@ enum BootstrapCmd {
/// Directorio de recetas a embeber.
#[arg(long, default_value = "recipes")]
recipes: String,
/// Binario `hammer` estático a instalar en `/usr/bin/hammer`.
/// Binario `takana` estático a instalar en `/usr/bin/hammer`.
#[arg(long, default_value = "target/x86_64-unknown-linux-musl/release/hammer")]
hammer_bin: String,
/// Toolchain del sandbox de build → `/toolchain` (rootfs Alpine de `.dev-fs`).
@@ -709,7 +709,7 @@ enum BootstrapCmd {
/// Caché de fuentes (`work/`) a embeber en `/work` para un rebuild offline.
#[arg(long)]
work_cache: Option<String>,
/// Pieza del toolchain construida por hammer desde fuente (variante b, SDD 11 §7.2b) que pisa
/// Pieza del toolchain construida por takana desde fuente (variante b, SDD 11 §7.2b) que pisa
/// la de Alpine en `/toolchain`. Formato `name=hash[:rel_path]`; `rel_path` por defecto
/// `usr/bin/<name>`. Repetible. Ej.: `--swap make=fbad44ac…`. Cada swap entra al hash lógico.
#[arg(long = "swap", value_name = "name=hash[:rel_path]")]
@@ -1450,7 +1450,7 @@ fn main() -> anyhow::Result<()> {
}
/// Activa un nodo del grafo de arranque, imprime el resultado y republica el grafo (la vista de mirada
/// debe reflejar el nuevo nodo vivo). Compartido por `hammer boot activate` y `hammer boot menu`.
/// debe reflejar el nuevo nodo vivo). Compartido por `takana boot activate` y `takana boot menu`.
fn activate_and_report(
root: &std::path::Path,
state_root: &std::path::Path,
@@ -2208,7 +2208,7 @@ fn run_install(
Some(&pkg_name), // reproduce con el nombre del paquete ⇒ cache-hit del corpus, sin duplicar
)?;
// Registrar en la DB de instalados (salvo dry-run de schema). Permite `hammer uninstall`.
// Registrar en la DB de instalados (salvo dry-run de schema). Permite `takana uninstall`.
if !skip_source_patch {
let mut idb = takana_core::InstalledDb::load(db_path)?;
let files: Vec<String> = created.iter().map(|p| p.to_string_lossy().into_owned()).collect();
@@ -2242,7 +2242,7 @@ fn run_import_nix(file: &str, out: Option<&std::path::Path>) -> anyhow::Result<(
let pkg: nix_import::NixPkg = serde_json::from_str(&json)
.map_err(|e| anyhow::anyhow!("JSON normalizado de nix inválido: {e}"))?;
let toml = nix_import::to_recipe_toml(&pkg).map_err(|e| anyhow::anyhow!(e))?;
// Sanity: la receta emitida debe parsear como Recipe de hammer.
// Sanity: la receta emitida debe parsear como Recipe de takana.
takana_core::Recipe::from_toml(&toml)
.map_err(|e| anyhow::anyhow!("la receta importada no parsea (bug del importador): {e}"))?;
match out {
@@ -3089,7 +3089,7 @@ fn collect_patches_inline(recipe: &takana_core::Recipe) -> Option<String> {
Some(all)
}
/// Ejecuta el bucle agéntico de Fase 6 vía `hammer-agent`. Acepta una intención NL +
/// Ejecuta el bucle agéntico de Fase 6 vía `takana-agent`. Acepta una intención NL +
/// catálogo YAML que mapea intentos exactos a `.swm`s pre-armados. La salida es un
/// `Proposal` legible en stdout + el siguiente paso recomendado al humano.
fn run_ai(
+14 -14
View File
@@ -1,9 +1,9 @@
//! Importador `nix → receta hammer` (Etapa G: poblar el catálogo desde nixpkgs).
//! Importador `nix → receta takana` (Etapa G: poblar el catálogo desde nixpkgs).
//!
//! El problema: a mano, 34 recetas = userland desierto; un distro usable necesita miles de
//! paquetes. nixpkgs es el mayor set de recetas DESDE FUENTE (sources pineados+hash, deps
//! explícitas), así que es la semilla natural del catálogo. Este importador toma la *receta* de
//! nix (qué bajar, flags, deps) y la reexpresa como una `Recipe` de hammer — **nunca el binario**
//! nix (qué bajar, flags, deps) y la reexpresa como una `Recipe` de takana — **nunca el binario**
//! del cache de nix (eso rompería "verificar, no confiar"; hammer reconstruye desde fuente igual).
//!
//! Consume un JSON NORMALIZADO (lo produce `scripts/nix-import.sh` vía `nix eval --apply`), no el
@@ -53,8 +53,8 @@ pub struct RawSource {
pub output_hash_mode: String,
}
/// El origen ya clasificado para hammer. `fetchurl` plano → `tarball+sha256`; GitHub (codeload) →
/// `repo+commit` (hammer pinea por commit y reproduce clonando, sin depender del hash NAR de nix).
/// El origen ya clasificado para takana. `fetchurl` plano → `tarball+sha256`; GitHub (codeload) →
/// `repo+commit` (takana pinea por commit y reproduce clonando, sin depender del hash NAR de nix).
#[derive(Debug, PartialEq, Eq)]
pub enum NixSource {
Tarball { url: String, sha256: String },
@@ -85,7 +85,7 @@ pub fn classify_source(raw: &RawSource) -> Result<NixSource, String> {
}
/// Traduce el esquema `mirror://<sitio>/...` de nix a una URL concreta. nix resuelve estos
/// mirrors en tiempo de eval; un import los deja literales y el fetch de hammer (curl) no los
/// mirrors en tiempo de eval; un import los deja literales y el fetch de takana (curl) no los
/// entiende. Mapeamos los más comunes a un mirror real; lo no mapeado se deja igual + el usuario
/// elige espejo. (nixpkgs tiene la lista completa en `pkgs/build-support/fetchurl/mirrors.nix`.)
fn expand_nix_mirror(url: &str) -> String {
@@ -138,7 +138,7 @@ fn parse_github_url(url: &str) -> Option<(String, String, String)> {
None
}
/// Convierte una receta nix normalizada en el TOML de una `Recipe` de hammer. El resultado es un
/// Convierte una receta nix normalizada en el TOML de una `Recipe` de takana. El resultado es un
/// PUNTO DE PARTIDA: lleva comentarios marcando lo que falta adaptar (toolchain, nombres de deps).
pub fn to_recipe_toml(pkg: &NixPkg) -> Result<String, String> {
let name = sanitize_name(&pkg.pname);
@@ -156,7 +156,7 @@ pub fn to_recipe_toml(pkg: &NixPkg) -> Result<String, String> {
};
// deps: build = nativeBuildInputs ++ buildInputs (por nombre nix; el humano remapea al corpus
// de hammer si difieren — p.ej. nix `pkg-config` ≈ hammer `pkgconf`).
// de takana si difieren — p.ej. nix `pkg-config` ≈ takana `pkgconf`).
let mut deps: Vec<String> = Vec::new();
for d in pkg.native_build_inputs.iter().chain(pkg.build_inputs.iter()) {
let n = sanitize_name(d);
@@ -170,7 +170,7 @@ pub fn to_recipe_toml(pkg: &NixPkg) -> Result<String, String> {
// zlib, rustls vs openssl) o lo dejan opt-in (pcre2). Cargo resuelve el grafo Rust por vendoring;
// un sys-lib C que SÍ haga falta es adaptación per-paquete (patch/feature, p.ej.
// ripgrep-no-jemalloc), no una dep de corpus. Las recetas Rust validadas (ripgrep/uutils) NO
// declaran `[deps]`; emitirlas rompe `hammer build` (busca recipes/<dep>.toml). Quedan como
// declaran `[deps]`; emitirlas rompe `takana build` (busca recipes/<dep>.toml). Quedan como
// COMENTARIO de provenance (no se pierden: señalan qué C podría necesitarse).
let deps_block = if pkg.is_go {
// Go: la única build-dep es el toolchain `go` (recipes/go.toml). Los buildInputs C de nix
@@ -191,7 +191,7 @@ pub fn to_recipe_toml(pkg: &NixPkg) -> Result<String, String> {
format!("\n[deps]\nbuild = [{list}]\n")
};
// Rust (buildRustPackage): emite la plantilla Cargo de hammer — `--bin <bin>` (cargo exige UN
// Rust (buildRustPackage): emite la plantilla Cargo de takana — `--bin <bin>` (cargo exige UN
// target) + install copiando `target/release/<bin>` a /out (mismo patrón que la receta ripgrep).
// El bin = meta.mainProgram de nix (ripgrep→rg), o el pname. Esto deja la receta build-ready.
let (flags_line, rust_install) = if pkg.is_rust {
@@ -230,7 +230,7 @@ pub fn to_recipe_toml(pkg: &NixPkg) -> Result<String, String> {
let toml = format!(
"# Importada de nixpkgs por `hammer import-nix` (Etapa G). PUNTO DE PARTIDA, no final:\n\
# - el build usa el lab de hammer (zig-cc / musl estático), NO el stdenv de nix revisá\n\
# - el build usa el lab de takana (zig-cc / musl estático), NO el stdenv de nix revisá\n\
# compiler/link/phases y adaptá hasta que compile.\n\
# - las deps van con su nombre NIX; remapealas a las recetas del corpus si difieren.\n\
name = \"{name}\"\n\
@@ -260,7 +260,7 @@ fn is_nix_noise(name: &str) -> bool {
|| name == "auditable-cargo"
|| name.starts_with("auditable-cargo-")
|| name == "version-check"
// El toolchain Rust ES el lab de hammer (cargo vendorea las crates), NO un paquete dep.
// El toolchain Rust ES el lab de takana (cargo vendorea las crates), NO un paquete dep.
|| name == "rustc"
|| name == "cargo"
|| name == "rust"
@@ -416,11 +416,11 @@ mod tests {
}"#;
let pkg: NixPkg = serde_json::from_str(json).unwrap();
let toml = to_recipe_toml(&pkg).unwrap();
// Parsea como Recipe válida de hammer.
// Parsea como Recipe válida de takana.
let recipe = takana_core::Recipe::from_toml(&toml).expect("recipe válida");
assert_eq!(recipe.name, "hello");
assert_eq!(recipe.version, "2.12.1");
// mirror://gnu/ se traduce a una URL concreta que el fetch de hammer entiende.
// mirror://gnu/ se traduce a una URL concreta que el fetch de takana entiende.
assert_eq!(recipe.source.tarball.as_deref(), Some("https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz"));
assert_eq!(
recipe.source.sha256.as_deref(),
@@ -447,7 +447,7 @@ mod tests {
#[test]
fn rust_buildinputs_are_not_active_deps() {
// nix lista el closure maximal (pcre2/openssl…); para Rust NO deben volverse `[deps]` (cargo
// vendorea, backend Rust por defecto) — romperían `hammer build`. Quedan como comentario.
// vendorea, backend Rust por defecto) — romperían `takana build`. Quedan como comentario.
let json = r#"{
"pname": "rg", "version": "14",
"source": { "url": "https://github.com/o/r/archive/abc.tar.gz", "output_hash_mode": "recursive" },
+9 -9
View File
@@ -1,4 +1,4 @@
//! `hammer qorpa` — imágenes ajenas ([ADR 0015](../../../docs/adr/0015-imagenes-ajenas.md)).
//! `takana qorpa` — imágenes ajenas ([ADR 0015](../../../docs/adr/0015-imagenes-ajenas.md)).
//!
//! Superficie en inglés, mensajes en castellano (`CLAUDE.md` regla 4).
//!
@@ -88,7 +88,7 @@ pub enum QorpaCmd {
/// [ADR 0015 §Orden 3] Tira la capa mutable y la rehace desde el manifiesto.
///
/// Es la prueba de D3: si esto duele, es que el `upper` se volvió el activo — y un blob
/// irreemplazable es justo lo que hammer existe para no tener.
/// irreemplazable es justo lo que takana existe para no tener.
Recreate {
id: String,
/// No reinstalar `packages` después de tirar la capa. Deja la instancia como recién creada.
@@ -130,7 +130,7 @@ pub enum QorpaCmd {
///
/// **Es lo que vuelve cierta la frase «el manifiesto es la verdad».** Sin esto `packages` era
/// una lista que nadie leía: `recreate` tiraba la capa y lo instalado a mano no volvía, así que
/// el `upper` era el activo de verdad — exactamente el blob irreemplazable que hammer existe
/// el `upper` era el activo de verdad — exactamente el blob irreemplazable que takana existe
/// para no tener.
Provision {
id: String,
@@ -146,7 +146,7 @@ pub enum QorpaCmd {
/// [ADR 0015 D8] Registra como imagen un artefacto del store que trae una imagen ajena SELLADA.
///
/// Es el consumidor de `recipes/steam-runtime-sniper.toml`: la imagen deja de venir de la CDN
/// de Valve y viene de nuestro store, replicable con `hammer mirror push` y cubrible por el
/// de Valve y viene de nuestro store, replicable con `takana mirror push` y cubrible por el
/// índice firmado (ADR 0014). **La identidad no cambia**: la imagen se registra bajo el sha256
/// del ARCHIVO de upstream que el artefacto declara, no bajo su ArtifactHash, así que la que
/// importa una máquina y la que otra trae con `pull` son la MISMA imagen.
@@ -627,7 +627,7 @@ fn write_manifest(dir: &Path, m: &ImageManifest) -> Result<()> {
//
// D1 dice que las imágenes ajenas no entran al store, y el motivo es que un rootfs MUTABLE no
// reproduce. Sniper no muta —nadie le instala nada adentro— así que sellarlo es una afirmación
// verdadera, y con eso se gana cadena de custodia nuestra: se replica con `hammer mirror push`, su
// verdadera, y con eso se gana cadena de custodia nuestra: se replica con `takana mirror push`, su
// digest puede ir al índice firmado (ADR 0014) y una instancia se arma sin depender de la CDN de
// Valve. Esto es lo que convierte ese artefacto en una imagen usable.
//
@@ -731,7 +731,7 @@ struct Instance {
base: String,
#[serde(skip_serializing_if = "Option::is_none")]
distro: Option<String>,
/// Lo que debe estar instalado adentro. **Se instala solo**: `hammer qorpa provision` lo pone
/// Lo que debe estar instalado adentro. **Se instala solo**: `takana qorpa provision` lo pone
/// con el gestor de la imagen, y `recreate` lo llama después de tirar la capa. Sin esto el
/// `upper` sería el activo y el manifiesto un adorno.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
@@ -2027,7 +2027,7 @@ fn medir(p: &Path) -> (u64, bool) {
// El objetivo es el poder de Bedrock Linux —apps de cualquier «stratum» disponibles en todos
// lados— SIN sus formas: nada de un FUSE global en el camino de cada `exec`, nada de arbitrar por
// heurística qué binario gana. Acá no hace falta, porque **el FHS de esta distro ya es una
// proyección**: `hammer hydrate` proyecta artefactos a un árbol, y extender la proyección a
// proyección**: `takana hydrate` proyecta artefactos a un árbol, y extender la proyección a
// instancias es idiomático.
//
// Tres propiedades que un FUSE no da: **cero costo en runtime** (no hay proceso en el camino
@@ -2170,8 +2170,8 @@ fn export(root: &Path, id: &str, into: Option<&Path>, remove: bool) -> Result<()
let dst = bin_dir.join(b);
let cuerpo = format!(
"#!/bin/sh\n\
# GENERADO por `hammer qorpa export {id}` {MARCA}. No editar: se reescribe.\n\
# Borralo (o corré `hammer qorpa export {id} --remove`) y la instancia deja de verse.\n\
# GENERADO por `takana qorpa export {id}` {MARCA}. No editar: se reescribe.\n\
# Borralo (o corré `takana qorpa export {id} --remove`) y la instancia deja de verse.\n\
exec {hammer} qorpa run{raiz_flag} {id} -- {b} \"$@\"\n",
hammer = shell_quote(&hammer.display().to_string()),
b = shell_quote(b),
+1 -1
View File
@@ -81,7 +81,7 @@ pub struct Proxy {
/// Arranca el proxy en segundo plano. Devuelve el socket a bindear en la jaula.
///
/// Los hilos son detached a propósito: viven lo que viva el proceso, que es exactamente lo que dura
/// la instancia (`hammer qorpa run` bloquea en bwrap).
/// la instancia (`takana qorpa run` bloquea en bwrap).
pub fn arrancar(dir: &Path, socket_real: &Path, permitidos: HashSet<String>) -> io::Result<Proxy> {
std::fs::create_dir_all(dir)?;
let socket = dir.join("wayland-0");
+2 -2
View File
@@ -1,4 +1,4 @@
//! E2E del orquestador `hammer boot menu` (ADR 0010, glue de arranque): emite el grafo, lanza el
//! E2E del orquestador `takana boot menu` (ADR 0010, glue de arranque): emite el grafo, lanza el
//! compositor (mirada) y activa el nodo elegido. Probamos el GLUE headless con stubs de compositor —
//! el pivot de generaciones ya lo cubren los tests de `boot_graph::activate`. Tres caminos:
//! A) sin compositor instalado (servidor headless) → emite el grafo y sigue el arranque (exit 0).
@@ -84,7 +84,7 @@ fn c_seleccion_viaja_de_boot_select_a_activate() {
);
}
// --- `hammer boot activate --from-select` idempotente (contrato PLAN-KIKIN §9) ---------------------
// --- `takana boot activate --from-select` idempotente (contrato PLAN-KIKIN §9) ---------------------
// arje-zero lo invoca INCONDICIONAL en todo apagado: sin selección debe ser un no-op limpio (exit 0),
// y el fichero sólo se consume si la activación tuvo éxito (si falla, queda para diagnóstico).
+1 -1
View File
@@ -1,4 +1,4 @@
//! E2E del CLI `hammer bootstrap stage0` (track posterior, SDD 11). Construye un tarball de
//! E2E del CLI `takana bootstrap stage0` (track posterior, SDD 11). Construye un tarball de
//! semilla falso, lo ingiere vía `file://` (sin red) y verifica el camino completo:
//! sella + imprime el hash, es idempotente, y un sha256 erróneo falla sin sellar.
+3 -3
View File
@@ -1,4 +1,4 @@
//! H4c e2e: el gate de **colisión de fichero observada** en `hammer install` real.
//! H4c e2e: el gate de **colisión de fichero observada** en `takana install` real.
//!
//! Reproduce el caso "logo" a nivel de fichero, sin declarar `slots`: dos paquetes escriben el
//! mismo path. El primero ya está instalado (pre-seed de la DB); al instalar el segundo, el gate
@@ -76,7 +76,7 @@ fn install_aborta_ante_colision_de_fichero_y_force_slots_la_supera() {
);
}
/// H4d e2e: `hammer compat <repo>` particiona un repo contra el estado instalado. La búsqueda
/// H4d e2e: `takana compat <repo>` particiona un repo contra el estado instalado. La búsqueda
/// del prototipo `wawa-memo` (`filtrar`), ahora sobre el repo real y read-only.
#[test]
fn compat_particiona_el_repo_contra_lo_instalado() {
@@ -123,7 +123,7 @@ fn compat_particiona_el_repo_contra_lo_instalado() {
assert!(stdout.contains("tema-oscuro"), "stdout: {stdout}");
}
/// H4e e2e: `hammer compat` marca INCOMPATIBLE un paquete cuya dep de runtime divergiste
/// H4e e2e: `takana compat` marca INCOMPATIBLE un paquete cuya dep de runtime divergiste
/// (el caso wayland, DERIVADO — sin que el paquete declare `requires`). Read-only: ve el
/// source_patch sin construirlo.
#[test]
+2 -2
View File
@@ -1,10 +1,10 @@
//! Roundtrip e2e de Fase 4: construimos un `.swm` a mano (los tipos viven en hammer-core),
//! Roundtrip e2e de Fase 4: construimos un `.swm` a mano (los tipos viven en takana-core),
//! lo aplicamos a un prefix temporal, leemos el resultado y verificamos que los archivos
//! coinciden con lo esperado.
//!
//! NO ejercitamos `source_patch` porque arrastra todo el lab (red + sandbox); para esos
//! casos existen los tests gated en `TAKANA_NETWORK_TESTS`. Aquí cubrimos los caminos puros
//! de hammer-core: `verify_schema`, `verify_base`, `apply_config_edit`, `apply_file_drop`.
//! de takana-core: `verify_schema`, `verify_base`, `apply_config_edit`, `apply_file_drop`.
use std::collections::BTreeMap;
use std::path::Path;
+1 -1
View File
@@ -5,7 +5,7 @@ edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
description = "Tipos compartidos de hammer: Recipe, Swm, hashing CAS y el store."
description = "Tipos compartidos de takana: Recipe, Swm, hashing CAS y el store."
[dependencies]
anyhow.workspace = true
+2 -2
View File
@@ -3,7 +3,7 @@
//! Estas funciones son **puras** sobre el sistema de archivos: reciben paths absolutos (ya
//! re-rooteados al overlay o a un prefix de test) y aplican un único `Mutation` cada una.
//! La orquestación (montar overlay, iterar mutaciones, build de source_patch) vive en
//! `hammer-cli`, no aquí — hammer-core no depende de hammer-build ni del overlay.
//! `takana-cli`, no aquí — takana-core no depende de takana-build ni del overlay.
//!
//! ## `config_edit`
//!
@@ -31,7 +31,7 @@ use crate::hash::ArtifactHash;
/// Directorio canónico donde `init_rule` materializa una regla por servicio. El init
/// (arje, PID 1) lee este árbol para saber qué supervisar. Es el contrato on-disk entre
/// hammer (declara) y el init (ejecuta) — análogo a `/etc/systemd/system` pero nativo.
/// takana (declara) y el init (ejecuta) — análogo a `/etc/systemd/system` pero nativo.
pub const INIT_RULES_DIR: &str = "/etc/hammer/init.d";
#[derive(Debug, thiserror::Error)]
+1 -1
View File
@@ -17,7 +17,7 @@
//! caps = ["query", "compile", "inject", "inject-real", "init"]
//!
//! [[rule]]
//! gid = 994 # p. ej. grupo `hammer`
//! gid = 994 # p. ej. grupo `takana`
//! caps = ["query", "compile", "inject", "init"]
//! ```
+1 -1
View File
@@ -1,6 +1,6 @@
//! H4b — el gate de **compatibilidad de configs** sobre la receta real (SDD 15 §H4).
//!
//! Sube a hammer el modelo de slots que `wawa-memo/src/compat.rs` demostró como
//! Sube a takana el modelo de slots que `wawa-memo/src/compat.rs` demostró como
//! prototipo (host/std, 52 tests verdes): una config/paquete declara qué
//! superficies del sistema **reclama** (`Slots::claims`) y **requiere**
//! (`Slots::requires`), ambas por hash. Antes de tocar el sistema, `install`
+2 -2
View File
@@ -1,4 +1,4 @@
//! `why-differs` — el diffoscope propio de hammer ([SDD 17 §1.2](../../docs/17-cierres-frontera.md)).
//! `why-differs` — el diffoscope propio de takana ([SDD 17 §1.2](../../docs/17-cierres-frontera.md)).
//!
//! Cuando un artefacto **no reproduce**, el store sólo sabe decir "el hash no coincide". Eso deja el
//! trabajo entero al humano: desempacar los dos árboles, `cmp` a mano, adivinar. Este módulo responde
@@ -17,7 +17,7 @@
//!
//! **Sin dependencias externas**: los parsers (gzip/ar/ELF) son mínimos y viven acá, en el mismo
//! estilo que `query::parse_elf_info`. Un diffoscope de verdad se apoya en medio mundo de binarios
//! ajenos; eso es exactamente lo que hammer no puede permitirse (ADR 0004: el catálogo se construye,
//! ajenos; eso es exactamente lo que takana no puede permitirse (ADR 0004: el catálogo se construye,
//! no se importa).
use serde::Serialize;
+1 -1
View File
@@ -1,4 +1,4 @@
//! Lectura de variables de entorno del proyecto durante el renombre `hammer` → `takana`.
//! Lectura de variables de entorno del proyecto durante el renombre `takana` → `takana`.
//!
//! # Por qué existe esto en vez de un `sed`
//!
+1 -1
View File
@@ -1,5 +1,5 @@
//! La **base de datos de paquetes instalados** (Etapa F): qué paquetes hay puestos en un root y
//! qué ficheros aportó cada uno. Es lo que hace posible `hammer uninstall`: sin un registro de
//! qué ficheros aportó cada uno. Es lo que hace posible `takana uninstall`: sin un registro de
//! "este paquete creó estos ficheros", quitar un paquete sería adivinar.
//!
//! Modelo simple y honesto: `install` registra los ficheros que CREÓ (los hidratados del
+1 -1
View File
@@ -29,7 +29,7 @@ pub const CATALOG_VERSION: u32 = 1;
pub struct Catalog {
pub version: u32,
/// Kernel contra el que se revisaron las fugas. Cambiar de versión no invalida el catálogo,
/// pero sí obliga a re-mirar las fugas (`hammer kernel closure`).
/// pero sí obliga a re-mirar las fugas (`takana kernel closure`).
#[serde(default)]
pub reviewed_against: Option<String>,
#[serde(default, rename = "bundle")]
+1 -1
View File
@@ -2,7 +2,7 @@
//! callado si faltan (SDD 25 §4 y §8-W6).
//!
//! ## Por qué existe este módulo
//! SDD 25 encontró, midiendo otra cosa, que los kernels de hammer se construyen **sin
//! SDD 25 encontró, midiendo otra cosa, que los kernels de takana se construyen **sin
//! `CONFIG_MEMCG`**: `memory.max` no existe, y el escritor del otro lado (`arje-incarnate::cgroup`)
//! descarta el error. Una Card que pide un tope de memoria arranca **sin tope** y la única huella
//! es una línea de log. Es la forma de fallo que `CLAUDE.md` §3 nombra: *un ausente falla
+1 -1
View File
@@ -219,7 +219,7 @@ impl Hardware {
}
/// La huella. BLAKE3 sobre [`Hardware::fingerprint_material`], con el mismo tipo que cualquier
/// otro identificador por contenido de hammer.
/// otro identificador por contenido de takana.
pub fn fingerprint(&self) -> ArtifactHash {
ArtifactHash::of_bytes(self.fingerprint_material().as_bytes())
}
+1 -1
View File
@@ -1,6 +1,6 @@
//! Lector del grafo de Kconfig — **sólo para analizar, nunca para resolver**.
//!
//! Regla dura heredada del handoff (§6) y ratificada en `docs/22-configurador-kernel.md`: hammer
//! Regla dura heredada del handoff (§6) y ratificada en `docs/22-configurador-kernel.md`: takana
//! **no** implementa un solver de Kconfig. El `.config` lo sigue produciendo el `olddefconfig` del
//! propio kernel. Lo que este módulo hace es *leer* los ~15 000 símbolos para poder contestar
//! preguntas sobre el grafo — «¿qué muere si apago `WIRELESS`?», «¿quién lo enciende por la
+1 -1
View File
@@ -12,7 +12,7 @@
//! `plan` emite una **receta derivada**, no un binario parametrizable.
//! 2. Atestar «este config booteó en esta huella» sale casi gratis: ya es un artefacto CAS.
//!
//! Y la regla dura que nunca se cruza: **hammer no resuelve Kconfig**. Emite fragmentos
//! Y la regla dura que nunca se cruza: **takana no resuelve Kconfig**. Emite fragmentos
//! (`scripts/config -e/-d`) y deja que el `olddefconfig` del propio kernel produzca el `.config`.
//! Ver [`kconfig`].
+2 -2
View File
@@ -14,7 +14,7 @@
//!
//! ## Qué se emite y qué no
//! Se emiten **raíces**, no clausuras. `-d WIRELESS -d WLAN` son dos banderas; los ~400 símbolos
//! que caen con ellas los calcula el `olddefconfig` **del propio kernel**. hammer sabe cuáles son
//! que caen con ellas los calcula el `olddefconfig` **del propio kernel**. takana sabe cuáles son
//! (para poder explicarlos), pero no los escribe: la app nunca escribe un `.config`.
//!
//! ## Procedencia por símbolo
@@ -477,7 +477,7 @@ compile = "make bzImage"
assert!(p.fragment.contains("-d WIRELESS"));
assert!(p.fragment.contains("-d WLAN"));
assert!(!p.fragment.contains("CFG80211"));
// …pero hammer SABE cuántos caen, para poder explicarlo.
// …pero takana SABE cuántos caen, para poder explicarlo.
assert_eq!(p.closure_size, 4);
}
+2 -2
View File
@@ -1,4 +1,4 @@
//! Tipos núcleo de hammer, compartidos por el lab, la CLI y el daemon.
//! Tipos núcleo de takana, 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
@@ -35,7 +35,7 @@ pub use sign::{KeyPair, SigStatus, TrustStore};
pub use store::Store;
pub use swm::{Base, BaseCompat, BaseRef, Mutation, PinDiff, Signature, Swm, SwmBuild};
/// Error común del ecosistema hammer.
/// Error común del ecosistema takana.
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("io: {0}")]
+1 -1
View File
@@ -13,7 +13,7 @@
//! aparezca un caso de uso real para componer queries, extenderemos sin romper este shape.
//!
//! Diseñado para evaluarse:
//! - **Local**: la CLI puede correr el evaluador en proceso (`hammer query bin:grep`).
//! - **Local**: la CLI puede correr el evaluador en proceso (`takana query bin:grep`).
//! - **Remoto**: el daemon expone `Command::Query { what: "expr", expr: Some(...) }` y
//! reusa este evaluador. Útil para que un cliente sin acceso al disco (o sin caps)
//! consulte estado a través del bus.
+3 -3
View File
@@ -201,7 +201,7 @@ pub struct Build {
///
/// POR QUÉ IMPORTA, medido: el **79%** del contenido binario del store son secciones `.debug_*`
/// ⇒ ~96 G de reserva de disco y un espejo público de ~30 G en vez de ~126 G. Y además **cierra
/// una fuga de reproducibilidad**: `hammer why-differs` mostró que `appstream` y `bison`
/// una fuga de reproducibilidad**: `takana why-differs` mostró que `appstream` y `bison`
/// divergen entre rebuilds ÚNICAMENTE en `.debug_*` (rutas internas del árbol de build), con el
/// código ejecutable idéntico. Sin esas secciones, el artefacto reproduce.
///
@@ -323,7 +323,7 @@ impl Slots {
}
/// Evidencia de comportamiento de una receta (H1 — proof-carrying recipes, SDD 15 §H1). La IA
/// entrega, junto al build, un conjunto de `checks` que el verificador (`hammer swm-verify
/// entrega, junto al build, un conjunto de `checks` que el verificador (`takana swm-verify
/// --evidence`) corre DENTRO del sandbox reproducible; la mutación sólo se propone si todos pasan.
/// La confianza vive en el checker (pequeño, auditable), no en el generador: el sistema mejora solo
/// sin poder corromperse solo. Deliberadamente FUERA de `Recipe::hash_inputs` (comportamiento ≠
@@ -505,7 +505,7 @@ impl Recipe {
toml::from_str(s).map_err(|e| crate::Error::Recipe(e.to_string()))
}
/// Serializa la receta a TOML. Útil para el sidecar de provenance que `hammer-build`
/// Serializa la receta a TOML. Útil para el sidecar de provenance que `takana-build`
/// escribe junto al artefacto en el store. `base_dir` no se serializa (es transient).
pub fn to_toml(&self) -> crate::Result<String> {
toml::to_string(self).map_err(|e| crate::Error::Recipe(e.to_string()))
+3 -3
View File
@@ -1,15 +1,15 @@
//! El **repositorio de paquetes** (Etapa F): un directorio con `.swm` + un `index.json` que
//! mapea `nombre → paquete`. Es el catálogo que `hammer install <nombre>` resuelve.
//! mapea `nombre → paquete`. Es el catálogo que `takana install <nombre>` resuelve.
//!
//! Un `.swm` es un manifiesto de mutación sin identidad propia (no lleva un campo "nombre de
//! paquete"); la **identidad** (nombre+versión) la asigna el repo cuando se publica. Esto
//! mantiene el formato honesto (describe una transformación, no se arroga ser "el paquete X")
//! y deja al repo ser el namespace. `hammer pack --repo` publica con el nombre/versión de la
//! y deja al repo ser el namespace. `takana pack --repo` publica con el nombre/versión de la
//! receta; `install` resuelve por nombre.
//!
//! El índice es CONTENIDO PLANO (serde_json), no un binario: legible, diffeable, firmable más
//! adelante como una release. El transporte del repo (filesystem / sshfs / mirror) es ortogonal
//! — ver `hammer-mirror` para el CAS del store; aquí el repo es simplemente un directorio.
//! — ver `takana-mirror` para el CAS del store; aquí el repo es simplemente un directorio.
use std::path::{Path, PathBuf};
+1 -1
View File
@@ -2,7 +2,7 @@
//!
//! La firma cubre **el contenido del manifiesto** (`swm_version` + `base` + `mutations`), no la
//! propia firma. Sirve para autoría e integridad en tránsito — **no** sustituye a la
//! verificación reproducible: `hammer apply` siempre reproduce y compara, firme o no.
//! verificación reproducible: `takana apply` siempre reproduce y compara, firme o no.
//!
//! Formato en disco (texto, una línea base64 cada uno):
//! - clave privada: `<name>.ed25519` → base64(semilla de 32 bytes), modo 0600
+2 -2
View File
@@ -43,12 +43,12 @@ impl Store {
/// y devolvió Ok**. Cuatro recetas dieron OK sin producir un solo fichero.
///
/// El criterio es "tiene al menos una entrada", NO el sidecar `.hammer/recipe.toml`: ese lo
/// escriben los llamantes (`hammer-build`, `hammer-cli`), no `seal()`, y **`hammer-bootstrap`
/// escriben los llamantes (`takana-build`, `takana-cli`), no `seal()`, y **`takana-bootstrap`
/// sella sin él** ⇒ exigirlo haría que el bootstrap se creyera nunca sellado y reconstruyera
/// siempre. Un artefacto de verdad nunca está vacío, así que esto basta para el fallo real.
///
/// Lo que esto NO cubre: una copia PARCIAL (algún fichero, no todos). Para integridad de
/// contenido están `of_tree` y `hammer attest`; esto es la caché, no la atestación.
/// contenido están `of_tree` y `takana attest`; esto es la caché, no la atestación.
pub fn has(&self, h: &ArtifactHash, name: &str) -> bool {
let dir = self.path_of(h, name);
// `read_dir(..).next().is_some()`: una sola entrada basta, no recorre el árbol.
+4 -4
View File
@@ -72,7 +72,7 @@ pub enum Mutation {
SourcePatch {
// Origen git (modo histórico) **o** tarball — exactamente uno, igual que
// [`crate::recipe::Source`]. Antes sólo se modelaba git y los tarballs caían a
// `file_drop`; ahora `hammer export` los reconstruye como source_patch. Los campos
// `file_drop`; ahora `takana export` los reconstruye como source_patch. Los campos
// son `Option` (con `serde(default)`) para que los `.swm` git existentes —que sólo
// traen `repo`+`commit`— sigan parseando sin cambios.
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -102,7 +102,7 @@ pub enum Mutation {
#[serde(default, skip_serializing_if = "crate::recipe::Deps::is_empty")]
deps: crate::recipe::Deps,
/// Evidencia de comportamiento que acompaña al paquete (H1 — proof-carrying recipes). El
/// receptor puede correr `hammer swm-verify --evidence` para reproducir cada check en el
/// receptor puede correr `takana swm-verify --evidence` para reproducir cada check en el
/// sandbox antes de aceptar la mutación. Vacío ⇒ paquete sin evidencia (sólo reproducibilidad).
#[serde(default, skip_serializing_if = "crate::recipe::Evidence::is_empty")]
evidence: crate::recipe::Evidence,
@@ -246,7 +246,7 @@ impl Swm {
/// sistema YA sabe construir se vuelve un paquete distribuible y reproducible-desde-fuente.
/// Es la inversa de `takana_build::swm_bridge` (que va `source_patch` → `Recipe` → build).
///
/// `hammer-core` no toca disco: el caller resuelve y **lee** los patches de la receta
/// `takana-core` no toca disco: el caller resuelve y **lee** los patches de la receta
/// (relativos a `recipe.base_dir`) y pasa su texto ya concatenado en `patch_text`. El
/// `expected_hash`, si se da, ancla "verificar, no confiar" (el receptor rehace y compara).
pub fn from_recipe(
@@ -394,7 +394,7 @@ impl Mutation {
));
}
// La evidencia (H1) viaja con el paquete: validamos su FORMA acá (cmd no vacío,
// hash bien formado). La EJECUCIÓN la hace `hammer swm-verify --evidence` (H1b).
// hash bien formado). La EJECUCIÓN la hace `takana swm-verify --evidence` (H1b).
evidence.validate().map_err(|e| e.to_string())?;
Ok(())
}
+1 -1
View File
@@ -5,7 +5,7 @@ edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
description = "El diario de mutaciones de hammer: append-only JSON-líneas sobre eventos del FHS."
description = "El diario de mutaciones de takana: append-only JSON-líneas sobre eventos del FHS."
[dependencies]
takana-core.workspace = true
+6 -6
View File
@@ -11,7 +11,7 @@
//! - El diario no bloquea acciones; sólo observa. Pero también acepta eventos "trazables"
//! (de un `hydrate` o `commit`) que llegan con `actor.source = HammerHydrate { artifact }`.
//! - Las "mutaciones opacas" (pisada manual sin rastro de artefacto) llegan con
//! `actor.source = External { pid, uid }`. Eso permite que `hammer export` distinga
//! `actor.source = External { pid, uid }`. Eso permite que `takana export` distinga
//! replays reproducibles de cambios que sólo se pueden compartir como warnings.
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
@@ -46,21 +46,21 @@ impl MutationOp {
}
/// Quién originó la mutación. Distinguir trazable vs opaca es lo que más tarde permite
/// que `hammer export` produzca un `.swm` reproducible (artefactos) más una lista de
/// que `takana export` produzca un `.swm` reproducible (artefactos) más una lista de
/// warnings (pisadas manuales).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum Source {
/// Vino de un `hammer hydrate <artifact>` — la mutación es replay-able a partir del hash.
/// Vino de un `takana hydrate <artifact>` — la mutación es replay-able a partir del hash.
HammerHydrate { artifact: String },
/// Vino de un `hammer commit <overlay>` — origen indirecto: el `actor` fue el overlay,
/// Vino de un `takana commit <overlay>` — origen indirecto: el `actor` fue el overlay,
/// y `artifact` es opcional (puede no haber, si la promoción fue de un edit manual).
HammerCommit {
overlay: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
artifact: Option<String>,
},
/// Pisada manual fuera del flujo de hammer. La grabamos sin juzgar — el SDD insiste en
/// Pisada manual fuera del flujo de takana. La grabamos sin juzgar — el SDD insiste en
/// que es información, no error.
External,
}
@@ -246,7 +246,7 @@ impl Journal {
}
/// Hash BLAKE3 del contenido de un archivo, con prefijo `b3:`. Es un blake3 **plano** del
/// contenido (lo que daría `b3sum`), distinto del `ArtifactHash::of_inputs` de hammer-core
/// contenido (lo que daría `b3sum`), distinto del `ArtifactHash::of_inputs` de takana-core
/// (que prefija longitudes para identidad de artefactos). Aquí queremos "¿el archivo cambió?".
pub fn content_hash_of(bytes: &[u8]) -> String {
format!("b3:{}", blake3::hash(bytes).to_hex())
+1 -1
View File
@@ -1,6 +1,6 @@
//! **E3 — mirror del store content-addressed** ([SDD 13 §E3](../../docs/13-release-engineering.md)).
//!
//! El `/store` de hammer es un CAS: cada artefacto vive en un directorio `<hash>-<name>` y *el hash es
//! El `/store` de takana es un CAS: cada artefacto vive en un directorio `<hash>-<name>` y *el hash es
//! la dirección*. Un mirror, entonces, es ese CAS **replicado** entre dos máquinas + **resolución por
//! hash**. Esta capa replica los artefactos que le faltan al destino y **ancla la integridad en
//! `of_tree`** (el content-hash BLAKE3 del árbol, [`ArtifactHash::of_tree`]): el receptor **recomputa**
+1 -1
View File
@@ -5,7 +5,7 @@ edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
description = "El overlay de experimentación de hammer: try/commit/discard/status sobre overlayfs."
description = "El overlay de experimentación de takana: try/commit/discard/status sobre overlayfs."
[dependencies]
takana-core.workspace = true
+1 -1
View File
@@ -14,7 +14,7 @@
//!
//! Diseño: la librería no asume privilegios — emite los comandos `mount`/`umount` y deja
//! que el proceso que la invoca tenga (o no) CAP_SYS_ADMIN. Esto permite:
//! - El CLI `hammer try` lo invoca con sudo (o setcap) en una máquina real.
//! - El CLI `takana try` lo invoca con sudo (o setcap) en una máquina real.
//! - `hammerd` lo invoca desde su contexto privilegiado (Fase 5).
//! - Los tests lo ejercitan dentro de `bwrap --unshare-user-try --unshare-pid` que da
//! CAP_SYS_ADMIN en un user-ns aislado.
+2 -2
View File
@@ -1,11 +1,11 @@
//! `hammer-recover` — auto-recuperación de upgrades al **arranque** (E4 / refinamiento #4b).
//!
//! El producto NO lleva el CLI `hammer` completo; este mini-binario (estático musl) es lo único que el
//! El producto NO lleva el CLI `takana` completo; este mini-binario (estático musl) es lo único que el
//! sistema instalado necesita para auto-sanar un upgrade interrumpido. El wrapper `/sbin/init` lo corre
//! **tras montar `/store` y `/var/lib/hammer`** y **antes** de incarnar arje-zero:
//!
//! - Sin intento pendiente ⇒ no-op (el caso normal).
//! - Con un `pending.json` (un `hammer upgrade apply` cortado por un apagón/reinicio): intenta
//! - Con un `pending.json` (un `takana upgrade apply` cortado por un apagón/reinicio): intenta
//! **completar** (roll-forward, re-ejecuta el plan idempotente + commitea); si no puede (p.ej. el árbol
//! ya no está en `/store`), **deshace** (roll-back) para dejar el FHS consistente. En cualquier caso el
//! sistema arranca en un estado coherente, no a medias.
+1 -1
View File
@@ -5,7 +5,7 @@ edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
description = "Upgrades atómicos de hammer (E4): aplica un árbol Stage1/producto del store al root vivo con generaciones y rollback."
description = "Upgrades atómicos de takana (E4): aplica un árbol Stage1/producto del store al root vivo con generaciones y rollback."
[dependencies]
takana-core.workspace = true
+9 -9
View File
@@ -1,18 +1,18 @@
//! **Arranque por grafo** ([ADR 0010](../../docs/adr/0010-arranque-grafo-mirada.md)).
//!
//! El menú de arranque de hammer no es una *lista* de kernels (GRUB) sino la **navegación del grafo
//! El menú de arranque de takana no es una *lista* de kernels (GRUB) sino la **navegación del grafo
//! content-addressed de estados** del sistema. Este módulo sube el modelo de generaciones in-place
//! ([`crate`]) a ese grafo navegable y define el **contrato de datos** con `mirada` (el compositor de
//! tawasuyu que dibuja el menú sobre KMS):
//!
//! - **hammer → mirada**: [`build`] arma el grafo y [`emit`] lo escribe en
//! - **takana → mirada**: [`build`] arma el grafo y [`emit`] lo escribe en
//! [`BOOT_GRAPH_PATH`] (`/run/hammer/boot-graph.json`), world-readable y regenerable.
//! - **mirada → hammer**: mirada escribe el `id` elegido en [`BOOT_SELECT_PATH`] **o** invoca
//! `hammer boot activate <id>`; [`activate`] valida el id contra el grafo y **activa** el nodo
//! - **mirada → takana**: mirada escribe el `id` elegido en [`BOOT_SELECT_PATH`] **o** invoca
//! `takana boot activate <id>`; [`activate`] valida el id contra el grafo y **activa** el nodo
//! (pivota la generación vía el rollback existente).
//!
//! La frontera es *un fichero + un comando*, no una API viva: mirada puede maquetar el menú contra un
//! `boot-graph.json` de ejemplo sin esperar a hammer, y hammer emite el grafo sin esperar a mirada.
//! `boot-graph.json` de ejemplo sin esperar a takana, y takana emite el grafo sin esperar a mirada.
//!
//! **Content-addressing.** El `id` de cada nodo es el `of_tree` (BLAKE3) del árbol que la generación
//! proyectó — sin el prefijo `b3:`, para casar con el `<blake3>` del contrato. `parents` forma un DAG
@@ -30,14 +30,14 @@ use crate::{
current, list, live_chain, rollback, Error, GenerationManifest, Result, RollbackReport,
};
/// Path bien conocido donde hammer publica el grafo para mirada. Regenerable, world-readable.
/// Path bien conocido donde takana publica el grafo para mirada. Regenerable, world-readable.
pub const BOOT_GRAPH_PATH: &str = "/run/hammer/boot-graph.json";
/// Path bien conocido donde mirada deja el `id` del nodo a activar (canal de vuelta alternativo a
/// `hammer boot activate <id>`).
/// `takana boot activate <id>`).
pub const BOOT_SELECT_PATH: &str = "/run/hammer/boot-select";
/// Qué representa un nodo del grafo de arranque. Guía cómo lo dibuja mirada y cómo lo activa hammer.
/// Qué representa un nodo del grafo de arranque. Guía cómo lo dibuja mirada y cómo lo activa takana.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum NodeKind {
@@ -219,7 +219,7 @@ pub struct ActivateReport {
/// [`rollback`] atómico del modelo E4).
/// - Si `id` es el nodo de **recuperación** ⇒ un rollback de la generación viva.
/// - Si `id` existe pero **no** está en la cadena viva (un "redo" hacia una generación huérfana) ⇒
/// error honesto: eso requiere re-aplicar el árbol con `hammer upgrade apply`.
/// error honesto: eso requiere re-aplicar el árbol con `takana upgrade apply`.
pub fn activate(
target_root: &Path,
state_root: &Path,
+1 -1
View File
@@ -484,7 +484,7 @@ fn raiz_real(target_root: &Path) -> PathBuf {
/// a través de él— y medir el destino lo rechazaría por error.
///
/// EL FALLO QUE TAPA (medido 2026-08-30, `apply_no_escribe_fuera_del_root_por_un_symlink`): igual
/// que en `hammer-build/hydrate.rs`, proyectar es `target_root.join(rel)` + escribir por ruta, así
/// que en `takana-build/hydrate.rs`, proyectar es `target_root.join(rel)` + escribir por ruta, así
/// que un symlink de DIRECTORIO ya presente en el root se sigue. Una generación que deja puesto
/// `usr/share/pkg → /algún/lado` hacía que la siguiente escribiera `usr/share/pkg/archivo` FUERA
/// del root, con `ApplyReport` en verde. Con `--root /` no cambia nada (todo empieza por `/`); con