diff --git a/ai/Dockerfile b/ai/Dockerfile index 3f5da57..698bd37 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -1,47 +1,30 @@ # 1. On récupère la version la plus récente d'UV FROM ghcr.io/astral-sh/uv:latest AS uv_source -# 2. Image de base stable -FROM debian:stable-slim +# 2. Image officielle Hermes Agent de NousResearch +# Contient déjà: Python, Node.js, npm, Playwright/Chromium, venv, tts_tool.py, etc. +FROM nousresearch/hermes-agent:latest -# Disable Python stdout buffering to ensure logs are printed immediately -ENV PYTHONUNBUFFERED=1 - -# Install system dependencies in one layer, clear APT cache -# tini reaps orphaned zombie processes (MCP stdio subprocesses, git, bun, etc.) +# ---------- System dependencies ---------- +# Piper a besoin de libportaudio2, et HuggingFace a besoin de ca-certificates +USER root RUN apt-get update && \ apt-get install -y --no-install-recommends \ - build-essential python3 ripgrep ffmpeg gcc python3-dev libffi-dev procps git openssh-client docker-cli tini \ - curl poppler-utils imagemagick \ - texlive-latex-base texlive-latex-extra texlive-fonts-recommended texlive-xetex texlive-science \ - qemu-user-static binfmt-support qemu-user-binfmt \ - emacs-nox \ libportaudio2 \ ca-certificates && \ rm -rf /var/lib/apt/lists/* -# Création de l'utilisateur 'hermes' directement avec les bons accès -RUN useradd -u 10000 -m -d /opt/data hermes - -# Copie d'uv (dernière version) +# ---------- UV (hyperfast pip alternative) ---------- COPY --chmod=0755 --from=uv_source /uv /usr/local/bin/ WORKDIR /opt/hermes -# On donne la propriété du dossier de travail à l'utilisateur hermes -RUN chown hermes:hermes /opt/hermes - # ---------- Hermes venv ---------- -# Passer immédiatement sous l'utilisateur hermes pour tout le reste du build USER hermes -# ---------- Source code ---------- -# On copie tout le projet d'un coup sans assumer la présence de fichiers de lock spécifiques -COPY --chown=hermes:hermes . . - # ---------- Python virtualenv avec Piper TTS ---------- RUN uv venv && \ - uv pip install --no-cache-dir hermes-agent piper-tts sounddevice numpy faster-whisper + uv pip install --no-cache-dir piper-tts sounddevice numpy # ---------- Télécharger la voix Piper Ryan (high quality) ---------- RUN mkdir -p /opt/hermes/.venv/share/piper/voices && \ @@ -52,62 +35,12 @@ url = 'https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/ryan/hi urllib.request.urlretrieve(url, base + '/en_US-ryan-high.onnx') urllib.request.urlretrieve(url + '.json', base + '/en_US-ryan-high.onnx.json') PYEOF - -# ---------- Patch tts_tool.py: replace Edge TTS fallback with Piper ---------- -# Edge TTS calls out to Microsoft servers — we never want that. -# Piper runs locally on CPU, no cloud, no data leaving the machine. -# hermes-agent is installed from pip so tools/tts_tool.py exists in the venv. + +# ---------- Patch tts_tool.py: remplacer Edge TTS par Piper ---------- +# Edge TTS appelle les serveurs Microsoft — on ne veut jamais ça. +# Piper roule localement sur CPU, aucun cloud, aucune donnée qui sort. COPY patch_tts_tool.py /tmp/patch_tts_tool.py RUN /opt/hermes/.venv/bin/python3 /tmp/patch_tts_tool.py && rm /tmp/patch_tts_tool.py - -# ---------- Patch atomic writes to preserve file permissions ---------- -# Fixes https://github.com/NousResearch/hermes-agent/issues/14181 -# tempfile.mkstemp() creates files as 0600; os.replace() preserves that mode, -# so group-readable files silently collapse to owner-private 0600. -# This affects: skills, sessions, memories, and any file written atomically. -RUN /opt/hermes/.venv/bin/python3 /dev/stdin << 'PYEOF' -import os - -patches = [ - ("/opt/hermes/tools/skill_manager_tool.py", [ - ("# Restore existing file mode if present", True), # already patched - ]), - ("/opt/hermes/tools/skills_sync.py", [ - ("# Restore existing file mode if present", True), # already patched - ]), -] - -for fpath, checks in patches: - if not os.path.exists(fpath): - print(f"SKIP {fpath} (not found)") - continue - with open(fpath) as f: - code = f.read() - all_ok = all(marker in code for marker, _ in checks) - if all_ok: - print(f"OK {fpath} (already patched)") - continue - print(f"PATCH {fpath}") - # _atomic_write_text in skill_manager_tool.py - code = code.replace( - " os.replace(temp_path, file_path)", - " if file_path.exists():\n" - " existing_mode = file_path.stat().st_mode\n" - " os.chmod(temp_path, existing_mode)\n" - " os.replace(temp_path, file_path)", - ) - # _write_manifest in skills_sync.py - code = code.replace( - " os.replace(tmp_path, MANIFEST_FILE)", - " if MANIFEST_FILE.exists():\n" - " existing_mode = MANIFEST_FILE.stat().st_mode\n" - " os.chmod(tmp_path, existing_mode)\n" - " os.replace(tmp_path, MANIFEST_FILE)", - ) - with open(fpath, 'w') as f: - f.write(code) - print(f"DONE {fpath}") -PYEOF # ---------- Runtime ---------- ENV HERMES_HOME=/opt/data @@ -115,9 +48,7 @@ ENV PATH="/opt/data/.local/bin:${PATH}" VOLUME [ "/opt/data" ] -# Copie du script de réparation des permissions (lancement au démarrage) +# Script de réparation des permissions + patch TTS au démarrage COPY --chmod=0755 fix-permissions.sh /opt/hermes/fix-permissions.sh -# Le conteneur tourne de manière ultra-sécurisée sous l'utilisateur hermes dès le départ -# fix-permissions.sh chown les répertoires critiques avant de chaîner vers entrypoint.sh ENTRYPOINT [ "/usr/bin/tini", "-g", "--", "/opt/hermes/fix-permissions.sh" ] diff --git a/ai/fix-permissions.sh b/ai/fix-permissions.sh index c1fb3d3..7af8d0c 100644 --- a/ai/fix-permissions.sh +++ b/ai/fix-permissions.sh @@ -1,13 +1,11 @@ #!/bin/bash -# Startup permission fix for the Hermes data volume. +# Startup permission fix + TTS patch. # Runs as root before the entrypoint drops to the hermes user. -# Fixes files that were created by root (host agent, cron jobs, etc.) -# becoming inaccessible to the hermes runtime user. set -e HERMES_HOME="${HERMES_HOME:-/opt/data}" -# Fix ownership on critical writable directories so hermes user can access them +# Fix ownership on critical writable directories chown -R hermes:hermes \ "$HERMES_HOME/sessions" \ "$HERMES_HOME/checkpoints" \ @@ -22,20 +20,19 @@ chown -R hermes:hermes \ "$HERMES_HOME/cache" \ 2>/dev/null || true -# Also fix the data volume root if it's wrong +# Fix data volume root ownership if [ "$(stat -c %u "$HERMES_HOME" 2>/dev/null)" != "$(id -u hermes)" ]; then chown hermes:hermes "$HERMES_HOME" 2>/dev/null || true fi # ---------- Patch tts_tool.py: replace Edge TTS with Piper ---------- -# Runs at startup so the patch is applied even if the Python package is -# updated (e.g. via pip upgrade on the volume). Idempotent -- if the -# patch is already applied the script does nothing. +# Fallback runtime patch in case the volume's site-packages differ from the image. +# Idempotent: if already patched, the script does nothing. PATCH_SCRIPT="/opt/hermes/patch_tts_tool.py" if [ -f "$PATCH_SCRIPT" ]; then echo "Applying TTS patch (Piper only, no Edge fallback)..." /opt/hermes/.venv/bin/python3 "$PATCH_SCRIPT" 2>&1 || true fi -# Now chain to the real entrypoint +# Chain to the official Hermes entrypoint exec /opt/hermes/docker/entrypoint.sh "$@"