From bef5215bea5b9ee498a5c81eab2c0f339b79030a Mon Sep 17 00:00:00 2001 From: luna Date: Sat, 19 Sep 2026 12:46:14 +0000 Subject: [PATCH] =?UTF-8?q?mars(kittentts):=20review=20round=202=20?= =?UTF-8?q?=E2=80=94=20setpriv=20delivery,=20staged=20hub=20tree,=20orderi?= =?UTF-8?q?ng=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- hosts/mars/hermes-agent.nix | 143 +++++++++++++++++++++--------------- 1 file changed, 84 insertions(+), 59 deletions(-) diff --git a/hosts/mars/hermes-agent.nix b/hosts/mars/hermes-agent.nix index e4fa364..089a5f6 100644 --- a/hosts/mars/hermes-agent.nix +++ b/hosts/mars/hermes-agent.nix @@ -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/; 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/ 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" ''; };