mars(kittentts): review round 2 — setpriv delivery, staged hub tree, ordering fix

1: stageVenv declared with the other path vars before the early-exit
reads it (set -u no longer kills the self-heal path on later boots).
2: delivery is rm+cp as the container uid via setpriv — no mv -Tf
rename-replace on a non-empty directory (that failed every
re-provision); chown -R step gone with it.
3: network-online.target back in after= — this unit CAN download at
boot, so uv must not run pre-network.
4: root never writes into hermesHome at all. refs/main, snapshots and
venv staging happen under root-owned staging; delivery drops to uid 986
via setpriv, rm -rf + cp -a from the staging sources. Symlink-takeover
of refs/install targets and the rm/cp race on the delivered tree are
structurally gone.
Minor: sanity check now constructs KittenTTS against the staged hub
tree, not just imports; fetchurls pinned to the commit sha matching
refs/main; fast-path comment matches behavior (staging venv skips
rebuild on damaged delivery).
This commit is contained in:
2026-09-19 12:46:14 +00:00
parent b2982c47a1
commit bef5215bea
+84 -59
View File
@@ -92,15 +92,15 @@ let
builtins.readFile ./kittentts/kitten-misaki-stub.py builtins.readFile ./kittentts/kitten-misaki-stub.py
); );
kittenttsModelOnnx = pkgs.fetchurl { kittenttsModelOnnx = pkgs.fetchurl {
url = "https://huggingface.co/KittenML/kitten-tts-mini-0.8/resolve/main/kitten_tts_mini_v0_8.onnx"; url = "https://huggingface.co/KittenML/kitten-tts-mini-0.8/resolve/c02725660cea441db4c383af69f1f26f5cd00947/kitten_tts_mini_v0_8.onnx";
sha256 = "sha256-D1u65PxIAMmNvFRKh+z6eVEN4vuCItsw0S5b/pF335E="; sha256 = "sha256-D1u65PxIAMmNvFRKh+z6eVEN4vuCItsw0S5b/pF335E=";
}; };
kittenttsModelVoices = pkgs.fetchurl { kittenttsModelVoices = pkgs.fetchurl {
url = "https://huggingface.co/KittenML/kitten-tts-mini-0.8/resolve/main/voices.npz"; url = "https://huggingface.co/KittenML/kitten-tts-mini-0.8/resolve/c02725660cea441db4c383af69f1f26f5cd00947/voices.npz";
sha256 = "sha256-QK0mOJUrd7ey8wEn4mCOFp/GndJWtTvYqqNAmjMZPEI="; sha256 = "sha256-QK0mOJUrd7ey8wEn4mCOFp/GndJWtTvYqqNAmjMZPEI=";
}; };
kittenttsModelConfig = pkgs.fetchurl { kittenttsModelConfig = pkgs.fetchurl {
url = "https://huggingface.co/KittenML/kitten-tts-mini-0.8/resolve/main/config.json"; url = "https://huggingface.co/KittenML/kitten-tts-mini-0.8/resolve/c02725660cea441db4c383af69f1f26f5cd00947/config.json";
sha256 = "sha256-axYLybGeJOyyHoS8FPin2iH99H7HLUJFC8XPUUthgEo="; sha256 = "sha256-axYLybGeJOyyHoS8FPin2iH99H7HLUJFC8XPUUthgEo=";
}; };
@@ -524,10 +524,16 @@ in
wants = [ "network-online.target" ]; wants = [ "network-online.target" ];
# after prepare-dirs (review #6): on a fresh state dir this unit must not # after prepare-dirs (review #6): on a fresh state dir this unit must not
# create hermesHome root-owned before prepare-dirs sets the ownership # create hermesHome root-owned before prepare-dirs sets the ownership
# layout — same ordering contract the mnemosyne unit has. # layout — same ordering contract the mnemosyne unit has. PLUS
after = [ "hermes-agent-prepare-dirs.service" ]; # network-online ordering (review2 #3): this unit CAN download at boot
# (unlike mnemosyne's store-path build), so uv must not run before the
# network is actually up.
after = [
"network-online.target"
"hermes-agent-prepare-dirs.service"
];
requires = [ "hermes-agent-prepare-dirs.service" ]; requires = [ "hermes-agent-prepare-dirs.service" ];
path = [ pkgs.uv pkgs.coreutils ]; path = [ pkgs.uv pkgs.coreutils pkgs.setpriv ];
serviceConfig = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";
TimeoutStartSec = 600; TimeoutStartSec = 600;
@@ -549,8 +555,12 @@ in
# into hermesHome, and only after being validated. Root doesn't follow # into hermesHome, and only after being validated. Root doesn't follow
# any uid-986-writable path while running as root. # any uid-986-writable path while running as root.
stageDir=/var/lib/hermes-kittentts stageDir=/var/lib/hermes-kittentts
# Root-side staging of the model files (hash-pinned store paths; the # Root-side staging of the DELIVERED TREE (venv + full HF hub layout
# delivered copy under hermesHome is compared against these). # including refs/main) everything root writes lives here, 0700.
# review2 #4: root never writes into hermesHome; delivery happens as
# the container uid via setpriv, copying from these root-owned sources.
stageVenv=$stageDir/venv
stageHub=$stageDir/kittentts-hf
stageSnap=$stageDir/hf-model stageSnap=$stageDir/hf-model
# Input key: requirements + wheel + stub + model files + resolved script. # Input key: requirements + wheel + stub + model files + resolved script.
# Review #5 a miss on the old requirements-only stamp let a changed # Review #5 a miss on the old requirements-only stamp let a changed
@@ -577,69 +587,84 @@ in
fi fi
fi fi
# ---- staging venv: root-owned path, uv cache included. Root runs # ---- staging venv + staged hub tree: root-owned path, uv cache
# python from HERE (container can write nothing in this tree), then the # included. Root runs python from HERE (container can write nothing in
# FINISHED result is copied into hermesHome. Review #1: a stamp symlink # this tree); the FINISHED result is copied into hermesHome AS THE
# into /etc/shadow can't redirect us the stamp dir is root-only. ---- # CONTAINER USER via setpriv (review2 #4) root never writes into
stageVenv=$stageDir/venv # hermesHome, so no uid-986-controlled path is ever followed while
mkdir -p "$stageDir" "$stageSnap" # running as root; symlink-takeover of stamps/refs/install targets is
# structurally impossible. ----
mkdir -p "$stageDir" "$stageSnap" "$stageVenv" "$stageHub"
chmod 0700 "$stageDir" chmod 0700 "$stageDir"
install -m 0444 ${kittenttsModelConfig} "$stageSnap/config.json" install -m 0444 ${kittenttsModelConfig} "$stageSnap/config.json"
install -m 0444 ${kittenttsModelOnnx} "$stageSnap/kitten_tts_mini_v0_8.onnx" install -m 0444 ${kittenttsModelOnnx} "$stageSnap/kitten_tts_mini_v0_8.onnx"
install -m 0444 ${kittenttsModelVoices} "$stageSnap/voices.npz" install -m 0444 ${kittenttsModelVoices} "$stageSnap/voices.npz"
rm -rf "$stageVenv" # Skip the venv REBUILD when staging is still valid (review2 minor:
UV_CACHE_DIR=$stageDir/uv-cache \ # damaged delivery should be a copy, not a rebuild).
uv venv "$stageVenv" --python ${pkgs.python313}/bin/python3 --quiet if ! [ -x "$stageVenv/bin/python" ] \
UV_CACHE_DIR=$stageDir/uv-cache \ || ! [ -f "$stageVenv/lib/python3.13/site-packages/kitten_tts_stub.py" ]; then
uv pip install --python "$stageVenv/bin/python" --quiet \ rm -rf "$stageVenv"
--requirement ${kittenttsReqs} UV_CACHE_DIR=$stageDir/uv-cache \
# kittentts --no-deps: its overlay of spacy/misaki[en] is what drags in uv venv "$stageVenv" --python ${pkgs.python313}/bin/python3 --quiet
# the CUDA tree; the requirements freeze already covers its real needs. UV_CACHE_DIR=$stageDir/uv-cache \
UV_CACHE_DIR=$stageDir/uv-cache \ uv pip install --python "$stageVenv/bin/python" --quiet \
uv pip install --python "$stageVenv/bin/python" --quiet --no-deps \ --requirement ${kittenttsReqs}
${kittenttsWheel} # kittentts --no-deps: its overlay of spacy/misaki[en] is what drags in
# the CUDA tree; the requirements freeze already covers its real needs.
UV_CACHE_DIR=$stageDir/uv-cache \
uv pip install --python "$stageVenv/bin/python" --quiet --no-deps \
${kittenttsWheel}
# Dead-import shim: .pth auto-loads kitten_tts_stub at interpreter start # Dead-import shim: .pth auto-loads kitten_tts_stub at interpreter
# so `from misaki import en, espeak` resolves without the real misaki.en. # start so `from misaki import en, espeak` resolves without the real
siteDir=$("$stageVenv/bin/python" -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])') # misaki.en.
cp ${kittenttsStub} "$siteDir/kitten_tts_stub.py" siteDir=$("$stageVenv/bin/python" -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')
printf 'import kitten_tts_stub\n' > "$siteDir/zz-kitten-stub.pth" cp ${kittenttsStub} "$siteDir/kitten_tts_stub.py"
printf 'import kitten_tts_stub\n' > "$siteDir/zz-kitten-stub.pth"
fi
# Root-side sanity: the venv python must import the plugin entrypoint # Stage the full delivered hub tree (exact hf_hub_download layout:
# cleanly offline; a failing build aborts BEFORE anything lands in # refs/main -> snapshots/<sha>; review1 #3) under root-owned staging.
# hermesHome (fail closed, not half-delivered). stageModelDir=$stageHub/hub/models--KittenML--kitten-tts-mini-0.8
HF_HOME=$stageDir/hf-cache \ mkdir -p "$stageModelDir/refs" "$stageModelDir/snapshots/$modelSha"
printf '%s' "$modelSha" > "$stageModelDir/refs/main"
install -m 0444 ${kittenttsModelOnnx} "$stageModelDir/snapshots/$modelSha/kitten_tts_mini_v0_8.onnx"
install -m 0444 ${kittenttsModelVoices} "$stageModelDir/snapshots/$modelSha/voices.npz"
install -m 0444 ${kittenttsModelConfig} "$stageModelDir/snapshots/$modelSha/config.json"
# No blobs/ indirection: kittentts reads paths RETURNED by
# hf_hub_download, which serves the resolved snapshot file directly
# (prefer-dir layout works offline for fully-materialized files).
# Root-side sanity: the staged interpreter must construct the model
# END-TO-END offline (review2 minor import alone doesn't exercise
# hf_hub_download; a broken cache layout must fail HERE, not in the
# gateway). Points HF_HOME at the staged hub tree itself.
HF_HOME=$stageHub \
HF_HUB_OFFLINE=1 \ HF_HUB_OFFLINE=1 \
PHONEMIZER_ESPEAK_LIBRARY="$("$stageVenv/bin/python" -c 'import espeakng_loader,pathlib;print(pathlib.Path(espeakng_loader.get_library_path()))')" \ PHONEMIZER_ESPEAK_LIBRARY="$("$stageVenv/bin/python" -c 'import espeakng_loader,pathlib;print(pathlib.Path(espeakng_loader.get_library_path()))')" \
PHONEMIZER_ESPEAK_DATA_PATH=$("$stageVenv/bin/python" -c 'import espeakng_loader,pathlib;print(pathlib.Path(espeakng_loader.get_data_path()))') \ PHONEMIZER_ESPEAK_DATA_PATH="$("$stageVenv/bin/python" -c 'import espeakng_loader,pathlib;print(pathlib.Path(espeakng_loader.get_data_path()))')" \
"$stageVenv/bin/python" -c 'from kittentts import KittenTTS; print("kittentts import ok")' >/dev/null "$stageVenv/bin/python" -c 'from kittentts import KittenTTS; KittenTTS("KittenML/kitten-tts-mini-0.8"); print("kittentts offline build ok")' >/dev/null
# ---- deliver: only finished artifacts into hermesHome. Copy, not # ---- deliver AS THE CONTAINER USER (review2 #4): root never writes
# move staging stays root-owned for the integrity check above. ---- # into hermesHome, so no symlink race and no `chown` step. setpriv
hubDir=${hermesHome}/kittentts-hf/hub/models--KittenML--kitten-tts-mini-0.8 # drops to uid 986, rm -rf's the old delivered copies and copies the
snap=$hubDir/snapshots/$modelSha # fresh staging tree in. cp-as-986 also fixes review2 #2: rm+cp in one
rm -rf "$venv.tmp" # step, no mv -Tf rename-replace on a non-empty directory.
cp -a "$stageVenv" "$venv.tmp" setpriv --reuid=${hermesUid} --regid=${hermesGid} --clear-groups \
mv -Tf "$venv.tmp" "$venv" # atomic-ish swap of the delivered venv env HOME="$hermesHome" sh -c '
set -eu
mkdir -p "$hubDir/refs" "$snap" "$hubDir/blobs" rm -rf "$1" "$2"
printf '%s' "$modelSha" > "$hubDir/refs/main" # review #3: resolves offline cp -a "$3" "$1"
install -m 0444 ${kittenttsModelOnnx} "$snap/kitten_tts_mini_v0_8.onnx" cp -a "$4" "$2"
install -m 0444 ${kittenttsModelVoices} "$snap/voices.npz" ' _ \
install -m 0444 ${kittenttsModelConfig} "$snap/config.json" "${hermesHome}/kittentts-venv" \
# HF hub layout puts the blob under blobs/<sha256> with snapshot "${hermesHome}/kittentts-hf" \
# symlinks pointing back but kittentts reads config/model paths "$stageVenv" \
# RETURNED by hf_hub_download, which serves the resolved snapshot file "$stageHub"
# directly. Files-as-plain-content suffices (no LFS indirection here:
# pre-materialized), no blob indirection needed.
# Hand ownership to the container uid BEFORE any python runs in
# hermesHome's copy root does its python in $stageVenv, not here.
chown -R ${hermesUid}:${hermesGid} "$venv" "$hubDir"
# Stamp LAST, root-owned outside hermesHome luna can delete it (which # Stamp LAST, root-owned outside hermesHome luna can delete it (which
# just forces a cheap re-provision copy), not tamper via symlink. # forces a cheap re-delivery on next boot), not tamper via symlink.
printf '%s' "$inputHash" > "$stampFile" printf '%s' "$inputHash" > "$stampFile"
''; '';
}; };