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 1471Shell 173Rust 105Markdown 943545 Definitions

paso 5: la hora la da ntpd-rs, en Rust — y el git del corpus SÍ traía los helpers

`chrony` era la hoja C más barata de cambiar: 0 consumidores, declarada sólo en `servidor`, así que
tocarla no invalida a nadie. Lo que se gana: el demonio que ajusta el reloj del sistema COMO ROOT
deja de ser C.

Sellado y verificado: b3:44e1e007… (el hash del dry-run, idéntico al del sellado), 5,1 M, ntp-daemon
y ntp-ctl ESTÁTICOS con 0 NEEDED, los dos contestan --version, y `ntp-ctl validate` sobre el ejemplo
que viaja dice "Config looks good". Se construyen dos de los tres binarios: el demonio y ntp-ctl, que
es el equivalente de chronyc — sin él la caja no se puede inspeccionar. Como `cargo rustc --` exige
UN target y ntpd produce tres, la fase compile itera (calcada de recipes/cargo-edit.toml, que existe
por lo mismo): +crt-static no puede ir en RUSTFLAGS porque alcanza a los proc-macros.

⚠ Lo que NO se gana, medido antes de prometerlo: NTS sigue apagado. chrony lo apaga porque necesita
nettle o gnutls, ausentes del corpus; acá el muro es el mismo un piso arriba — el TLS de NTS lo hace
rustls y sus dos proveedores (aws-lc-rs, ring) son C. El build va con --no-default-features
--features rustcrypto: la criptografía del PROTOCOLO sí es pura-Rust, el NTS-KE queda sin TLS.

El label pasa de `chronyd` a `ntpd-rs`: un label nombra el SERVICIO, no la función, y dejar `chronyd`
apuntando a otro demonio haría que `arjectl status chronyd` mostrara algo que no es chrony. chrony se
queda en el catálogo, que no es la imagen.

Y el bloqueante que apareció en el camino: el fetch murió con `git: 'remote-https' is not a git
command`, y la causa NO es la que CLAUDE.md §2 tenía escrita. El git del corpus SÍ trae los helpers
(2 en su artefacto, medido); lo que pasa es que los busca en un directorio HORNEADO al build
(`git --exec-path` ⇒ /usr/libexec/git-core, la ruta FHS), que en la jaula tiene 0. Faltaba
GIT_EXEC_PATH. Arreglado en el generador (jaula-herramientas.sh) y en el envoltorio vivo; con eso el
mismo binario clona por https y empuja al espejo, así que el push doble desde la jaula deja de estar
roto. CLAUDE.md §2 corregido dejando el síntoma y tachando el diagnóstico.

ed4518ac9c3d46b5cb8d94ca5d9013cccd8b4045 · by Sergio · (1 month ago) · parent e30ffbbf0 · browse the tree at this commit

Semantic delta

No definition changed: this change is text, documentation or data.

6 files · +239 −11

diff --git a/CLAUDE.md b/CLAUDE.md
index ecc62a11..abdbaab2 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -103,10 +103,19 @@ Sólo rutas explícitas. Un `add -A` arrastra al commit los ficheros a medias de
 granulares, en español, directo sobre `main`, y `git push` tras cada unidad de trabajo (el `origin`
 empuja a gitea **y** al espejo privado de GitHub; ver `scripts/espejo-setup.sh`).
 
-⚠ **Desde la jaula qorpa ese `push` doble SÓLO llega a gitea: el espejo de GitHub falla siempre**
-(medido 2026-09-18). El `PATH` del lab pone primero el git del CORPUS —`/work/sergio/bin/git`,
-2.54.0, musl— y ese build **no trae los helpers `remote-http`/`remote-https`**. El push termina así,
-con gitea ya actualizado y GitHub intacto:
+✅ **ARREGLADO el 2026-09-22 — y la causa NO era la que decía acá.** Lo de abajo se deja porque el
+síntoma es el mismo y alguien lo va a volver a buscar, pero el diagnóstico estaba mal: el git del
+corpus **SÍ trae** `git-remote-https` (está en su artefacto, medido: `ls …-git/usr/libexec/git-core |
+grep -c remote-http` ⇒ **2**). Lo que pasaba es que git busca sus treinta ayudantes en un directorio
+**horneado al build** —`git --exec-path` ⇒ `/usr/libexec/git-core`, la ruta FHS—, y corrido desde el
+store mira el libexec de la IMAGEN de la jaula, que tiene **0**. Faltaba `GIT_EXEC_PATH`, nada más.
+Puesto en el generador (`scripts/servidor/jaula-herramientas.sh`) y en el envoltorio
+`/work/sergio/bin/git`; con eso el mismo binario clona por https y empuja al espejo. Si el síntoma
+vuelve, mirá `git --exec-path` ANTES de creer que faltan los helpers.
+
+⚠ **El síntoma histórico, para que se reconozca** (medido 2026-09-18): el `PATH` del lab pone primero
+el git del CORPUS —`/work/sergio/bin/git`, 2.54.0, musl— y su envoltorio no le decía dónde están sus
+ayudantes. El push terminaba así, con gitea ya actualizado y GitHub intacto:
 
 ```
 To ssh://git.gioser.net:2345/sergio/takana.git
diff --git a/docs/plan-que-sigue-tras-busybox.md b/docs/plan-que-sigue-tras-busybox.md
index acdbc0db..336810e5 100644
--- a/docs/plan-que-sigue-tras-busybox.md
+++ b/docs/plan-que-sigue-tras-busybox.md
@@ -682,3 +682,75 @@ Es la misma sonda que `hydrate-profile.py` ya tenía; `install-image.sh` sólo t
   propia decisión.
 - Para cualquier objetivo **UEFI la cadena ya es soberana y no lleva gestor**: el bzImage *es* el
   binario EFI (ADR 0018), y ahora además se puede armar acá.
+
+---
+
+# Paso 5 — HECHO: la hora la da `ntpd-rs`, en Rust (2026-09-22)
+
+`chrony` era la hoja C **más barata de cambiar** del corpus: 0 consumidores, declarada sólo en
+`servidor`. Tocarla no invalida a nadie, y lo que se gana es concreto: el demonio que **ajusta el
+reloj del sistema como root** deja de ser C.
+
+## Sellado y verificado
+
+```
+recipes/ntpd-rs.toml  v1.9.0 · commit 46ec9bb4d5b6cb24f814f5543d85b9138afb4cba (tag v1.9.0)
+b3:44e1e0079eb7d33ee37261db9cdc3e3b5432c7d08ca8ee0617a59b19c6eb4a84   ⇒ el hash del dry-run,
+                                                                        idéntico al del sellado
+artefacto: 5,1 M · ntp-daemon y ntp-ctl ESTÁTICOS, 0 NEEDED · los dos contestan `--version`
+ntp-ctl validate contra el ejemplo que viaja  ⇒  "Config looks good"
+```
+
+Se construyen **dos** de los tres binarios: el demonio y `ntp-ctl`, que es el equivalente de
+`chronyc` — sin él la caja no se puede inspeccionar, y eso sería una regresión frente a chrony. Como
+`cargo rustc --` exige UN target y `ntpd` produce tres, la fase `compile` itera, calcada de
+`recipes/cargo-edit.toml` (que existe por lo mismo): `+crt-static` no puede ir en `RUSTFLAGS` porque
+alcanza a los proc-macros y rustc aborta con «cannot produce proc-macro».
+
+## Lo que NO se gana, medido antes de prometerlo: NTS sigue apagado
+
+`recipes/chrony.toml` apaga NTS porque necesita nettle o gnutls y **ninguna está en el corpus**. Acá
+el muro es el mismo un piso más arriba: el TLS de NTS lo hace `rustls`, y rustls necesita un
+*proveedor* de criptografía compilado — `aws-lc-rs` o `ring` — **y los dos son C** (`aws-lc-sys`
+arrastra cmake + asm; leído en el `Cargo.lock` del tag). Por eso el build va con
+`--no-default-features --features rustcrypto`: la criptografía del **protocolo** (AES-SIV, MD5) sí
+queda pura-Rust —`ntp-proto` la declara proveedor de primera clase y aborta con `compile_error!` si no
+hay ninguno— pero el NTS-KE queda sin TLS. ⇒ **NTP clásico, igual que chrony hoy.** Subir NTS pide un
+proveedor de rustls, no esta receta.
+
+## El label cambia con el paquete
+
+`servicios` pasa de `"chronyd"` a `"ntpd-rs"`. Un label es el nombre del SERVICIO, no de la función:
+dejar `chronyd` apuntando a ntpd-rs haría que `arjectl status chronyd` mostrara algo que no es chrony.
+La card guarda el mismo contrato que la de chrony —sale **78** nombrando `/etc/ntpd-rs/ntp.toml`, con
+el ejemplo en `/usr/share/doc/ntpd-rs/ntp.toml.default`— porque a qué servidores NTP se pregunta es
+decisión del sitio. `chrony` se queda en el CATÁLOGO, que no es la imagen: su receta documenta media
+docena de lecciones sobre un `configure` que prueba el entorno, y borrarla sería tirar eso.
+
+## Y el bloqueante que apareció: el `git` del corpus SÍ trae los helpers
+
+El primer intento de build murió en el `fetch`:
+
+```
+Cloning into bare repository '/work/repos/ntpd-rs.git'...
+git: 'remote-https' is not a git command.
+```
+
+**Y la causa no es la que este repo tenía escrita.** `CLAUDE.md` §2 dice que el git del corpus «no trae
+los helpers `remote-http`/`remote-https`». **Los trae** — están en su artefacto, medidos:
+
+```
+ls …-git/usr/libexec/git-core | grep -c remote-http   ⇒ 2
+git --exec-path                                       ⇒ /usr/libexec/git-core   (ruta COMPILADA)
+ls /usr/libexec/git-core | grep -c remote-http        ⇒ 0   (el libexec de la IMAGEN, vacío)
+```
+
+git es un multiplexor que busca sus treinta ayudantes en un directorio **horneado al build**: la ruta
+FHS. Corrido desde el store, mira el `/usr/libexec/git-core` de la jaula, que no tiene ninguno. Lo
+único que faltaba era `GIT_EXEC_PATH`. Con eso, el MISMO binario clona por https **y empuja al espejo
+de GitHub** — comprobado con `ls-remote` y con `push --dry-run`, que llegó hasta el rechazo por
+non-fast-forward, o sea que habló TLS y autenticó.
+
+Arreglado donde se genera (`scripts/servidor/jaula-herramientas.sh`: `git` sale del bucle y lleva su
+`export GIT_EXEC_PATH="$b/usr/libexec/git-core"`) y en el envoltorio vivo `/work/sergio/bin/git`.
+⇒ **El push doble desde la jaula deja de estar roto**, que era una molestia diaria documentada.
diff --git a/docs/state/busybox-applets.tsv b/docs/state/busybox-applets.tsv
index 2378d754..6b983f5d 100644
--- a/docs/state/busybox-applets.tsv
+++ b/docs/state/busybox-applets.tsv
@@ -225,7 +225,7 @@ nologin	shadow	sellada y EN IMAGEN	sin-verificar	base,cli,escritorio-cosmic,escr
 nproc	uutils	sellada y EN IMAGEN	sin-verificar	construida y no viaja en ninguna imagen	NPROC	sigue		coreutils,uutils	2026-09-22
 nsenter	util-linux	sellada y EN IMAGEN	sin-verificar	base,cli,escritorio-cosmic,escritorio-gnome,escritorio-kde,escritorio-sway,servidor	NSENTER	sigue		util-linux	2026-09-22
 nslookup	dns-TRAER	sin receta	n/a	TRAER — no hay receta bind-tools/dog/doggo	NSLOOKUP	sigue		SOLO-BUSYBOX	2026-09-22
-ntpd	chrony	sellada y EN IMAGEN	NO-drop-in	chrony es otro daemon y otro config	NTPD	sigue		SOLO-BUSYBOX	2026-09-22
+ntpd	ntpd-rs	sellada y EN IMAGEN	NO-drop-in	el dueño es `ntpd-rs` desde el 2026-09-22 (antes `chrony`, que salió de los perfiles). Ninguno de los dos provee un binario llamado `ntpd`: el demonio se llama `ntp-daemon` ⇒ NO es drop-in del applet, es migración de llamadores.	NTPD	sigue		SOLO-BUSYBOX	2026-09-22
 od	uutils	sellada y EN IMAGEN	sin-verificar	construida y no viaja en ninguna imagen	OD	sigue		coreutils,uutils	2026-09-22
 openvt	kbd	sellada y EN IMAGEN	sin-verificar	base,cli,escritorio-cosmic,escritorio-gnome,escritorio-kde,escritorio-sway,servidor	OPENVT	sigue		kbd	2026-09-22
 partprobe	parted	sellada y EN IMAGEN	sin-verificar	base,cli,escritorio-cosmic,escritorio-gnome,escritorio-kde,escritorio-sway,servidor	PARTPROBE	sigue		parted	2026-09-22
diff --git a/docs/state/targets.toml b/docs/state/targets.toml
index ce8f35d2..0197ae5e 100644
--- a/docs/state/targets.toml
+++ b/docs/state/targets.toml
@@ -1870,9 +1870,26 @@ paquetes = [
   # de «que el servidor sirva sus paquetes a su propio host»: el host necesita `takana` instalado para
   # consumirlos, y no puede instalarlo desde el repo porque no está en el repo.
   "takana",
-  # `chrony`: la hora. Sin NTP, un reloj que deriva rompe la validación de certificados TLS y la de
-  # firmas con expiración — y falla con errores que no dicen «la hora», dicen «firma inválida».
-  "chrony",
+  # ── LA HORA: `ntpd-rs`, y `chrony` SALE (2026-09-22) ─────────────────────────────────────────
+  # Sin NTP, un reloj que deriva rompe la validación de certificados TLS y la de firmas con
+  # expiración — y falla con errores que no dicen «la hora», dicen «firma inválida». Eso no cambia;
+  # lo que cambia es quién lo hace.
+  #
+  # `chrony` era la hoja C más barata de cambiar del corpus: 0 consumidores y declarada sólo acá, así
+  # que tocarla no invalida a NADIE. Y lo que se gana es concreto: el demonio que ajusta el reloj del
+  # sistema **como root** deja de ser C. `ntpd-rs` (Trifecta Tech / Tweede Golf) está en Debian y
+  # Fedora y se escribió para este nicho exacto.
+  #
+  # ⚠ **Lo que NO se gana, dicho para que nadie lo lea al revés: NTS sigue apagado.** `chrony` lo
+  # apagaba porque NTS necesita nettle o gnutls y ninguna está en el corpus; acá el muro es el mismo
+  # un piso más arriba — el TLS de NTS lo hace `rustls`, que necesita un proveedor de criptografía
+  # compilado (`aws-lc-rs` o `ring`) **y los dos son C**. El build va con
+  # `--no-default-features --features rustcrypto`: la criptografía del PROTOCOLO (AES-SIV, MD5) sí es
+  # pura-Rust, el NTS-KE queda sin proveedor. O sea: NTP clásico, igual que antes.
+  #
+  # `chrony` se queda en el CATÁLOGO, que no es la imagen — su receta documenta media docena de
+  # lecciones sobre el `configure` que prueba el entorno, y borrarla sería tirar eso.
+  "ntpd-rs",
   # `nftables`: el cortafuegos. `dev.gioser.net` está hoy expuesto a internet con fuerza bruta a root
   # en el journal; una caja nueva no debería nacer así.
   "nftables",
@@ -1907,7 +1924,10 @@ paquetes = [
 # `gitea` en `/etc/passwd`— y sale con 78 si falta alguna. Es a propósito: las dos vienen con los
 # datos de la mudanza, y un gitea que arranca sin su config ofrece el asistente de «crear
 # administrador» a quien pase. Hasta que la mudanza traiga ambas, este servicio NO levanta y lo dice.
-# ⚠ `crond` y `chronyd` (2026-09-15) son las dos CAPACIDADES que un servidor necesita y que ninguna
+# ⚠ `chronyd` → `ntpd-rs` (2026-09-22): la hora la da ahora el demonio en Rust. El label cambia con
+# el paquete porque un label es el nombre del SERVICIO, no de la función: dejar `chronyd` apuntando a
+# ntpd-rs haría que `arjectl status chronyd` mostrara algo que no es chrony.
+# ⚠ `crond` y `ntpd-rs` (2026-09-15 / 2026-09-22) son las dos CAPACIDADES que un servidor necesita y que ninguna
 # métrica reclama: el latido y la hora. Sus paquetes ya estaban declarados arriba y sellados —lo que
 # faltaba era el eslabón que los ARRANCA, que es este. Una caja con `cronie` instalado y sin `crond`
 # corriendo se ve idéntica a una con latido, y no rota un log ni respalda nada.
@@ -1927,7 +1947,7 @@ paquetes = [
 #     glibc: pide jaula qorpa.
 # Sus Cards están instaladas en `cards.d` (se pueden encarnar a pedido) y sus guardas salen 78
 # diciendo qué falta. La distinción `cards.d` vs `genesis` es exactamente ésta.
-servicios = ["sshd", "gitea", "caddy", "minga", "squid", "crond", "chronyd", "shuma-daemon", "shuma-gateway",
+servicios = ["sshd", "gitea", "caddy", "minga", "squid", "crond", "ntpd-rs", "shuma-daemon", "shuma-gateway",
              "matilda", "tupu", "pacha", "pacha-secretos", "willay-daemon"]
 
 # ════════════════════════════════════════════════════════════════════════════════════════════════
diff --git a/recipes/ntpd-rs.toml b/recipes/ntpd-rs.toml
new file mode 100644
index 00000000..a81a8e7f
--- /dev/null
+++ b/recipes/ntpd-rs.toml
@@ -0,0 +1,106 @@
+# ntpd-rs 1.9.0 — el reloj, en Rust. Reemplaza a `chrony` como raíz del perfil `servidor`.
+#
+# ── POR QUÉ SE CAMBIA, Y LO QUE NO SE GANA ──────────────────────────────────────────────────────
+# `chrony` es una hoja: 0 consumidores, declarada sólo en `servidor`. Eso la hace la pieza C vital
+# más baratа de cambiar del corpus — tocarla no invalida a nadie. Lo que se gana es que el demonio
+# que **ajusta el reloj del sistema como root** deje de ser C: ntpd-rs es de Trifecta Tech /
+# Tweede Golf, está en Debian y Fedora, y se escribió para este nicho exacto.
+#
+# ⚠ **Lo que NO se gana hoy, dicho antes de que alguien lo lea al revés: NTS sigue apagado.**
+# `recipes/chrony.toml` lo apaga porque NTS necesita nettle o gnutls y ninguna está en el corpus. Acá
+# el muro es el mismo un piso más arriba: el TLS de NTS lo hace `rustls`, y rustls necesita un
+# *proveedor* de criptografía compilado —`aws-lc-rs` o `ring`—, **y los dos son C** (`aws-lc-sys`
+# arrastra cmake + asm; medido en el `Cargo.lock` del tag: `aws-lc-sys` ← `aws-lc-rs` ← `rustls`).
+# Por eso este build va con `--no-default-features --features rustcrypto`: la criptografía del
+# protocolo NTP (AES-SIV, MD5) sí queda pura-Rust —`ntp-proto` la declara como proveedor de primera
+# clase y aborta con `compile_error!` si no hay ninguno— pero el NTS-KE queda sin proveedor de TLS.
+# ⇒ **NTP clásico, igual que chrony hoy.** Subir NTS es su propia unidad de trabajo y lo que pide es
+# un proveedor de rustls, no esta receta.
+#
+# ── EL BUILD: DOS BINARIOS, Y POR ESO LA FASE `compile` ES PROPIA ───────────────────────────────
+# El camino por defecto del lab es `cargo rustc -- <flags>`, que exige UN solo target; `ntpd` produce
+# tres (`ntp-daemon`, `ntp-ctl`, `ntp-metrics-exporter`). Se itera, igual que `recipes/cargo-edit.toml`
+# —de donde sale este bloque verbatim— y por la misma razón: `+crt-static` NO puede ir en RUSTFLAGS
+# porque alcanza a los proc-macros (`serde_derive`, `tokio-macros`, `tracing-attributes`) y rustc
+# aborta con «cannot produce proc-macro». Tras `cargo rustc --` sólo toca el binario final.
+#
+# Se construyen DOS de los tres: el demonio y `ntp-ctl`, que es el equivalente de `chronyc` —sin él la
+# caja no se puede inspeccionar y eso sería una regresión frente a chrony—. El exportador de
+# Prometheus queda fuera: nadie lo pide y es un binario más.
+#
+# `NTPD_RS_GIT_REV` se fija al commit PINEADO a propósito: el `build.rs` de ntpd pregunta a `git` por
+# la revisión y, si el árbol de fuentes trae `.git`, el string de versión del binario cambiaría con
+# él. Fijarlo lo vuelve función de la receta —que sí entra en `hash_inputs`— y no del árbol.
+#
+# MSRV 1.88; el lab trae rust 1.97.0 (`docs/state/lab-toolchain.lock`). Cargo.lock commiteado
+# upstream ⇒ `--locked` + vendoreo dan un build reproducible (ADR 0006).
+name    = "ntpd-rs"
+version = "1.9.0"
+license = "Apache-2.0 OR MIT"
+
+[source]
+repo   = "https://github.com/pendulum-project/ntpd-rs"
+# tag v1.9.0. El commit es el identificador inmutable (ADR 0006), no el tag.
+commit = "46ec9bb4d5b6cb24f814f5543d85b9138afb4cba"
+
+[build]
+compiler = "zig-cc"
+target   = "x86_64-linux-musl"
+link     = "static"
+strip_debug = true
+flags    = []
+
+[build.phases]
+compile = '''
+printf '%s\n' '#!/bin/sh' 'for a do' 'case "$a" in --target=*) a=--target=x86_64-linux-musl ;; esac' 'set -- "$@" "$a"' 'shift' 'done' 'exec zig cc -mcpu=baseline "$@"' > "$PWD/.hammer-zig-cc"
+chmod +x "$PWD/.hammer-zig-cc"
+RF="-C linker=$PWD/.hammer-zig-cc"
+rustc -vV | grep -q 'host: .*-alpine-' || RF="$RF -C link-self-contained=no"
+for b in ntp-daemon ntp-ctl; do
+  NTPD_RS_GIT_REV=46ec9bb4d5b6cb24f814f5543d85b9138afb4cba \
+  CC="$PWD/.hammer-zig-cc" RUSTFLAGS="$RF" \
+  cargo rustc --release --locked -p ntpd --bin "$b" --no-default-features --features rustcrypto \
+    -- -C target-feature=+crt-static -C relocation-model=static
+done
+'''
+# El install es propio porque además de los binarios viaja el EJEMPLO de config. No se instala como
+# `/etc/ntpd-rs/ntp.toml` —eso lo hace el .deb de upstream— porque a qué servidores NTP se pregunta
+# es decisión del SITIO: ver el `[[service]]` de abajo, que sale 78 nombrando el fichero.
+install = '''
+mkdir -p /out/usr/bin /out/usr/share/doc/ntpd-rs
+cp target/release/ntp-daemon /out/usr/bin/ntp-daemon
+cp target/release/ntp-ctl    /out/usr/bin/ntp-ctl
+cp docs/examples/conf/ntp.toml.default /out/usr/share/doc/ntpd-rs/ntp.toml.default
+'''
+
+[deps]
+build = ["binutils"]
+
+# ── EL SERVICIO QUE ESTE PAQUETE TRAE (SDD 30) ──────────────────────────────────────────────────
+# Fuera de `hash_inputs`: declararlo NO re-hashea nada.
+#
+# El porqué es el de `recipes/chrony.toml` y no cambia: **sin hora no hay TLS ni firmas**, y el
+# síntoma no se parece a la causa — los certificados salen «not yet valid» o «expired», el ACME falla,
+# y todo apunta al servidor web. Una caja de Hetzner arranca con la hora del reloj virtual y sin nadie
+# que la corrija: `netup` pone la red y ahí termina su trabajo.
+#
+# Corre como root porque ajusta el reloj del sistema (`clock_settime`/`adjtimex`), que es capability
+# de root. Bajar de privilegio necesita una cuenta de servicio, y esa decisión va con la del setuid
+# (ver `crates/takana-core/src/setuid.rs`), no antes.
+#
+# ⚠ La CONFIG es del sitio y el servicio sale **78** nombrando el fichero en vez de inventarse un
+# pool: una caja que se sincroniza contra un servidor que nadie eligió es peor que una sin hora,
+# porque nadie lo mira. El ejemplo viaja en `/usr/share/doc/ntpd-rs/ntp.toml.default`.
+#
+# ⚠ `exec = "/bin/busybox"` es el patrón de TODAS las cards de hoy (chrony, gitea, pacha…): el `sh`
+# sale del `product-rootfs`, que lo trae por `STAGE1_COMPONENTS`. Cuando el frente C de busybox se
+# cierre, las cards se mueven juntas — no es deuda de esta receta.
+[[service]]
+label      = "ntpd-rs"
+id         = "01M2EKDA00NTPDRS7K2X4M9V40"
+exec       = "/bin/busybox"
+argv = ["sh", "-c", "test -f /etc/ntpd-rs/ntp.toml || { echo 'ntpd-rs: falta /etc/ntpd-rs/ntp.toml — a qué servidores NTP se pregunta es decisión del sitio, no del paquete (ejemplo en /usr/share/doc/ntpd-rs/ntp.toml.default)' >&2; exit 78; }; mkdir -p /var/run/ntpd-rs; exec /usr/bin/ntp-daemon -c /etc/ntpd-rs/ntp.toml"]
+envp       = [["PATH", "/usr/sbin:/usr/bin:/sbin:/bin"], ["HOME", "/root"], ["USER", "root"]]
+networking = "full"
+cgroup     = "arje.slice/ntpd-rs"
+restart    = { initial_ms = 1000, max_ms = 30000 }
diff --git a/scripts/servidor/jaula-herramientas.sh b/scripts/servidor/jaula-herramientas.sh
index 61d9c51e..a6eb90a7 100755
--- a/scripts/servidor/jaula-herramientas.sh
+++ b/scripts/servidor/jaula-herramientas.sh
@@ -129,10 +129,31 @@ envoltorio_dinamico() {
   chmod 755 "$BIN/$t"
 }
 
-for par in make:make pkgconf:pkgconf jq:jq rsync:rsync git:git curl:curl takana:takana; do
+for par in make:make pkgconf:pkgconf jq:jq rsync:rsync curl:curl takana:takana; do
   envoltorio "${par%%:*}" "${par##*:}" ":"
 done
 
+# ── `git` aparte: SU EXEC-PATH ESTÁ COMPILADO Y ES ABSOLUTO (medido el 2026-09-22) ──────────────
+# `git` no es un binario: es un multiplexor que lanza `git-remote-https`, `git-http-fetch` y otros
+# treinta ayudantes buscándolos en un directorio COMPILADO AL BUILD — `/usr/libexec/git-core`, la ruta
+# FHS. Corrido desde el store, esa ruta es la de la IMAGEN de la jaula, que acá está VACÍA de
+# ayudantes (`ls /usr/libexec/git-core | grep -c remote-http` ⇒ **0**), así que git no encuentra los
+# SUYOS aunque estén en su artefacto:
+#
+#     $ git ls-remote https://github.com/…
+#     git: 'remote-https' is not a git command.
+#
+# ⚠ **Y eso NO es «el git del corpus no trae los helpers», que es lo que este repo creía y escribió en
+# CLAUDE.md §2.** Los trae: `…-git/usr/libexec/git-core/git-remote-https` está ahí, medido. Lo único
+# que faltaba era decirle dónde mirar. Con `GIT_EXEC_PATH` apuntando a su propio libexec, el mismo
+# binario clona por https Y empuja al espejo de GitHub — comprobado con `ls-remote` y con
+# `push --dry-run`, que llegó hasta el rechazo por non-fast-forward, o sea que habló TLS y autenticó.
+#
+# Es la misma familia que el resto de este guion (un binario del store que no encuentra lo suyo
+# porque la ruta está horneada), sólo que acá lo que falta no es el cargador: es un directorio.
+envoltorio git git 'export GIT_EXEC_PATH="$b/usr/libexec/git-core"'
+
+
 # `python3` y `cargo` no llevan banderas; `rustc` sí — son las tres que necesita el enlace en este
 # triple, las mismas que `cc-por-zig.sh` pone en la caja. `link-self-contained=no` porque este
 # toolchain no trae `rust-lld` (incompatible con el llvm-config externo).