SDD 30 §4b: la seed de producto ya sale de la RECETA — y openssh reproduce bit a bit
El emisor lee la receta que viaja DENTRO del artefacto sellado (el sidecar
.hammer/recipe.toml) y no de recipes/, porque seal_product_rootfs sirve a las
DOS vetas —product desde recetas locales y product_from_repo desde el repo
firmado— y sólo una tiene recipes/ a mano. El sidecar es la única fuente que
ambas comparten, que es lo que sostiene que converjan al mismo product-rootfs.
LA TRAMPA QUEDA ARMADA, NO CERRADA: el sidecar no entra al ArtifactHash, así que
un artefacto anterior al bloque [[service]] es un cache-hit válido que no se
reconstruye solo. Si el emisor devolviera lista vacía sin protestar, el producto
saldría booteando, verde y SIN SSHD. Falla ruidosamente, y hay un test que exige
que el mensaje siga explicando la causa Y dando el arreglo (regla 3 del
CLAUDE.md: un ausente falla ruidosamente, un vacío llega hasta el final diciendo
que todo fue bien).
LA MIGRACIÓN, CON CONTROL, y el hecho nuevo que dejó: openssh no estaba
certificado como reproducible ("el criterio es construye y corre", dice su
receta). Manifiesto de 33 sha256+modos ANTES de tocar nada, respaldo por
hardlink, borrado, rebuild bajo flock -o, comparación. Resultado: 33/33
ficheros, modos idénticos, y la ÚNICA diferencia en todo el árbol es
.hammer/recipe.toml. openssh REPRODUCE BIT A BIT.
Gotcha que costó el primer intento: store/ es bind-mount de /dev/sdb y work/
vive en /dev/sdc ⇒ `cp -al` cruza mounts y muere con EXDEV. El respaldo va
DENTRO del mount del store; un dot-dir ahí es invisible para store-gc.sh, que
sólo mira dirs ^[0-9a-f]{64}-.
This commit is contained in:
@@ -540,19 +540,75 @@ pub fn verify_attestation(rootfs_dir: &Path) -> Result<AttestReport> {
|
|||||||
Ok(report)
|
Ok(report)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Las Cards de servicio que aportan los componentes de servicio, **leídas de la receta que viaja
|
||||||
|
/// DENTRO de cada artefacto sellado** (el sidecar de provenance `.hammer/recipe.toml`).
|
||||||
|
///
|
||||||
|
/// Por qué del sidecar y no del directorio de recetas: [`seal_product_rootfs`] sirve a las DOS
|
||||||
|
/// vetas —`product` desde recetas locales y `product_from_repo` desde el repo firmado— y sólo una
|
||||||
|
/// tiene `recipes/` a mano. El sidecar es la única fuente que ambas comparten, y así las dos
|
||||||
|
/// convergen al mismo `product-rootfs`, que es la propiedad que esa función existe para sostener.
|
||||||
|
///
|
||||||
|
/// Sólo entran las de `scope = "system"`: una Card de sesión en el `genesis` arrancaría antes de que
|
||||||
|
/// exista `XDG_RUNTIME_DIR` o un usuario logueado (ver `takana_core::service::Scope`).
|
||||||
|
///
|
||||||
|
/// ⚠ **Falla RUIDOSAMENTE si no sale ninguna**, y la razón es una trampa concreta: el sidecar **no
|
||||||
|
/// entra en el `ArtifactHash`**, así que un artefacto sellado antes de que la receta declarara sus
|
||||||
|
/// servicios sigue siendo un cache-hit válido y **no se reconstruye solo**. Si esto devolviera una
|
||||||
|
/// lista vacía sin protestar, el producto saldría sin sshd —booteando, verde y sin acceso— que es
|
||||||
|
/// exactamente la clase de fallo que este proyecto ya pagó con los artefactos vacíos.
|
||||||
|
fn service_cards(
|
||||||
|
store: &Store,
|
||||||
|
services: &[(String, ArtifactHash)],
|
||||||
|
) -> Result<Vec<serde_json::Value>> {
|
||||||
|
let mut cards = Vec::new();
|
||||||
|
let mut sin_sidecar = Vec::new();
|
||||||
|
for (nombre, hash) in services {
|
||||||
|
let recipe = match store.recipe_for_hash(hash.as_str()) {
|
||||||
|
Ok(Some(r)) => r,
|
||||||
|
Ok(None) => {
|
||||||
|
sin_sidecar.push(nombre.clone());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(e) => return Err(Error::Other(format!("sidecar de `{nombre}` ilegible: {e}"))),
|
||||||
|
};
|
||||||
|
for s in &recipe.services {
|
||||||
|
if s.scope == takana_core::service::Scope::System {
|
||||||
|
cards.push(s.card());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cards.is_empty() {
|
||||||
|
return Err(Error::Other(format!(
|
||||||
|
"ningún componente de servicio ({}) declara un servicio de sistema.\n\
|
||||||
|
Casi seguro los artefactos son ANTERIORES al bloque `[[service]]`: el sidecar no entra \
|
||||||
|
en el ArtifactHash, así que siguen siendo cache-hit válidos y no se reconstruyen solos.\n\
|
||||||
|
Arreglo: borrar el artefacto del store y reconstruirlo (`flock -o work/.farm-build.lock \
|
||||||
|
takana build recipes/<receta>.toml`), comparando el árbol nuevo con el viejo.{}",
|
||||||
|
services.iter().map(|(n, _)| n.as_str()).collect::<Vec<_>>().join(", "),
|
||||||
|
if sin_sidecar.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!("\nSin sidecar siquiera: {}", sin_sidecar.join(", "))
|
||||||
|
}
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(cards)
|
||||||
|
}
|
||||||
|
|
||||||
/// Seed de producto = la seed base con los cards de servicio APENDADOS al `genesis`. Composición vía
|
/// Seed de producto = la seed base con los cards de servicio APENDADOS al `genesis`. Composición vía
|
||||||
/// `serde_json` (takana 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.
|
/// `product-rootfs` es reproducible. El núcleo (hammerd + getty) se preserva tal cual.
|
||||||
fn product_seed_card() -> Result<String> {
|
fn product_seed_card(cards: &[serde_json::Value]) -> Result<String> {
|
||||||
let mut seed: serde_json::Value = serde_json::from_str(STAGE1_SEED_CARD)
|
let mut seed: serde_json::Value = serde_json::from_str(STAGE1_SEED_CARD)
|
||||||
.map_err(|e| Error::Other(format!("seed base inválida: {e}")))?;
|
.map_err(|e| Error::Other(format!("seed base inválida: {e}")))?;
|
||||||
seed["label"] = serde_json::Value::String("hammer-product".into());
|
seed["label"] = serde_json::Value::String("hammer-product".into());
|
||||||
let card: serde_json::Value = serde_json::from_str(SSHD_SERVICE_CARD)
|
let genesis = seed
|
||||||
.map_err(|e| Error::Other(format!("card sshd inválida: {e}")))?;
|
.get_mut("genesis")
|
||||||
seed.get_mut("genesis")
|
|
||||||
.and_then(|g| g.as_array_mut())
|
.and_then(|g| g.as_array_mut())
|
||||||
.ok_or_else(|| Error::Other("seed base sin array `genesis`".into()))?
|
.ok_or_else(|| Error::Other("seed base sin array `genesis`".into()))?;
|
||||||
.push(card);
|
for c in cards {
|
||||||
|
genesis.push(c.clone());
|
||||||
|
}
|
||||||
serde_json::to_string_pretty(&seed).map_err(|e| Error::Other(format!("serializar seed: {e}")))
|
serde_json::to_string_pretty(&seed).map_err(|e| Error::Other(format!("serializar seed: {e}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,7 +801,7 @@ fn seal_product_rootfs(
|
|||||||
services: &[(String, ArtifactHash)],
|
services: &[(String, ArtifactHash)],
|
||||||
store: &Store,
|
store: &Store,
|
||||||
) -> Result<RootfsHash> {
|
) -> Result<RootfsHash> {
|
||||||
let seed = product_seed_card()?;
|
let seed = product_seed_card(&service_cards(store, services)?)?;
|
||||||
let phash = product_rootfs_hash(base, userland, services, &seed);
|
let phash = product_rootfs_hash(base, userland, services, &seed);
|
||||||
let store_name = "product-rootfs";
|
let store_name = "product-rootfs";
|
||||||
|
|
||||||
@@ -2070,7 +2126,7 @@ mod tests {
|
|||||||
fn product_seed_appends_sshd_to_base_genesis() {
|
fn product_seed_appends_sshd_to_base_genesis() {
|
||||||
// La seed de producto = base + sshd; el núcleo (hammerd, getty) se preserva intacto y el
|
// La seed de producto = base + sshd; el núcleo (hammerd, getty) se preserva intacto y el
|
||||||
// resultado es JSON válido (lo que arje-zero validará al boot).
|
// resultado es JSON válido (lo que arje-zero validará al boot).
|
||||||
let seed = product_seed_card().unwrap();
|
let seed = product_seed_card(&[card_sshd_esperada()]).unwrap();
|
||||||
let v: serde_json::Value = serde_json::from_str(&seed).unwrap();
|
let v: serde_json::Value = serde_json::from_str(&seed).unwrap();
|
||||||
assert_eq!(v["label"], "hammer-product");
|
assert_eq!(v["label"], "hammer-product");
|
||||||
assert_eq!(v["payload"], "Virtual");
|
assert_eq!(v["payload"], "Virtual");
|
||||||
@@ -2083,6 +2139,35 @@ mod tests {
|
|||||||
assert!(sshd["supervision"]["Restart"]["initial"].is_number(), "sshd es Restart (lifecycle)");
|
assert!(sshd["supervision"]["Restart"]["initial"].is_number(), "sshd es Restart (lifecycle)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// **El sidecar rancio NO puede pasar en silencio.**
|
||||||
|
///
|
||||||
|
/// Es la trampa que este mecanismo tiene incorporada y que hay que dejar armada: el sidecar de
|
||||||
|
/// provenance **no entra en el `ArtifactHash`**, así que un artefacto sellado antes de que su
|
||||||
|
/// receta declarara servicios sigue siendo un cache-hit perfectamente válido y no se reconstruye
|
||||||
|
/// nunca solo. Si `service_cards` devolviera una lista vacía sin protestar, el producto saldría
|
||||||
|
/// **booteando, verde y sin sshd**. Un ausente falla ruidosamente; un vacío llega hasta el final
|
||||||
|
/// diciendo que todo fue bien — regla 3 del CLAUDE.md, aplicada acá antes de pagarla.
|
||||||
|
#[test]
|
||||||
|
fn service_cards_falla_ruidosamente_con_sidecar_rancio() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let store = Store::open(tmp.path().join("store")).unwrap();
|
||||||
|
// Un store sin ese artefacto ⇒ sin sidecar ⇒ sin servicios: exactamente lo que se ve cuando
|
||||||
|
// el artefacto es anterior al bloque `[[service]]`.
|
||||||
|
let servicios = [("openssh".to_string(), ArtifactHash::from_hex("ab"))];
|
||||||
|
let e = service_cards(&store, &servicios).unwrap_err().to_string();
|
||||||
|
assert!(e.contains("ningún componente de servicio"), "{e}");
|
||||||
|
assert!(e.contains("ArtifactHash"), "el error tiene que EXPLICAR la trampa: {e}");
|
||||||
|
assert!(e.contains("borrar el artefacto del store"), "y decir el arreglo: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// La Card de `sshd` de referencia: la constante que el producto **ya bootea en QEMU**. Los
|
||||||
|
/// tests componen la seed con ella (no con la derivada del sidecar) a propósito: así el
|
||||||
|
/// esqueleto de la seed se prueba sin necesitar un store, y la equivalencia
|
||||||
|
/// «receta ⇒ esta misma card» la prueba `la_receta_de_openssh_reproduce_la_card_hardcodeada`.
|
||||||
|
fn card_sshd_esperada() -> serde_json::Value {
|
||||||
|
serde_json::from_str(SSHD_SERVICE_CARD).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
/// **La receta produce la MISMA Card que la constante hardcodeada.**
|
/// **La receta produce la MISMA Card que la constante hardcodeada.**
|
||||||
///
|
///
|
||||||
/// Esta es la prueba que autoriza a mover el servicio de un literal de Rust a `[[service]]` en
|
/// Esta es la prueba que autoriza a mover el servicio de un literal de Rust a `[[service]]` en
|
||||||
@@ -2113,7 +2198,7 @@ mod tests {
|
|||||||
let base = ArtifactHash::from_hex("ab");
|
let base = ArtifactHash::from_hex("ab");
|
||||||
let userland = [("uutils".to_string(), ArtifactHash::from_hex("11"))];
|
let userland = [("uutils".to_string(), ArtifactHash::from_hex("11"))];
|
||||||
let svcs = [("openssh".to_string(), ArtifactHash::from_hex("cd"))];
|
let svcs = [("openssh".to_string(), ArtifactHash::from_hex("cd"))];
|
||||||
let seed = product_seed_card().unwrap();
|
let seed = product_seed_card(&[card_sshd_esperada()]).unwrap();
|
||||||
let a = product_rootfs_hash(&base, &userland, &svcs, &seed);
|
let a = product_rootfs_hash(&base, &userland, &svcs, &seed);
|
||||||
assert_eq!(a, product_rootfs_hash(&base, &userland, &svcs, &seed), "determinista");
|
assert_eq!(a, product_rootfs_hash(&base, &userland, &svcs, &seed), "determinista");
|
||||||
let other_base = ArtifactHash::from_hex("ef");
|
let other_base = ArtifactHash::from_hex("ef");
|
||||||
@@ -2165,7 +2250,7 @@ mod tests {
|
|||||||
std::fs::write(w.join("usr/bin/netup"), b"\x7fELFfake").unwrap();
|
std::fs::write(w.join("usr/bin/netup"), b"\x7fELFfake").unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
let seed = product_seed_card().unwrap();
|
let seed = product_seed_card(&[card_sshd_esperada()]).unwrap();
|
||||||
let userland = [("uutils".to_string(), huu)];
|
let userland = [("uutils".to_string(), huu)];
|
||||||
let services = [("openssh".to_string(), hossh), ("netup".to_string(), hnet)];
|
let services = [("openssh".to_string(), hossh), ("netup".to_string(), hnet)];
|
||||||
let staging = store.root().join("prod-staging");
|
let staging = store.root().join("prod-staging");
|
||||||
@@ -2266,7 +2351,7 @@ mod tests {
|
|||||||
std::fs::write(w.join("usr/bin/arje-zero"), b"\x7fELF-arje-zero-CORE").unwrap();
|
std::fs::write(w.join("usr/bin/arje-zero"), b"\x7fELF-arje-zero-CORE").unwrap();
|
||||||
std::fs::write(w.join("usr/bin/hammerd"), b"\x7fELF-hammerd").unwrap();
|
std::fs::write(w.join("usr/bin/hammerd"), b"\x7fELF-hammerd").unwrap();
|
||||||
std::fs::write(w.join("bin/busybox"), b"\x7fELF-busybox").unwrap();
|
std::fs::write(w.join("bin/busybox"), b"\x7fELF-busybox").unwrap();
|
||||||
std::fs::write(w.join("ente/seed.card.json"), product_seed_card().unwrap()).unwrap();
|
std::fs::write(w.join("ente/seed.card.json"), product_seed_card(&[card_sshd_esperada()]).unwrap()).unwrap();
|
||||||
// attest.json del producto base (con el arje-zero del NÚCLEO) — debe regenerarse.
|
// attest.json del producto base (con el arje-zero del NÚCLEO) — debe regenerarse.
|
||||||
let a = build_attestation(w).unwrap();
|
let a = build_attestation(w).unwrap();
|
||||||
std::fs::write(w.join("ente/attest.json"), serde_json::to_string(&a).unwrap()).unwrap();
|
std::fs::write(w.join("ente/attest.json"), serde_json::to_string(&a).unwrap()).unwrap();
|
||||||
@@ -2363,7 +2448,7 @@ open(os.path.join(os.path.dirname(out),"..","..","bins-seen.txt"),"w").write("\n
|
|||||||
// (lo que el gate busca en las concesiones firmadas) y las rutas resolver dentro del staging.
|
// (lo que el gate busca en las concesiones firmadas) y las rutas resolver dentro del staging.
|
||||||
let staging = Path::new("/tmp/fake-staging");
|
let staging = Path::new("/tmp/fake-staging");
|
||||||
let seed: serde_json::Value =
|
let seed: serde_json::Value =
|
||||||
serde_json::from_str(&product_seed_card().unwrap()).unwrap();
|
serde_json::from_str(&product_seed_card(&[card_sshd_esperada()]).unwrap()).unwrap();
|
||||||
let bins = critical_bins_from_seed(&seed, staging);
|
let bins = critical_bins_from_seed(&seed, staging);
|
||||||
let labels: Vec<&str> = bins.iter().map(|(l, _)| l.as_str()).collect();
|
let labels: Vec<&str> = bins.iter().map(|(l, _)| l.as_str()).collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# SDD 30 — Los servicios que trae un paquete
|
# SDD 30 — Los servicios que trae un paquete
|
||||||
|
|
||||||
> **Estado:** §3 y §4a IMPLEMENTADOS (2026-09-12), §4b/§4c y §5 abiertos. Nace de una pregunta directa:
|
> **Estado:** §3, §4a y §4b IMPLEMENTADOS (2026-09-12/13), §4c y §5 abiertos. Nace de una pregunta directa:
|
||||||
> *«los paquetes que tienen sus servicios para systemd, openrc u otros, ¿ya saben empaquetarse en
|
> *«los paquetes que tienen sus servicios para systemd, openrc u otros, ¿ya saben empaquetarse en
|
||||||
> takana con arje?»*. La respuesta medida era **no**, y este documento dice exactamente qué faltaba,
|
> takana con arje?»*. La respuesta medida era **no**, y este documento dice exactamente qué faltaba,
|
||||||
> qué se cerró y qué queda.
|
> qué se cerró y qué queda.
|
||||||
@@ -142,24 +142,45 @@ CONTROL, que tiene que pasar en verde**— incluidos «dos recetas del mismo per
|
|||||||
label» (ERROR) y «el mismo label en dos colas distintas» (NO es colisión: `upower` existe
|
label» (ERROR) y «el mismo label en dos colas distintas» (NO es colisión: `upower` existe
|
||||||
legítimamente en incoming-gnome y en incoming-kde, y son la misma unidad en dos imágenes).
|
legítimamente en incoming-gnome y en incoming-kde, y son la misma unidad en dos imágenes).
|
||||||
|
|
||||||
**(b) El emisor, y la trampa del sidecar rancio.** `seal_product_rootfs()` sirve a las dos vetas
|
**(b) El emisor — HECHO, y la trampa quedó armada en un test.** `service_cards()` lee la receta que
|
||||||
(`product` desde recetas locales, `product_from_repo` desde el repo firmado) y **sólo una tiene el
|
viaja DENTRO de cada artefacto sellado (el sidecar de provenance `.hammer/recipe.toml`, vía
|
||||||
directorio de recetas**. La fuente uniforme que ambas comparten es el **sidecar de provenance**
|
`Store::recipe_for_hash`) y filtra `scope = "system"`. Se lee de ahí y no de `recipes/` porque
|
||||||
`.hammer/recipe.toml`, que `takana-build` congela DENTRO del artefacto y `Store::recipe_for_hash`
|
`seal_product_rootfs()` sirve a las DOS vetas —`product` desde recetas locales y `product_from_repo`
|
||||||
sabe leer.
|
desde el repo firmado— y **sólo una tiene `recipes/` a mano**: el sidecar es la única fuente que
|
||||||
|
ambas comparten, que es lo que sostiene que las dos converjan al mismo `product-rootfs`.
|
||||||
|
|
||||||
Pero ahí está la trampa, y es de la familia de «el cache-hit congela regresiones»: el sidecar
|
La trampa era real y sigue ahí: **el sidecar no entra en el `ArtifactHash`**, así que un artefacto
|
||||||
**no entra en el `ArtifactHash`**, así que el openssh ya sellado en
|
sellado antes de que su receta declarara servicios es un cache-hit perfectamente válido y no se
|
||||||
`store/938835e6…-openssh/.hammer/recipe.toml` **no tiene** el bloque `[[service]]` y, como el hash
|
reconstruye nunca solo. Si el emisor devolviera lista vacía sin protestar, el producto saldría
|
||||||
no se movió, `store.has()` da verdadero y **nunca se va a reconstruir solo**. Derivar la seed del
|
**booteando, verde y sin sshd**. Por eso `service_cards()` **falla ruidosamente** con un mensaje que
|
||||||
sidecar hoy produciría un producto **sin sshd**, en silencio.
|
explica la causa y da el arreglo, y hay un test
|
||||||
|
(`service_cards_falla_ruidosamente_con_sidecar_rancio`) que exige que el mensaje siga diciendo las
|
||||||
|
dos cosas. Es la regla 3 del CLAUDE.md aplicada antes de pagarla: un ausente falla ruidosamente; un
|
||||||
|
vacío llega hasta el final diciendo que todo fue bien.
|
||||||
|
|
||||||
⇒ La migración (re-sellar openssh para que su sidecar lleve el bloque) es una unidad de trabajo
|
### 4b.1 La migración de openssh, medida — y un hecho nuevo
|
||||||
aparte y **con control**: hay que borrar el artefacto del store para forzar el rebuild, y openssh no
|
|
||||||
está certificado como reproducible (su propia receta dice «el criterio es construye y corre»), así
|
Re-sellar openssh era el paso que faltaba, y se hizo **con control** porque openssh no estaba
|
||||||
que el árbol nuevo hay que compararlo contra el viejo antes de darlo por bueno. Cambiar un artefacto
|
certificado como reproducible (su propia receta dice «el criterio es construye y corre»). El
|
||||||
que bootea en QEMU sin ese control es exactamente cómo se cuela una regresión que nadie ve.
|
procedimiento, por si hay que repetirlo con otro componente de servicio:
|
||||||
Hasta entonces `product_seed_card()` **sigue usando la constante**, a propósito y dicho acá.
|
|
||||||
|
1. **Manifiesto de control ANTES de tocar nada**: `sha256` + modo de los 33 ficheros.
|
||||||
|
2. **Respaldo por hardlink** — y acá saltó el gotcha conocido: `store/` es un bind-mount de
|
||||||
|
`/dev/sdb` y `work/` vive en `/dev/sdc`, así que `cp -al` a `work/` muere con `Invalid
|
||||||
|
cross-device link`. El respaldo tiene que ir **dentro del mount del store**; `store/.respaldo-…`
|
||||||
|
sirve y es invisible para `store-gc.sh`, que sólo considera dirs `^[0-9a-f]{64}-`.
|
||||||
|
3. Borrar el artefacto (los bytes sobreviven por el respaldo: 11 enlaces → 10) y reconstruir bajo
|
||||||
|
`flock -o`.
|
||||||
|
4. Comparar árbol nuevo contra el manifiesto.
|
||||||
|
|
||||||
|
**Resultado: openssh REPRODUCE BIT A BIT.** 33/33 ficheros, modos idénticos, y la única diferencia
|
||||||
|
en todo el árbol es `.hammer/recipe.toml` — exactamente el sidecar que se quería migrar y nada más.
|
||||||
|
Eso es un hecho nuevo sobre el corpus, no sólo un paso de esta campaña: openssh pasa de «no
|
||||||
|
certificado» a «reproduce», comprobado contra un control tomado antes del borrado.
|
||||||
|
|
||||||
|
Coste que conviene saber: los árboles ya hidratados compartían inodes con el artefacto viejo, y el
|
||||||
|
nuevo tiene inodes propios (`%h = 1`). El contenido es el mismo, pero hasta que esos árboles se
|
||||||
|
refresquen el disco lleva dos copias.
|
||||||
|
|
||||||
**(c) Los de GNOME: DECLARADOS, no todavía arrancados por arje.** Las nueve unidades que
|
**(c) Los de GNOME: DECLARADOS, no todavía arrancados por arje.** Las nueve unidades que
|
||||||
`gnome-start-qemu.sh` levanta con `&` están declaradas en sus recetas y habilitadas en el perfil
|
`gnome-start-qemu.sh` levanta con `&` están declaradas en sus recetas y habilitadas en el perfil
|
||||||
@@ -210,7 +231,8 @@ arriba en arje.
|
|||||||
| El vigía CORRE SOLO (cosecha-cron) → `docs/state/servicios.txt` | ✅ 0 errores, 18 avisos |
|
| El vigía CORRE SOLO (cosecha-cron) → `docs/state/servicios.txt` | ✅ 0 errores, 18 avisos |
|
||||||
| Los dos compat que no estaban en ningún perfil | ✅ corregido §4a |
|
| Los dos compat que no estaban en ningún perfil | ✅ corregido §4a |
|
||||||
| Los 9 de GNOME declarados y habilitados (hash sin mover, 9/9) | ✅ §4c |
|
| Los 9 de GNOME declarados y habilitados (hash sin mover, 9/9) | ✅ §4c |
|
||||||
| Emisor desde el sidecar + re-sellado de openssh | ◑ §4b (con control de reproducibilidad) |
|
| Emisor desde el sidecar (`service_cards`, falla ruidosa + test) | ✅ §4b |
|
||||||
|
| Re-sellado de openssh con control → **reproduce bit a bit** | ✅ §4b.1 |
|
||||||
| Que la imagen los arranque por arje y no por el script `&` | ◻ §4c, depende de §4b |
|
| Que la imagen los arranque por arje y no por el script `&` | ◻ §4c, depende de §4b |
|
||||||
| `escritorio-cosmic`: auditar su `cosmic-start.sh` y habilitar | ◻ hoy sale con 5 AVISO |
|
| `escritorio-cosmic`: auditar su `cosmic-start.sh` y habilitar | ◻ hoy sale con 5 AVISO |
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user