repo list/verify: hablan HTTP — un repo remoto con 173 paquetes se leía como vacío

`repo list --repo <url>` tomaba un `PathBuf`, así que trataba la URL como una ruta
relativa que no existe y respondía **«repo vacío» con salida 0**. No es que no
soportara el caso: es que afirmaba lo contrario de lo que pasaba, y con rc=0, que
es la forma de fallo que un script no puede detectar. Salió al probar el repo del
dominio nuevo.

`install --repo` ya sabía hablar HTTP con `RepoSource` (ADR 0014: lista de
orígenes separada por comas, probados en orden). `list` y `verify` ahora usan el
MISMO resolvedor, así que la misma cadena de espejos vale en los tres verbos.
`sign` se queda local a propósito: firmar es escribir.

Y `verify` remoto no es un extra — es el caso de uso del ADR 0014. Verificar el
release de un espejo ANTES de instalarle nada era imposible sin construir.

De paso, tres estados que se daban por iguales y ahora se distinguen (CLAUDE.md
§3: un ausente falla ruidosamente, un vacío llega hasta el final diciendo que todo
fue bien):

  dir que NO EXISTE        → error, rc=1, y sugiere que quizá era una URL
  dir sin index.json       → «repo por crear» (estado válido: lo crea `pack --repo`)
  índice con 0 paquetes    → «índice publicado y SIN paquetes»
  origen que no sirve      → error del fetch, con los orígenes probados

Y el índice se atribuye al origen que REALMENTE lo sirvió, no a la lista entera:
con `<espejo-caído>,<bueno>` la salida dice el bueno. Es la misma regla que el
propio `fetch_desde_algun_origen` aplica a los `.swm` —«decir bajado de la lista
entera cuando sólo uno respondió es una media verdad»— que `read_index` tiraba.

Medido contra el repo vivo: `list` 173 paquetes [release firmado por release],
`verify` trusted. 93+93+6+1+3+4 tests del crate en verde.
This commit is contained in:
Sergio
2026-09-21 21:28:49 +00:00
parent 008692f7b9
commit e0e390c282
2 changed files with 74 additions and 16 deletions
+64 -13
View File
@@ -600,9 +600,10 @@ enum BootCmd {
enum RepoCmd { enum RepoCmd {
/// Lista los paquetes publicados en el repositorio. /// Lista los paquetes publicados en el repositorio.
List { List {
/// Directorio del repositorio (con `index.json`). /// Repositorio: un directorio local (con `index.json`) o una LISTA de orígenes HTTP(S)
/// separada por comas, igual que `install --repo`.
#[arg(long, default_value = DEFAULT_REPO)] #[arg(long, default_value = DEFAULT_REPO)]
repo: PathBuf, repo: String,
}, },
/// Firma el ÍNDICE entero (release): ancla qué paquetes/versiones/hashes existen. Re-firmá /// Firma el ÍNDICE entero (release): ancla qué paquetes/versiones/hashes existen. Re-firmá
/// tras publicar (cada `pack --repo` invalida la firma previa). /// tras publicar (cada `pack --repo` invalida la firma previa).
@@ -618,8 +619,11 @@ enum RepoCmd {
}, },
/// Verifica la firma del release del repositorio contra las claves de confianza. /// Verifica la firma del release del repositorio contra las claves de confianza.
Verify { Verify {
/// Repositorio: un directorio local o una lista de orígenes HTTP(S), igual que
/// `install --repo`. Verificar un release REMOTO es justo lo que hace falta antes de
/// confiar en un espejo, y hasta el 2026-09-21 no se podía: sólo miraba directorios.
#[arg(long, default_value = DEFAULT_REPO)] #[arg(long, default_value = DEFAULT_REPO)]
repo: PathBuf, repo: String,
/// Directorio de claves de confianza. Default `/var/lib/hammer/trust`. /// Directorio de claves de confianza. Default `/var/lib/hammer/trust`.
#[arg(long)] #[arg(long)]
trust: Option<PathBuf>, trust: Option<PathBuf>,
@@ -2651,13 +2655,24 @@ impl RepoSource {
/// Lee el índice del repo (local: `index.json`; http: `GET <base>/index.json`). /// Lee el índice del repo (local: `index.json`; http: `GET <base>/index.json`).
fn read_index(&self) -> anyhow::Result<takana_core::RepoIndex> { fn read_index(&self) -> anyhow::Result<takana_core::RepoIndex> {
Ok(self.read_index_con_origen()?.0)
}
/// Igual, pero dice **quién lo sirvió**. Con varios espejos, `label()` nombra a todos los
/// configurados y sólo uno respondió: atribuir el índice a la lista entera es la misma media
/// verdad que `fetch_desde_algun_origen` evita para los `.swm`, y es justo el dato que hace
/// falta cuando un espejo se queda atrás y otro no.
fn read_index_con_origen(&self) -> anyhow::Result<(takana_core::RepoIndex, String)> {
match self { match self {
RepoSource::Local(d) => Ok(takana_core::RepoIndex::load(d)?), RepoSource::Local(d) => {
Ok((takana_core::RepoIndex::load(d)?, d.display().to_string()))
}
RepoSource::Http(bases) => { RepoSource::Http(bases) => {
let (bytes, origen) = let (bytes, origen) =
Self::fetch_desde_algun_origen(bases, takana_core::repo::INDEX_FILE)?; Self::fetch_desde_algun_origen(bases, takana_core::repo::INDEX_FILE)?;
serde_json::from_slice(&bytes) let index = serde_json::from_slice(&bytes)
.map_err(|e| anyhow::anyhow!("índice remoto de {origen}: {e}")) .map_err(|e| anyhow::anyhow!("índice remoto de {origen}: {e}"))?;
Ok((index, origen.to_string()))
} }
} }
} }
@@ -3466,15 +3481,18 @@ fn run_repo_sign(
/// Verifica la firma del release de un repo contra el TrustStore. /// Verifica la firma del release de un repo contra el TrustStore.
fn run_repo_verify( fn run_repo_verify(
repo_dir: &std::path::Path, repo: &str,
trust: Option<&std::path::Path>, trust: Option<&std::path::Path>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let index = takana_core::RepoIndex::load(repo_dir)?; let (index, origen) = RepoSource::parse(repo).read_index_con_origen()?;
let trust_dir = trust.unwrap_or_else(|| std::path::Path::new(DEFAULT_TRUST_DIR)); let trust_dir = trust.unwrap_or_else(|| std::path::Path::new(DEFAULT_TRUST_DIR));
let store = takana_core::TrustStore::load(trust_dir)?; let store = takana_core::TrustStore::load(trust_dir)?;
match index.verify_signature(&store) { match index.verify_signature(&store) {
takana_core::SigStatus::Trusted { by } => { takana_core::SigStatus::Trusted { by } => {
println!("release: trusted (by {by}) — {} paquete(s)", index.packages.len()); println!(
"release: trusted (by {by}) — {} paquete(s) · índice de {origen}",
index.packages.len()
);
Ok(()) Ok(())
} }
takana_core::SigStatus::UnknownKey { by } => { takana_core::SigStatus::UnknownKey { by } => {
@@ -3495,17 +3513,50 @@ fn run_repo_verify(
} }
/// Lista los paquetes publicados en un repositorio. /// Lista los paquetes publicados en un repositorio.
fn run_repo_list(repo_dir: &std::path::Path) -> anyhow::Result<()> { ///
let index = takana_core::RepoIndex::load(repo_dir)?; /// # Por qué acepta una URL (2026-09-21)
///
/// Antes tomaba un `PathBuf` y sólo miraba directorios, así que
/// `repo list --repo https://takana-os.net/repo` trataba la URL como una ruta relativa que no
/// existe y respondía **«vacío», con salida 0**: la única forma de mirar un repo remoto era
/// `install`, que además construye. Peor que no soportarlo: decía que el repo no tenía nada
/// cuando tenía 173 paquetes. `install --repo` ya sabía hablar HTTP con `RepoSource`; esto usa
/// exactamente el mismo resolvedor, así que la misma cadena de orígenes vale en los dos verbos.
fn run_repo_list(repo: &str) -> anyhow::Result<()> {
let src = RepoSource::parse(repo);
let (index, origen) = src.read_index_con_origen()?;
if index.packages.is_empty() { if index.packages.is_empty() {
println!("repo {} vacío (sin index.json o sin paquetes)", repo_dir.display()); // Un directorio SIN `index.json` no es lo mismo que un índice con cero paquetes, y la
// salida vieja los daba por iguales. Lo primero es un repo que todavía no existe —estado
// válido: `pack --repo <dir-nuevo>` lo crea—, lo segundo es un repo publicado y vacío.
// (El caso remoto no llega acá: si el origen no sirve `index.json`, `read_index` falla.)
match &src {
// AUSENTE y VACÍO no son lo mismo (CLAUDE.md §3): el directorio que no existe suele
// ser una ruta mal escrita —o una URL pasada sin `http(s)://`, que `parse` toma por
// ruta— y decirle «repo por crear» manda a buscar el problema al sitio equivocado.
RepoSource::Local(d) if !d.exists() => {
anyhow::bail!(
"repo {}: el directorio no existe (¿ruta mal escrita? si era un repo REMOTO, la URL tiene que empezar por http(s)://)",
d.display()
);
}
RepoSource::Local(d) if !d.join(takana_core::repo::INDEX_FILE).exists() => {
println!(
"repo {} — todavía no hay {}: es un repo por crear, no uno vacío \
(lo crea `pack --repo`)",
d.display(),
takana_core::repo::INDEX_FILE
);
}
_ => println!("repo {origen} — índice publicado y SIN paquetes"),
}
return Ok(()); return Ok(());
} }
let rel = match &index.signature { let rel = match &index.signature {
Some(s) => format!(" [release firmado por {}]", s.by), Some(s) => format!(" [release firmado por {}]", s.by),
None => String::new(), None => String::new(),
}; };
println!("repo {}{} paquete(s){}:", repo_dir.display(), index.packages.len(), rel); println!("repo {}{} paquete(s){}:", origen, index.packages.len(), rel);
for p in &index.packages { for p in &index.packages {
let sig = p.signed_by.as_deref().map(|b| format!(" [firmado por {b}]")).unwrap_or_default(); let sig = p.signed_by.as_deref().map(|b| format!(" [firmado por {b}]")).unwrap_or_default();
let anchor = p let anchor = p
+10 -3
View File
@@ -147,10 +147,17 @@ CLI:
`.swm`; con URL baja el índice + el cierre de deps a un temporal y procede igual). `.swm`; con URL baja el índice + el cierre de deps a un temporal y procede igual).
`--prefix`/`--skip-source-patch` para staging y dry-run de schema. `--require-signed` exige que `--prefix`/`--skip-source-patch` para staging y dry-run de schema. `--require-signed` exige que
el release esté firmado por una clave confiada (modo estricto: no basta con reproducir). el release esté firmado por una clave confiada (modo estricto: no basta con reproducir).
- `takana repo list [--repo DIR]` — lista el catálogo (`<repo>/index.json`). - `takana repo list [--repo DIR|URL]` — lista el catálogo (`<repo>/index.json`). Desde el
2026-09-21 acepta lo MISMO que `install --repo`: un directorio o una lista de orígenes HTTP(S)
separada por comas. Antes sólo miraba directorios y ante una URL respondía **«vacío» con salida
0** — un repo remoto con 173 paquetes se leía como uno sin nada. Y ahora distingue «todavía no
hay `index.json`» (repo por crear) de «índice publicado y sin paquetes», que también se daban
por iguales.
- `takana repo sign --repo DIR --key KEY` — firma el ÍNDICE entero (release). Re-firmá tras - `takana repo sign --repo DIR --key KEY` — firma el ÍNDICE entero (release). Re-firmá tras
publicar (cada `pack --repo` invalida la firma del release). publicar (cada `pack --repo` invalida la firma del release). **Sólo local:** firmar es escribir.
- `takana repo verify --repo DIR [--trust DIR]` — verifica la firma del release. - `takana repo verify --repo DIR|URL [--trust DIR]` — verifica la firma del release. También
acepta orígenes HTTP(S): verificar el release de un espejo ANTES de instarle nada es justo el
caso de uso del ADR 0014, y hasta el 2026-09-21 no había forma de hacerlo sin construir.
- `takana uninstall <nombre> [--db FILE]` — borra los ficheros que el paquete registró (refcount: - `takana uninstall <nombre> [--db FILE]` — borra los ficheros que el paquete registró (refcount:
respeta los que otro paquete instalado también aporta) y lo quita de la DB de instalados. respeta los que otro paquete instalado también aporta) y lo quita de la DB de instalados.
- `takana installed [--db FILE]` — lista los paquetes instalados. `install` registra cada paquete - `takana installed [--db FILE]` — lista los paquetes instalados. `install` registra cada paquete