SDD 26: restauradas §2.ter–§2.sexies, que mi commit anterior borró sin querer

Al reescribir el §6.8 corté el texto entre «lo que NO está probado» y `## 7`, y
entre medio no había sólo eso: vivían ahí §2.ter (el modo `[source] dir`),
§2.quater (los tres fallos que destapó abrirlo en pantalla), §2.quinquies (los
tres mecanismos de la v0.3) y §2.sexies (lo que quedó probado y con qué). Ciento
veinte líneas de método pagado con horas.

Recuperadas de HEAD~1 y comprobadas byte a byte con `diff` contra el original, no
a ojo.

La causa, que es la de siempre en este repo: usé un ANCLA («hasta el próximo
`## 7`») dando por hecho que el documento estaba en el orden en que yo lo
imaginaba. El §6.8 lo había insertado yo mismo antes del §2.ter hace unas horas,
así que el ancla saltaba por encima de cuatro secciones. Un ancla es una
etiqueta, no una propiedad del documento — la misma familia que la cabecera de
hunk que nombra la función anterior.

Lo que lo hizo visible en el mismo minuto fue mirar el `git show --stat`: 210
líneas cambiadas en un fichero donde yo había escrito 45. La regla 2 del
CLAUDE.md manda mirarlo para el ALCANCE del commit, pero sirve igual para el
tamaño del cambio.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
This commit is contained in:
Sergio
2026-09-06 23:48:19 +00:00
co-authored by Claude Opus 5
parent 2e093f9cab
commit 26a0dd31c7
+120
View File
@@ -449,6 +449,126 @@ directas, nadie llamando al proxy—. La única diferencia entre las dos corrida
configuración y el observable se da vuelta entero. Sin ese modo, una prueba que se hubiera vuelto
ciega se vería idéntica a una que funciona.
### 2.ter Dónde vive la fuente de `atuq` — hizo falta un modo de `[source]` nuevo
Escribir la receta destapó un muro que este documento no había visto: `[source]` era
**obligatoriamente** git o tarball, así que una receta cuyo contenido no viene de upstream sino de
nosotros no se podía ni expresar. Los rodeos eran todos peores — fetchear una fuente que después se
ignora **miente** sobre la identidad del artefacto, y colgar el overlay de un repo aparte obliga al
worker a tener acceso a un repo privado.
Se agregó **`source.dir`** (commit `54fa4a9`): un árbol dentro del propio repo, hasheado por
**contenido** con `ArtifactHash::of_tree` —que ya existía para verificar bit-reproducibilidad en
Stage 2— y no por ruta. Es la misma disciplina que ya tenían los `patches`, que entran al hash por
bytes. Editar un CSS del overlay mueve el `ArtifactHash`; renombrar el directorio, no.
Un `.swm` **no** puede llevar una receta derivada, y los cuatro sitios que lo tocan fallan
diciéndolo: el manifiesto es compartible y necesita un puntero resoluble desde fuera.
**Hecho el 2026-09-05** (commits `aeebb19` y `6f6c32b`): `recipes/atuq.toml` + `recipes/atuq/`.
La **v0.2** (`b3:7eff4ba4`) ya trae el branding: `application.ini` (Vendor/Name/RemotingName/
CodeName, con el `ID` intacto para no salirse del ecosistema de complementos), `brand.ftl` dentro de
`browser/omni.ja`, los binarios renombrados, iconos propios y `.desktop`. El re-empaque preserva
orden, `STORED` y fechas del original, y se verificó contra el artefacto: **5306 entradas, mismo
orden, y una sola con contenido distinto**. El `BuildID` se deriva del contenido del overlay —no de
la fecha— porque es la clave con la que Gecko invalida su startup cache: sin eso, un cambio de
chrome arranca con la interfaz vieja y *parece* que el overlay no agarró.
La capa de configuración de la v0.1 son cuatro ficheros —autoconfig, `atuq.cfg`, `chrome/atuq.css`,
`policies.json`— y el CSS se registra como `USER_SHEET` desde autoconfig porque `userChrome.css`
vive en el perfil y exigiría que el usuario prenda una pref: atuq tiene que verse como atuq en el
primer arranque. Falta construirlo: su dep `firefox` está en vuelo.
### 2.quater Abrirlo en una pantalla: tres fallos que la clausura no podía ver
El 2026-09-05, con `atuq` sellado y verificado, se hidrató y se abrió en el compositor de la sesión
(waypipe). **No arrancó**, y los tres motivos son la diferencia entre «sellado» y «usable»:
1. **EXDEV al hidratar.** `hydrate` proyecta con hardlinks y `linkat()` rechaza cruzar un punto de
montaje aunque los dos lados sean el mismo filesystem. Acá el store es `/dev/sdb` bind-monteado
y `work/` vive en `/dev/sdc`. El rootfs va bajo `/mnt/cosecha`, donde el volumen está montado
entero. Se comprueba con `findmnt -T`, **nunca** con `stat -c %d`.
2. **El lanzador no puede ser un symlink.** Ni el binario ni sus `.so` traen `RPATH`/`RUNPATH`
(`readelf -d`), así que el motor no encontraba su propio `libnspr4.so` y moría en
`XPCOMGlueLoad`. Es un script con `LD_LIBRARY_PATH` + `exec`, como Debian y Fedora. La
alternativa limpia —`RPATH=$ORIGIN` con `patchelf`, que es lo que hace Alpine— espera a que
`patchelf` exista como receta del corpus.
3. **Un bug del corpus, no de atuq: `atk` se había tragado GObject.** Declaraba la variante
ESTÁTICA de glib y produce un objeto compartido, así que `libatk-1.0.so` llevaba una copia
entera del sistema de tipos. Dos GObject en un proceso ⇒ 45 `GLib-GObject-CRITICAL` y
`Segmentation fault`. **El síntoma no nombra a atk**, y no se ve mirando el rootfs: había una
sola `libgobject`. Se cazó preguntando quién DEFINE el símbolo:
`nm -D --defined-only <cada .so> | grep " T g_type_register_static"` — deben salir uno, salieron
dos.
Radio medido antes de tocar (`yupana radio atk`): 4 transitivos, 3 sellados a deuda; GNOME fuera
porque usa gtk4. La cadena `atk → gtk3 → firefox → atuq` volvió a sellar 4/4, y **firefox tardó
~50 minutos, no las cuatro horas** que repetía este documento — el número venía del folclore de la
receta, no de una medición.
**La lección para el plan: ninguna unidad de este frente está cerrada hasta que algo se ABRE en una
pantalla.** La clausura decía 100% con un navegador que no llegaba a pintar un píxel.
### 2.quinquies La v0.3 y los tres mecanismos: dos muertos, uno vivo
La página de inicio y la pestaña nueva parecían un `pref` y resultaron ser un frente. Lo que se
midió, en orden:
| Mecanismo | Resultado |
|---|---|
| `distribution/extensions/` — el clásico de las distros | **No instala nada.** Firefox retiró el *sideloading*. El `extensions.json` del perfil ni lo mencionaba y el log no dijo una palabra: fallo perfectamente silencioso |
| `policies.json``ExtensionSettings` con `install_url` | **Se lee** (`browser.policies.applied=true` en el perfil) y falla nombrándose: `ERROR_SIGNEDSTATE_REQUIRED` |
| `xpinstall.signatures.required = false` | **No alcanza** — la exigencia viene COMPILADA |
**El instrumento que destrabó el diagnóstico fue un testigo.** `defaultPref` no deja rastro en
`prefs.js` —sólo se guarda lo que difiere del default— así que un autoconfig que **no se ejecuta**
es indistinguible de uno que sí y no hace nada. `atuq.cfg` escribe ahora `atuq.autoconfig.ok` como
pref de **usuario**, legible desde fuera sin abrir el navegador. Salió `true` ⇒ el `.cfg` corría, y
por lo tanto la pref de firma se estaba ignorando.
De ahí `MOZ_REQUIRE_SIGNING` vacío en `recipes/firefox.toml`. Dos detalles que costaron cada uno su
vuelta:
1. El default sale del **milestone** (`milestone.is_release_or_beta`), no del canal de actualización.
El nuestro es `default` y la exigencia estaba activa igual.
2. **Se desactiva con el valor VACÍO, no con cero.** `=0` muere con «takes 0 values»: es booleana y
el cero es un valor que no acepta. Leído del parser de mozbuild, no deducido.
**El precio, escrito:** este Firefox deja de exigir la firma de Mozilla para cualquier complemento.
La alternativa es firmar en AMO —cuenta y revisión de Mozilla por versión—, que es la dependencia
externa que esta distro existe para no tener. Y no es un desvío: **sin esto, el `sct` del §6.1
tampoco se podría shipear nunca**, así que la unidad 6 dependía de ésta sin que el plan lo dijera.
**Método que conviene repetir:** la prueba del testigo se hizo **sin reconstruir**, montando el
`.cfg` modificado con `--ro-bind` por encima del artefacto. Editar el fichero en el rootfs habría
escrito sobre un **hardlink del store** y corrompido el artefacto sellado.
### 2.sexies Lo que quedó PROBADO, y con qué
Tres afirmaciones que este documento venía haciendo sin medir, y cómo se cerraron:
| Afirmación | Prueba |
|---|---|
| El re-empaque del `omni.ja` es determinista | `why-differs` entre dos builds de `atuq`: **85 entradas idénticas, 0 divergen** |
| La capa de configuración se aplica | testigo `atuq.autoconfig.ok` escrito como pref de **usuario** en el perfil |
| El branding se ve | captura de la pantalla real con `grim` desde dentro de la jaula, y la página de inicio renderizada por el propio navegador con `--headless --screenshot` |
| La base también reproduce | `why-differs` entre dos builds de `firefox` en el worker: **56 entradas idénticas, 0 divergen** |
Y el contraste que salió de la primera: **el derivado reproducía y la base no.** El `BuildID` de
`firefox` era la hora del build, así que dos construcciones con el mismo `ArtifactHash` daban bytes
distintos — y `build-state.json` no lo podía ver, porque el hash es input-addressed y no se mueve
por esto. Verde y mintiendo. Cerrado con `MOZ_BUILD_DATE` desde `SOURCE_DATE_EPOCH`, que es el
mecanismo de hermeticidad que el sandbox ya tenía — y **comprobado como se comprueba esto**:
apartando el artefacto como `.ref`, construyendo de nuevo y pasándole `why-differs`. Reproduce.
**Dos guardianes nuevos en la fase `install` de `firefox`**, hermanos entre sí, porque los dos fallos
son de la misma familia —una bandera que el `configure` acepta y el artefacto ignora, muda y a 50
minutos de distancia—: uno comprueba `MOZ_REQUIRE_SIGNING: false` dentro del `omni.ja`, el otro que
el `BuildID` sea el que `SOURCE_DATE_EPOCH` obliga.
**La regla que sale de todo el día:** cuando la duda es «¿llegó al artefacto?», la respuesta no está
en el log del build ni en la receta — está **dentro del artefacto**, y hay que ir a buscarla ahí.
## 7. La costura: un host de native messaging en Rust
Todo lo del §6 que no es CSS pasa por **un solo mecanismo**: un proceso Rust que habla native