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
);
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=";
};
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=";
};
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=";
};
@@ -524,10 +524,16 @@ in
wants = [ "network-online.target" ];
# after prepare-dirs (review #6): on a fresh state dir this unit must not
# create hermesHome root-owned before prepare-dirs sets the ownership
# layout — same ordering contract the mnemosyne unit has.
after = [ "hermes-agent-prepare-dirs.service" ];
# layout — same ordering contract the mnemosyne unit has. PLUS
# 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" ];
path = [ pkgs.uv pkgs.coreutils ];
path = [ pkgs.uv pkgs.coreutils pkgs.setpriv ];
serviceConfig = {
Type = "oneshot";
TimeoutStartSec = 600;
@@ -549,8 +555,12 @@ in
# into hermesHome, and only after being validated. Root doesn't follow
# any uid-986-writable path while running as root.
stageDir=/var/lib/hermes-kittentts
# Root-side staging of the model files (hash-pinned store paths; the
# delivered copy under hermesHome is compared against these).
# Root-side staging of the DELIVERED TREE (venv + full HF hub layout
# 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
# Input key: requirements + wheel + stub + model files + resolved script.
# Review #5 a miss on the old requirements-only stamp let a changed
@@ -577,69 +587,84 @@ in
fi
fi
# ---- staging venv: root-owned path, uv cache included. Root runs
# python from HERE (container can write nothing in this tree), then the
# FINISHED result is copied into hermesHome. Review #1: a stamp symlink
# into /etc/shadow can't redirect us the stamp dir is root-only. ----
stageVenv=$stageDir/venv
mkdir -p "$stageDir" "$stageSnap"
# ---- staging venv + staged hub tree: root-owned path, uv cache
# included. Root runs python from HERE (container can write nothing in
# this tree); the FINISHED result is copied into hermesHome AS THE
# CONTAINER USER via setpriv (review2 #4) root never writes into
# hermesHome, so no uid-986-controlled path is ever followed while
# running as root; symlink-takeover of stamps/refs/install targets is
# structurally impossible. ----
mkdir -p "$stageDir" "$stageSnap" "$stageVenv" "$stageHub"
chmod 0700 "$stageDir"
install -m 0444 ${kittenttsModelConfig} "$stageSnap/config.json"
install -m 0444 ${kittenttsModelOnnx} "$stageSnap/kitten_tts_mini_v0_8.onnx"
install -m 0444 ${kittenttsModelVoices} "$stageSnap/voices.npz"
rm -rf "$stageVenv"
UV_CACHE_DIR=$stageDir/uv-cache \
uv venv "$stageVenv" --python ${pkgs.python313}/bin/python3 --quiet
UV_CACHE_DIR=$stageDir/uv-cache \
uv pip install --python "$stageVenv/bin/python" --quiet \
--requirement ${kittenttsReqs}
# 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}
# Skip the venv REBUILD when staging is still valid (review2 minor:
# damaged delivery should be a copy, not a rebuild).
if ! [ -x "$stageVenv/bin/python" ] \
|| ! [ -f "$stageVenv/lib/python3.13/site-packages/kitten_tts_stub.py" ]; then
rm -rf "$stageVenv"
UV_CACHE_DIR=$stageDir/uv-cache \
uv venv "$stageVenv" --python ${pkgs.python313}/bin/python3 --quiet
UV_CACHE_DIR=$stageDir/uv-cache \
uv pip install --python "$stageVenv/bin/python" --quiet \
--requirement ${kittenttsReqs}
# 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
# so `from misaki import en, espeak` resolves without the real misaki.en.
siteDir=$("$stageVenv/bin/python" -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')
cp ${kittenttsStub} "$siteDir/kitten_tts_stub.py"
printf 'import kitten_tts_stub\n' > "$siteDir/zz-kitten-stub.pth"
# Dead-import shim: .pth auto-loads kitten_tts_stub at interpreter
# start so `from misaki import en, espeak` resolves without the real
# misaki.en.
siteDir=$("$stageVenv/bin/python" -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')
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
# cleanly offline; a failing build aborts BEFORE anything lands in
# hermesHome (fail closed, not half-delivered).
HF_HOME=$stageDir/hf-cache \
# Stage the full delivered hub tree (exact hf_hub_download layout:
# refs/main -> snapshots/<sha>; review1 #3) under root-owned staging.
stageModelDir=$stageHub/hub/models--KittenML--kitten-tts-mini-0.8
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 \
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()))') \
"$stageVenv/bin/python" -c 'from kittentts import KittenTTS; print("kittentts import ok")' >/dev/null
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; KittenTTS("KittenML/kitten-tts-mini-0.8"); print("kittentts offline build ok")' >/dev/null
# ---- deliver: only finished artifacts into hermesHome. Copy, not
# move staging stays root-owned for the integrity check above. ----
hubDir=${hermesHome}/kittentts-hf/hub/models--KittenML--kitten-tts-mini-0.8
snap=$hubDir/snapshots/$modelSha
rm -rf "$venv.tmp"
cp -a "$stageVenv" "$venv.tmp"
mv -Tf "$venv.tmp" "$venv" # atomic-ish swap of the delivered venv
mkdir -p "$hubDir/refs" "$snap" "$hubDir/blobs"
printf '%s' "$modelSha" > "$hubDir/refs/main" # review #3: resolves offline
install -m 0444 ${kittenttsModelOnnx} "$snap/kitten_tts_mini_v0_8.onnx"
install -m 0444 ${kittenttsModelVoices} "$snap/voices.npz"
install -m 0444 ${kittenttsModelConfig} "$snap/config.json"
# HF hub layout puts the blob under blobs/<sha256> with snapshot
# symlinks pointing back but kittentts reads config/model paths
# RETURNED by hf_hub_download, which serves the resolved snapshot file
# 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"
# ---- deliver AS THE CONTAINER USER (review2 #4): root never writes
# into hermesHome, so no symlink race and no `chown` step. setpriv
# drops to uid 986, rm -rf's the old delivered copies and copies the
# fresh staging tree in. cp-as-986 also fixes review2 #2: rm+cp in one
# step, no mv -Tf rename-replace on a non-empty directory.
setpriv --reuid=${hermesUid} --regid=${hermesGid} --clear-groups \
env HOME="$hermesHome" sh -c '
set -eu
rm -rf "$1" "$2"
cp -a "$3" "$1"
cp -a "$4" "$2"
' _ \
"${hermesHome}/kittentts-venv" \
"${hermesHome}/kittentts-hf" \
"$stageVenv" \
"$stageHub"
# 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"
'';
};