gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1181Shell 82Rust 62Markdown 521515 Sutichaykuna

atuq v0.3: página de inicio y pestaña nueva — y la firma de complementos venía COMPILADA

La página de inicio va en una extensión de sistema (`inicio@atuq.tawasuyu`) y no en una pref, porque
la pestaña NUEVA no se puede redirigir con prefs desde hace años: el único mecanismo soportado es
`chrome_url_overrides.newtab`. La misma pieza resuelve la home con `chrome_settings_overrides`.

La página: el medallón, el nombre y una barra que decide sola. Si lo tecleado PARECE una dirección,
navega; si no, BUSCA CON EL MOTOR POR DEFECTO DEL USUARIO vía `browser.search.query`. No se cablea
ningún buscador: una distro que hornea su motor en la página de inicio está tomando por el usuario
la única decisión que esa página debería respetar. Y dice cuál es el motor, para que la búsqueda no
sea una caja negra.

DOS MECANISMOS MUERTOS Y UNO VIVO, TODOS MEDIDOS:
- `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 decía una palabra —
  fallo perfectamente silencioso.
- `policies.json` con `ExtensionSettings` SÍ se lee (`browser.policies.applied=true` en el perfil) y
  falla con un error que al menos se nombra: `ERROR_SIGNEDSTATE_REQUIRED`.
- Y `xpinstall.signatures.required=false` TAMPOCO alcanza.

PARA SABER SI EL AUTOCONFIG SIQUIERA CORRÍA, HIZO FALTA UN TESTIGO. `defaultPref` no deja rastro en
`prefs.js` (sólo se guarda lo que difiere del default), así que un `.cfg` que no se ejecuta es
INDISTINGUIBLE de uno que sí y no hace nada. `atuq.cfg` ahora escribe `atuq.autoconfig.ok` como pref
de USUARIO, legible desde fuera sin abrir el navegador. Salió `true` ⇒ el .cfg corre, y la pref de
firma se estaba ignorando: la exigencia viene COMPILADA. El default de `MOZ_REQUIRE_SIGNING` sale
del MILESTONE (154.0 es release), no del canal — el nuestro es `default` y aun así estaba activa.

De ahí `export MOZ_REQUIRE_SIGNING=0` en firefox.toml, con su 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 SDD 26 §6.1 tampoco se podría shipear nunca.

La prueba se hizo SIN reconstruir: se montó el `.cfg` con el testigo por encima del artefacto con
`--ro-bind`, en vez de editar un hardlink del store (que habría corrompido el artefacto sellado).

1dc662c48d230982edbac7d364359c79f3781a4a · ruwaq Sergio · (1 killa ñawpaq) · tayta 114aeba51 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 3

recipes/atuq/extension/inicio.js

recipes/atuq/tools/rebrand.py

9 qillqakuna · +211 −2

diff --git a/recipes/atuq/README.md b/recipes/atuq/README.md
index 9721b6b9..ab76eea7 100644
--- a/recipes/atuq/README.md
+++ b/recipes/atuq/README.md
@@ -13,7 +13,18 @@ hashea por CONTENIDO. Editar un fichero de acá mueve el `ArtifactHash`; no hay
 | `prefs/autoconfig.js` | `defaults/pref/autoconfig.js` | Le dice a Gecko que lea `atuq.cfg`. Es el único gancho posible antes de que arranque el perfil. |
 | `atuq.cfg` | `<appdir>/atuq.cfg` | Prefs de fábrica + carga la hoja de estilo del chrome. |
 | `chrome/atuq.css` | `<appdir>/chrome/atuq.css` | El aspecto. Se inyecta como AGENT_SHEET desde `atuq.cfg`. |
-| `distribution/policies.json` | `<appdir>/distribution/policies.json` | Política de fábrica: telemetría, updates, scope de extensiones. |
+| `distribution/policies.json` | `<appdir>/distribution/policies.json` | Política de fábrica: telemetría, updates, **y la instalación de la extensión de inicio**. |
+| `extension/` | `<appdir>/distribution/extensions/inicio@atuq.tawasuyu.xpi` | Página de inicio y pestaña nueva. |
+
+## Por qué la extensión se instala por POLÍTICA y no por carpeta
+
+Dejar el `.xpi` en `distribution/extensions/` era el mecanismo clásico de las distros y **ya no
+funciona**: Firefox retiró el *sideloading* desde esa carpeta. Se comprobó midiendo, no leyendo — con
+el XPI puesto ahí, el `extensions.json` del perfil no lo mencionaba siquiera, y el log no decía nada:
+un fallo perfectamente silencioso.
+
+El mecanismo vigente es `ExtensionSettings` en `policies.json`, con `install_url` apuntando al XPI
+por ruta absoluta del FHS. Eso es estable acá porque el árbol se hidrata siempre en `/usr/lib/atuq`.
 
 ## Por qué el CSS no es `userChrome.css`
 
diff --git a/recipes/atuq/atuq.cfg b/recipes/atuq/atuq.cfg
index 31124a25..ea148586 100644
--- a/recipes/atuq/atuq.cfg
+++ b/recipes/atuq/atuq.cfg
@@ -31,6 +31,28 @@ defaultPref("toolkit.telemetry.archive.enabled", false);
 // `recipes/firefox.toml` y por eso no se puede resolver desde acá.
 defaultPref("extensions.autoDisableScopes", 0);
 
+// ── LA FIRMA DE COMPLEMENTOS ───────────────────────────────────────────────────────────────────
+// Sin esto la política de instalación muere con un error que no menciona la firma por su nombre:
+//     Policies: Download failed - ERROR_SIGNEDSTATE_REQUIRED - file:///…/inicio@atuq…xpi
+// Firefox exige que todo XPI esté firmado por Mozilla, y `--with-unsigned-addon-scopes=app,system`
+// —que sí está en el build— NO cubre el camino de instalación por política.
+//
+// Esta pref sólo tiene efecto en builds **unbranded**, y la nuestra lo es: `recipes/firefox.toml`
+// construye con `--with-branding=browser/branding/unofficial`. O sea que esto funciona por una
+// decisión que ya estaba tomada, no por una excepción nueva.
+//
+// ⚠ Lo que se pierde, dicho: atuq deja de exigir la firma de Mozilla para CUALQUIER complemento,
+// no sólo el suyo. Es la misma postura que toman las distros que empaquetan extensiones propias, y
+// la alternativa real —firmar con AMO— exige una cuenta y una revisión de Mozilla por cada versión,
+// que es exactamente la dependencia externa que esta distro existe para no tener.
+lockPref("xpinstall.signatures.required", false);
+
+// La página de inicio la pone la extensión `inicio@atuq.tawasuyu` con
+// `chrome_settings_overrides.homepage`; acá sólo se dice que al arrancar se abra la HOME y no la
+// sesión anterior. Poner la URL a mano en `browser.startup.homepage` pelearía con la extensión, y
+// gana la última que escribe: dos dueños para un valor es la forma de que el arranque sea sorpresa.
+defaultPref("browser.startup.page", 1);
+
 // ── EL CHROME ──────────────────────────────────────────────────────────────────────────────────
 // Por qué acá y no en `userChrome.css`: ese fichero vive en el PERFIL y exige que el usuario prenda
 // `toolkit.legacyUserProfileCustomizations.stylesheets`. atuq tiene que verse como atuq en el
@@ -43,6 +65,14 @@ defaultPref("extensions.autoDisableScopes", 0);
 // páginas, que no es lo que queremos tocar.
 try {
   const { classes: Cc, interfaces: Ci } = Components;
+  // ── TESTIGO: «¿corrió este fichero?» es la PRIMERA pregunta de cualquier diagnóstico ─────────
+  // `defaultPref` no deja rastro en `prefs.js` (sólo se guardan los valores que difieren del
+  // default), así que un autoconfig que no se ejecuta es INDISTINGUIBLE de uno que se ejecutó y no
+  // hizo nada. Esto escribe una pref de USUARIO, que sí queda en el perfil y se puede leer desde
+  // fuera sin abrir el navegador.
+  Cc["@mozilla.org/preferences-service;1"]
+    .getService(Ci.nsIPrefBranch)
+    .setBoolPref("atuq.autoconfig.ok", true);
   const sss = Cc["@mozilla.org/content/style-sheet-service;1"]
     .getService(Ci.nsIStyleSheetService);
   const ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
diff --git a/recipes/atuq/distribution/policies.json b/recipes/atuq/distribution/policies.json
index d929a12d..4fe60e28 100644
--- a/recipes/atuq/distribution/policies.json
+++ b/recipes/atuq/distribution/policies.json
@@ -6,6 +6,12 @@
     "DontCheckDefaultBrowser": true,
     "DisableProfileImport": false,
     "OverrideFirstRunPage": "",
-    "OverridePostUpdatePage": ""
+    "OverridePostUpdatePage": "",
+    "ExtensionSettings": {
+      "inicio@atuq.tawasuyu": {
+        "installation_mode": "normal_installed",
+        "install_url": "file:///usr/lib/atuq/distribution/extensions/inicio@atuq.tawasuyu.xpi"
+      }
+    }
   }
 }
diff --git a/recipes/atuq/extension/atuq128.png b/recipes/atuq/extension/atuq128.png
new file mode 100644
index 00000000..b058a9d7
Binary files /dev/null and b/recipes/atuq/extension/atuq128.png differ
diff --git a/recipes/atuq/extension/inicio.html b/recipes/atuq/extension/inicio.html
new file mode 100644
index 00000000..14413f63
--- /dev/null
+++ b/recipes/atuq/extension/inicio.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html lang="es">
+<head>
+<meta charset="utf-8">
+<title>atuq</title>
+<style>
+/* La paleta es la MISMA que `chrome/atuq.css`, muestreada del medallón. Está duplicada acá y no
+   importada porque esta página vive en la extensión y aquélla en el chrome: son dos mundos que no
+   comparten hoja de estilo. Si cambian los tokens, cambian en los dos sitios — está anotado en el
+   README para que no se desincronicen en silencio. */
+:root {
+  --naranja: #c15728;
+  --teal:    #1b6470;
+  --noche:   #04161d;
+  --crema:   #f5ecde;
+}
+* { box-sizing: border-box; }
+html, body { height: 100%; margin: 0; }
+body {
+  display: flex; flex-direction: column; align-items: center; justify-content: center;
+  gap: 1.6rem;
+  background: radial-gradient(120% 90% at 50% 0%, color-mix(in srgb, var(--teal) 45%, var(--noche)), var(--noche));
+  color: var(--crema);
+  font: 16px/1.5 system-ui, sans-serif;
+}
+img { width: 132px; height: 132px; }
+h1 { margin: 0; font-size: 2.6rem; font-weight: 600; letter-spacing: .02em; }
+form { width: min(560px, 82vw); display: flex; }
+input {
+  flex: 1; padding: .85rem 1.1rem; font: inherit;
+  color: var(--crema); background: color-mix(in srgb, var(--noche) 70%, transparent);
+  border: 1px solid color-mix(in srgb, var(--crema) 28%, transparent);
+  border-radius: 999px; outline: none;
+}
+input::placeholder { color: color-mix(in srgb, var(--crema) 55%, transparent); }
+input:focus { border-color: var(--naranja); box-shadow: 0 0 0 3px color-mix(in srgb, var(--naranja) 30%, transparent); }
+p { margin: 0; font-size: .82rem; color: color-mix(in srgb, var(--crema) 55%, transparent); }
+@media (prefers-reduced-motion: no-preference) { img { transition: transform .2s } img:hover { transform: scale(1.04) } }
+</style>
+</head>
+<body>
+  <img src="atuq128.png" alt="">
+  <h1>atuq</h1>
+  <form id="f" autocomplete="off">
+    <input id="q" type="text" placeholder="Buscá o escribí una dirección" autofocus>
+  </form>
+  <p id="motor"></p>
+  <script src="inicio.js"></script>
+</body>
+</html>
diff --git a/recipes/atuq/extension/inicio.js b/recipes/atuq/extension/inicio.js
new file mode 100644
index 00000000..a6696dd6
--- /dev/null
+++ b/recipes/atuq/extension/inicio.js
@@ -0,0 +1,33 @@
+// Qué hace la barra: si lo tecleado PARECE una dirección, navega; si no, busca.
+//
+// La búsqueda va por `browser.search.query`, que usa el motor por defecto DEL USUARIO. Es
+// deliberado no cablear ningún buscador acá: una distro que hornea su motor en la página de inicio
+// está tomando por el usuario la única decisión que esa página debería respetar.
+const f = document.getElementById("f");
+const q = document.getElementById("q");
+
+// «Parece una dirección» sin adivinar de más: un esquema explícito, algo con punto y sin espacios,
+// o localhost. Todo lo demás es una búsqueda. Un heurístico más listo se equivoca más.
+function esDireccion(t) {
+  if (/^[a-z][a-z0-9+.-]*:\/\//i.test(t)) return true;
+  if (/^(about|file|moz-extension):/i.test(t)) return true;
+  if (/^localhost(:\d+)?(\/|$)/i.test(t)) return true;
+  return /^[^\s/]+\.[^\s/]{2,}(\/|$|:)/.test(t);
+}
+
+f.addEventListener("submit", (e) => {
+  e.preventDefault();
+  const t = q.value.trim();
+  if (!t) return;
+  if (esDireccion(t)) {
+    location.href = /:\/\//.test(t) || /^(about|file|moz-extension):/i.test(t) ? t : "https://" + t;
+  } else {
+    browser.search.query({ text: t, disposition: "CURRENT_TAB" });
+  }
+});
+
+// Decir CUÁL es el motor por defecto, en vez de que la búsqueda sea una caja negra.
+browser.search.get().then((ms) => {
+  const d = ms.find((m) => m.isDefault);
+  if (d) document.getElementById("motor").textContent = "busca con " + d.name;
+}).catch(() => {});
diff --git a/recipes/atuq/extension/manifest.json b/recipes/atuq/extension/manifest.json
new file mode 100644
index 00000000..f50a9e51
--- /dev/null
+++ b/recipes/atuq/extension/manifest.json
@@ -0,0 +1,16 @@
+{
+  "manifest_version": 2,
+  "name": "atuq inicio",
+  "version": "1.0",
+  "description": "La página de inicio y la pestaña nueva de atuq.",
+  "browser_specific_settings": {
+    "gecko": {
+      "id": "inicio@atuq.tawasuyu",
+      "strict_min_version": "128.0"
+    }
+  },
+  "icons": { "128": "atuq128.png" },
+  "permissions": ["search"],
+  "chrome_url_overrides": { "newtab": "inicio.html" },
+  "chrome_settings_overrides": { "homepage": "inicio.html" }
+}
diff --git a/recipes/atuq/tools/rebrand.py b/recipes/atuq/tools/rebrand.py
index c2cd27db..61ead1f4 100644
--- a/recipes/atuq/tools/rebrand.py
+++ b/recipes/atuq/tools/rebrand.py
@@ -179,6 +179,50 @@ def escribir_iconos():
     return escritos
 
 
+# ── 4.bis La extensión de sistema: página de inicio y pestaña nueva ────────────────────────────
+# POR QUÉ UNA EXTENSIÓN Y NO UNA PREF. `browser.startup.homepage` acepta una URL, pero la pestaña
+# NUEVA no se puede redirigir con una pref desde hace años: el único mecanismo soportado es
+# `chrome_url_overrides.newtab`. Y una página servida por `file://` mostraría una ruta del disco en
+# la barra de direcciones, que es feo y además frágil (depende de dónde quedó hidratado el árbol).
+# La extensión resuelve las dos —`chrome_url_overrides` y `chrome_settings_overrides.homepage`— con
+# una sola pieza y sin URLs absolutas.
+#
+# Esto sólo funciona porque `recipes/firefox.toml` construye con
+# `--with-unsigned-addon-scopes=app,system` y `atuq.cfg` pone `extensions.autoDisableScopes = 0`.
+# Son tres piezas en tres ficheros distintos y **las tres tienen que estar**: sin la primera Gecko
+# rechaza el XPI sin firmar, sin la segunda lo instala DESACTIVADO.
+#
+# EL ZIP VA `STORED` Y CON FECHA FIJA, igual que el `omni.ja`: un XPI comprimido metería la versión
+# de zlib como variable del artefacto, y son cuatro ficheros de texto — no hay nada que ahorrar.
+EXT_ID = "inicio@atuq.tawasuyu"
+
+
+def empaquetar_extension():
+    origen = os.path.join(OVERLAY, "extension")
+    if not os.path.isdir(origen):
+        fatal(f"no está el árbol de la extensión en {origen}")
+    ficheros = sorted(os.listdir(origen))
+    if "manifest.json" not in ficheros:
+        fatal("la extensión no tiene manifest.json")
+    destino_dir = os.path.join(APPDIR, "distribution", "extensions")
+    os.makedirs(destino_dir, exist_ok=True)
+    destino = os.path.join(destino_dir, f"{EXT_ID}.xpi")
+    with zipfile.ZipFile(destino, "w") as z:
+        for nombre in ficheros:
+            ruta = os.path.join(origen, nombre)
+            if not os.path.isfile(ruta):
+                continue
+            with open(ruta, "rb") as fh:
+                datos = fh.read()
+            if not datos:
+                fatal(f"la extensión trae {nombre} VACÍO")
+            zi = zipfile.ZipInfo(nombre, date_time=(2010, 1, 1, 0, 0, 0))
+            zi.compress_type = zipfile.ZIP_STORED
+            zi.external_attr = 0o644 << 16
+            z.writestr(zi, datos)
+    return f"{EXT_ID}.xpi ({len(ficheros)} ficheros, {os.path.getsize(destino)} bytes)"
+
+
 # ── 5. Los binarios ────────────────────────────────────────────────────────────────────────────
 # Se renombran los dos y se deja un symlink `firefox` → `atuq` DENTRO del appdir. El symlink no es
 # indecisión: hay código y scripts de terceros que invocan el binario por su nombre histórico, y
@@ -246,6 +290,7 @@ def main():
     binarios = renombrar_binarios()
     # Después del renombrado: los ficheros ya se llaman atuq/atuq-bin.
     parches = [rebrandear_binario(MARCA), rebrandear_binario(f"{MARCA}-bin")]
+    ext = empaquetar_extension()
 
     print(f"rebrand: overlay {huella[:16]}… ⇒ BuildID {bid}")
     print(f"rebrand: application.ini {sorted(claves)}")
@@ -254,6 +299,7 @@ def main():
     print(f"rebrand: binarios {', '.join(binarios)} (+ symlink firefox → {MARCA})")
     for l in parches:
         print(f"rebrand: {l}")
+    print(f"rebrand: extensión de sistema {ext}")
 
 
 main()
diff --git a/recipes/firefox.toml b/recipes/firefox.toml
index 33ba9d16..21389610 100644
--- a/recipes/firefox.toml
+++ b/recipes/firefox.toml
@@ -159,6 +159,23 @@ export MOZ_NOSPAM=1
 # `x86_64-unknown-linux-gnu` y usarlo produciría un cross-compile silencioso).
 export RUST_TARGET=x86_64-alpine-linux-musl
 
+# ── `MOZ_REQUIRE_SIGNING=0`: SIN ESTO LA DISTRO NO PUEDE SHIPEAR NI UNA EXTENSIÓN PROPIA ──────
+# Firefox exige que todo XPI esté firmado por Mozilla. La pref `xpinstall.signatures.required` NO
+# alcanza: se ignora cuando la exigencia viene COMPILADA, y viene compilada porque el default de
+# `MOZ_REQUIRE_SIGNING` sale del MILESTONE (154.0 es release), no del canal de actualización — el
+# nuestro es `default`, y aun así estaba activa.
+#
+# Medido, no deducido: con el autoconfig ejecutándose de verdad (testigo `atuq.autoconfig.ok=true`
+# en el perfil) y `lockPref("xpinstall.signatures.required", false)` puesta, la instalación por
+# política seguía muriendo con `ERROR_SIGNEDSTATE_REQUIRED`.
+#
+# ⚠ EL PRECIO, ESCRITO: este Firefox —y por herencia `atuq`— deja de exigir la firma de Mozilla
+# para CUALQUIER complemento, no sólo los nuestros. La alternativa real es firmar en AMO, que pide
+# cuenta y revisión de Mozilla por cada versión: exactamente la dependencia externa que esta distro
+# existe para no tener. Es la misma postura que ya tomaba `--with-unsigned-addon-scopes`, llevada
+# hasta donde de verdad hace falta.
+export MOZ_REQUIRE_SIGNING=0
+
 # ── LOS PARCHES ROMPEN EL CHECKSUM DE LOS CRATES VENDORIZADOS ─────────────────────────────────
 # `error: the listed checksum of third_party/rust/zeitstempel/src/unix.rs has changed`. Los crates
 # de Rust vienen VENDORIZADOS en el tarball y cargo verifica el sha de CADA fichero contra