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:
+70
-45
@@ -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,17 +587,23 @@ 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"
|
||||
|
||||
# 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
|
||||
@@ -600,46 +616,55 @@ in
|
||||
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.
|
||||
# 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"
|
||||
'';
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user