refactor(naming): A1 — ente→arje, vista→revista, pluma→fana

Rename batch de la Fase A del PLAN_MACRO:
- 25 crates ente-* → arje-* (protocol/init/runtime/compat). El linaje
  arje (init Linux) queda con prefijo coherente.
- vista → revista (revista-core + revista-web).
- pluma → fana (fana-md + fana-md-reader-web). fana absorbe el linaje
  markdown de pluma; será el writer DAG editor (prioridad alta).

Cambios:
- git mv de 29 crate dirs + 2 SDDs
- package/lib/bin names + path refs + imports .rs reescritos
- workspace Cargo.toml + comentarios de sección
- SDDs de init/runtime/compat/protocol actualizados a arje-
- SDD de revista + SDD de fana (reescrito: writer DAG editor)
- docs/STATUS.md, ROADMAP.md, PLAN_MACRO.md, arje-boot.md,
  arje-replace-systemd.md actualizados
- docs/changelog/akasha.md → chasqui.md

scripts/rename-fase-a.py idempotente (--dry-run soportado).
cargo check --workspace verde.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-20 00:10:14 +00:00
parent 3fc6dcfa72
commit b83d40a833
159 changed files with 2384 additions and 1111 deletions
+184
View File
@@ -0,0 +1,184 @@
//! Persistencia de la keypair Ed25519 de identidad libp2p de Arje.
//!
//! El `peer_id` que Arje presenta en la malla `brahman-net` deriva de
//! esta keypair. Si se regenera en cada arranque, el peer_id cambia
//! y los nodos remotos pierden la referencia. Persistir el secret a
//! disco (32 bytes raw, permisos 0o600) garantiza identidad estable.
//!
//! ## Path
//!
//! Por orden de precedencia:
//! 1. `BRAHMAN_KEYPAIR_PATH` env var (override explícito).
//! 2. Si PID 1 / root: `/var/lib/brahman/init-keypair.bin`.
//! 3. Si dev mode: `$XDG_DATA_HOME/brahman/init-keypair.bin`, fallback
//! a `$HOME/.local/share/brahman/init-keypair.bin`, último recurso
//! `/tmp/brahman-init-keypair.bin` (sin persistencia útil pero al
//! menos no rompe en CI minimalista).
//!
//! ## Formato
//!
//! 32 bytes raw del secret Ed25519. Sin header, sin metadata. La
//! public key se deriva determinísticamente al cargar. Esto evita
//! depender de un schema de serialización (postcard, json) que
//! pudiera bumpear y romper compat de identidad.
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use brahman_net::Keypair;
/// Tamaño exacto del secret Ed25519.
const SECRET_LEN: usize = 32;
/// Carga la keypair desde `path` si existe, o genera una nueva,
/// la persiste y la devuelve. Devuelve también si fue cargada (true)
/// o generada (false), para logging.
pub fn load_or_generate(path: &Path) -> Result<(Keypair, bool)> {
if path.exists() {
let bytes = std::fs::read(path)
.with_context(|| format!("leer keypair de {}", path.display()))?;
if bytes.len() != SECRET_LEN {
bail!(
"keypair en {} tiene {} bytes, esperaba {}",
path.display(),
bytes.len(),
SECRET_LEN
);
}
let mut secret = [0u8; SECRET_LEN];
secret.copy_from_slice(&bytes);
let kp = Keypair::ed25519_from_bytes(secret)
.with_context(|| format!("decodificar keypair en {}", path.display()))?;
Ok((kp, true))
} else {
let kp = Keypair::generate_ed25519();
save(path, &kp).context("persistir keypair recién generada")?;
Ok((kp, false))
}
}
/// Persiste el secret de `keypair` a `path`. Crea directorios padres,
/// escribe atómico (vía rename), y aplica permisos 0o600 (sólo dueño).
fn save(path: &Path, keypair: &Keypair) -> Result<()> {
let secret = extract_secret_bytes(keypair)?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("crear dir {}", parent.display()))?;
}
let tmp = path.with_extension("tmp");
std::fs::write(&tmp, secret).with_context(|| format!("write tmp {}", tmp.display()))?;
apply_owner_only_perms(&tmp).context("permisos 0o600 en tmp")?;
std::fs::rename(&tmp, path)
.with_context(|| format!("rename {}{}", tmp.display(), path.display()))?;
Ok(())
}
fn extract_secret_bytes(keypair: &Keypair) -> Result<[u8; SECRET_LEN]> {
// libp2p::Keypair no expone secret() directo; pasamos
// por la variante ed25519. Solo Ed25519 soportado en brahman-net,
// así que el unwrap es seguro tras with_keypair.
let ed = keypair
.clone()
.try_into_ed25519()
.map_err(|_| anyhow::anyhow!("la keypair no es Ed25519 (no debería pasar)"))?;
let bytes = ed.secret();
let raw: &[u8] = bytes.as_ref();
if raw.len() != SECRET_LEN {
bail!("ed25519 secret no es {} bytes", SECRET_LEN);
}
let mut out = [0u8; SECRET_LEN];
out.copy_from_slice(raw);
Ok(out)
}
#[cfg(unix)]
fn apply_owner_only_perms(path: &Path) -> std::io::Result<()> {
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o600);
std::fs::set_permissions(path, perms)
}
#[cfg(not(unix))]
fn apply_owner_only_perms(_path: &Path) -> std::io::Result<()> {
Ok(())
}
/// Resuelve el path del keystore según convención (env > root path >
/// XDG > HOME > tmp).
pub fn default_path(dev_mode: bool) -> PathBuf {
if let Ok(p) = std::env::var("BRAHMAN_KEYPAIR_PATH") {
return PathBuf::from(p);
}
if !dev_mode {
// PID 1: paths del sistema. /var/lib es el lugar canónico
// para state persistente de servicios root.
return PathBuf::from("/var/lib/brahman/init-keypair.bin");
}
// Dev mode: paths de usuario.
if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
return PathBuf::from(xdg).join("brahman").join("init-keypair.bin");
}
if let Ok(home) = std::env::var("HOME") {
return PathBuf::from(home)
.join(".local")
.join("share")
.join("brahman")
.join("init-keypair.bin");
}
PathBuf::from("/tmp/brahman-init-keypair.bin")
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn generate_persist_and_reload_yields_same_peer_id() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("identity.bin");
let (kp1, loaded) = load_or_generate(&path).unwrap();
assert!(!loaded, "primera vez debe generar");
let peer1 = kp1.public().to_peer_id();
let (kp2, loaded) = load_or_generate(&path).unwrap();
assert!(loaded, "segunda vez debe cargar");
let peer2 = kp2.public().to_peer_id();
assert_eq!(peer1, peer2, "peer_id estable across reloads");
}
#[test]
fn rejects_corrupted_file() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("bad.bin");
std::fs::write(&path, b"too short").unwrap();
assert!(load_or_generate(&path).is_err());
}
#[test]
#[cfg(unix)]
fn persisted_file_is_owner_only() {
use std::os::unix::fs::PermissionsExt;
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("perm.bin");
let _ = load_or_generate(&path).unwrap();
let mode = std::fs::metadata(&path).unwrap().permissions().mode();
assert_eq!(
mode & 0o777,
0o600,
"permisos del keypair file deben ser 0o600 (solo dueño), got {:o}",
mode & 0o777
);
}
#[test]
fn default_path_honors_env() {
std::env::set_var("BRAHMAN_KEYPAIR_PATH", "/custom/path.bin");
assert_eq!(default_path(false), PathBuf::from("/custom/path.bin"));
assert_eq!(default_path(true), PathBuf::from("/custom/path.bin"));
std::env::remove_var("BRAHMAN_KEYPAIR_PATH");
}
}