diff --git a/ai/compose.yml b/ai/compose.yml old mode 100644 new mode 100755 index 1db7831..5c87263 --- a/ai/compose.yml +++ b/ai/compose.yml @@ -28,17 +28,22 @@ services: hermes: build: context: ./hermes - ssh: - - default + args: + HERMES_PLUGIN_URLS: "git+https://code.lazyworkhorse.net/gortium/hermes-piper-plugin.git;git+https://code.lazyworkhorse.net/gortium/hermes-identity-plugin.git" container_name: hermes entrypoint: ["/bin/bash", "-c", - "bash /opt/data/hermes-tools/install.sh && exec /usr/bin/tini -g -- /opt/hermes/docker/entrypoint.sh \"$@\"", + "bash /opt/data/hermes-tools/install.sh && bash /usr/local/bin/run-multi-gateways.sh && exec /usr/bin/tini -g -- /opt/hermes/docker/entrypoint.sh \"$@\"", "hermes-entrypoint"] restart: always # Gateway run enables the internal API server on port 8642 command: gateway run environment: - OLLAMA_HOST=http://ollama:11434 + - HERMES_DASHBOARD=1 + # Multi-profile: comma-separated list of profiles to run as gateways. + # The entrypoint reads this and starts one gateway per profile. + # Add profiles here when they exist on disk (e.g. default,researcher,writer) + - HERMES_PROFILES=ashley,claire,finn,matt,paul - API_SERVER_ENABLED=true - API_SERVER_PORT=8642 - API_SERVER_HOST=0.0.0.0 @@ -66,6 +71,30 @@ services: - "26" networks: - ai_backend + - ai_net + labels: + - "traefik.enable=true" + - "traefik.docker.network=ai_net" + + # Router for HTTP + redirection to HTTPS + - "traefik.http.routers.hermes-web-http.rule=Host(`hermes.lazyworkhorse.net`)" + - "traefik.http.routers.hermes-web-http.entrypoints=web" + - "traefik.http.routers.hermes-web-http.middlewares=redirect-to-https" + + # Router for HTTPS with TLS — protected by Authelia + - "traefik.http.routers.hermes-web-https.rule=Host(`hermes.lazyworkhorse.net`)" + - "traefik.http.routers.hermes-web-https.entrypoints=websecure" + - "traefik.http.routers.hermes-web-https.tls=true" + - "traefik.http.routers.hermes-web-https.tls.certresolver=njalla" + - "traefik.http.routers.hermes-web-https.middlewares=hermes-auth" + + # Authelia forwardAuth + - "traefik.http.middlewares.hermes-auth.forwardauth.address=http://authelia:9091/api/verify?rd=https://auth.lazyworkhorse.net/" + - "traefik.http.middlewares.hermes-auth.forwardauth.trustforwardheader=true" + - "traefik.http.middlewares.hermes-auth.forwardauth.authresponseheaders=X-Forwarded-User,X-Forwarded-Groups" + + # Service Loadbalancer (dashboard port 9119) + - "traefik.http.services.hermes-web.loadbalancer.server.port=9119" syncthing: image: syncthing/syncthing:latest diff --git a/ai/hermes/Dockerfile b/ai/hermes/Dockerfile index a6edcfc..eac4d47 100644 --- a/ai/hermes/Dockerfile +++ b/ai/hermes/Dockerfile @@ -1,45 +1,22 @@ # syntax=docker/dockerfile:1 -# Hermes Agent -- custom fork build -# Builds on top of official image + overlays our forked source from Gitea. -# Requires Docker BuildKit. Pass SSH agent for git clone: +# Hermes Agent -- official image + custom plugins layered on top. +# No fork needed — customizations are pip-installable plugins from Gitea. # docker compose build hermes # Or manually: -# DOCKER_BUILDKIT=1 docker build --ssh default -t hermes-agent:custom . +# DOCKER_BUILDKIT=1 docker build --build-arg HERMES_PLUGIN_URLS="url1 url2" -t hermes-agent:custom . # ---------- Base: official Hermes image (system deps, npm, uv, Playwright) ---------- FROM nousresearch/hermes-agent:latest -# ---------- Overlay our forked source ---------- -# Uses SSH agent forwarding from the build host (no key baked into image). -# --exclude node_modules/.venv keeps the base image's pre-built layers intact. -# Only the Python source, web UI source, and config change. -RUN --mount=type=ssh \ - mkdir -p /root/.ssh && \ - ssh-keyscan -p 2222 code.lazyworkhorse.net >> /root/.ssh/known_hosts 2>/dev/null && \ - cd /tmp && \ - GIT_SSH_COMMAND='ssh -p 2222 -o StrictHostKeyChecking=no' \ - git clone --depth 1 --branch main \ - git@code.lazyworkhorse.net:gortium/hermes-agent.git fork && \ - rsync -a --delete fork/ /opt/hermes/ \ - --exclude node_modules \ - --exclude .venv \ - --exclude .git && \ - rm -rf /tmp/fork /root/.ssh/ - -# ---------- Rebuild web UI ---------- -# Source files changed; node_modules (from base image) reused. -RUN cd /opt/hermes && npm run build - -# ---------- Reinstall Python package (editable) ---------- -# Picks up source changes from our fork. -RUN . /opt/hermes/.venv/bin/activate && \ - uv pip install --no-cache-dir --no-deps -e /opt/hermes +# ---------- Plugin URLs (semicolon-separated, set via compose.yml build args) ---------- +ARG HERMES_PLUGIN_URLS="" # ---------- Extra system deps ---------- USER root RUN apt-get update && \ apt-get install -y --no-install-recommends \ libportaudio2 ca-certificates poppler-utils imagemagick \ + libolm-dev \ texlive-latex-base texlive-latex-extra texlive-fonts-recommended \ texlive-xetex texlive-science \ qemu-user-static binfmt-support emacs-nox && \ @@ -48,6 +25,10 @@ RUN apt-get update && \ # ---------- UV ---------- COPY --chmod=0755 --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/ +# ---------- Matrix bridge + extra pip deps ---------- +RUN . /opt/hermes/.venv/bin/activate && \ + uv pip install --no-cache-dir 'mautrix[encryption]' openai + # ---------- Piper TTS ---------- RUN . /opt/hermes/.venv/bin/activate && \ uv pip install --no-cache-dir piper-tts sounddevice numpy && \ @@ -75,9 +56,22 @@ os.remove(tgz) print('himalaya v1.2.0 installed') PYEOF -# ---------- Install himalaya-ro wrapper ---------- -COPY --chmod=0755 himalaya-ro.sh /usr/local/bin/himalaya-ro +# ---------- Install custom plugins from URLs ---------- +# HERMES_PLUGIN_URLS is a semicolon-separated list of pip-installable +# package URLs (e.g. git+https:// or direct .tar.gz archives from Gitea). +# Each plugin is installed into the Hermes venv. +RUN if [ -n "$HERMES_PLUGIN_URLS" ]; then \ + . /opt/hermes/.venv/bin/activate && \ + IFS=';' read -ra URLS <<< "$HERMES_PLUGIN_URLS" && \ + for url in "${URLS[@]}"; do \ + echo "Installing plugin: $url" && \ + uv pip install --no-cache-dir "$url"; \ + done; \ + fi +# ---------- Install multi-gateway launcher ---------- +# Launches one gateway process per profile (HERMES_PROFILES env var) +COPY --chmod=0755 run-multi-gateways.sh /usr/local/bin/run-multi-gateways.sh # ---------- Runtime ---------- USER hermes @@ -88,6 +82,7 @@ ENV CHROME_EXECUTABLE=/opt/hermes/.playwright/chromium/chrome-linux/chrome # Ensure tools directory and toolsets.py are writable by the hermes runtime user # so custom tools can be injected from the persistent volume at startup. +USER root RUN chown -R hermes:hermes /opt/hermes/tools /opt/hermes/toolsets.py -VOLUME [ "/opt/data" ] \ No newline at end of file +VOLUME [ "/opt/data" ] diff --git a/ai/hermes/himalaya-ro.sh b/ai/hermes/himalaya-ro.sh deleted file mode 100644 index 212f1ae..0000000 --- a/ai/hermes/himalaya-ro.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env bash -# ───────────────────────────────────────────────────────────── -# himalaya-ro — Read-only wrapper for himalaya -# -# Blocks destructive commands and logs audit trail. -# Pass-through for read-only commands (list, read, search). -# -# Usage: himalaya-ro [options] [args...] -# -# Install: place in PATH before the real himalaya, or use -# `ln -sf himalaya-ro /usr/local/bin/himalaya` -# ───────────────────────────────────────────────────────────── -set -o pipefail - -# ── Configuration ─────────────────────────────────────────── -HIMALAYA_BIN="${HIMALAYA_BIN:-/usr/local/bin/himalaya}" -AUDIT_LOG="${HIMALAYA_AUDIT_LOG:-/var/log/himalaya-audit.log}" - -# ── Destructive commands we block ────────────────────────── -BLOCKED_CMDS=( - "message move" - "message delete" - "message copy" - "flag add" - "flag remove" - "folder create" - "folder delete" - "folder rename" - "template send" - "account configure" - "account delete" -) - -# ── Determine the subcommand being invoked ───────────────── -# Strip leading options (--account, --output, etc.) to find the verb -ARGS=() -SKIP_NEXT=false -for arg in "$@"; do - if $SKIP_NEXT; then - SKIP_NEXT=false - continue - fi - if [[ "$arg" == --* ]]; then - case "$arg" in - --account|--output|--page|--page-size|--folder|--color|--format) - SKIP_NEXT=true ;; - esac - continue - fi - ARGS+=("$arg") -done - -# Build subcommand string and check against blocklist -CMD_STR="" -for ((i=0; i<${#ARGS[@]}; i++)); do - if [ -z "$CMD_STR" ]; then - CMD_STR="${ARGS[$i]}" - else - CMD_STR="$CMD_STR ${ARGS[$i]}" - fi - for blocked in "${BLOCKED_CMDS[@]}"; do - if [[ "$CMD_STR" == "$blocked" ]]; then - TS=$(date '+%Y-%m-%d %H:%M:%S') - echo "[AUDIT] $TS BLOCKED: himalaya $*" >> "$AUDIT_LOG" - echo "ERROR: Command 'himalaya $CMD_STR ...' is blocked by read-only policy." >&2 - echo " Audit log: $AUDIT_LOG" >&2 - exit 100 - fi - done -done - -# ── Allow pass-through ───────────────────────────────────── -exec "$HIMALAYA_BIN" "$@" diff --git a/ai/hermes/run-multi-gateways.sh b/ai/hermes/run-multi-gateways.sh new file mode 100755 index 0000000..f23ac78 --- /dev/null +++ b/ai/hermes/run-multi-gateways.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Multi-gateway launcher for HERMES_PROFILES env var. +# Reads comma-separated profile names, spawns one gateway per profile. +# Designed to run before the main entrypoint — gateways run in background. +set -e + +if [ -z "${HERMES_PROFILES}" ]; then + echo "HERMES_PROFILES not set — skipping multi-gateway launch" + exit 0 +fi + +# Source venv to make 'hermes' available (entrypoint.sh sources it later, +# but we need it NOW for the background gateways) +HERMES_BIN="/opt/hermes/.venv/bin/hermes" +if [ ! -x "$HERMES_BIN" ]; then + echo "ERROR: hermes binary not found at $HERMES_BIN" + exit 1 +fi + +mkdir -p /opt/data/logs + +IFS=',' read -ra PROFILES <<< "${HERMES_PROFILES}" +for profile in "${PROFILES[@]}"; do + profile="$(echo "${profile}" | xargs)" # trim whitespace + [ -z "${profile}" ] && continue + + echo "Starting gateway for profile: ${profile}" + nohup env API_SERVER_ENABLED=false API_SERVER_KEY= gosu hermes "$HERMES_BIN" --profile "${profile}" gateway run \ + >> "/opt/data/logs/gateway-${profile}.log" 2>&1 & +done + +echo "All gateways launched: ${HERMES_PROFILES}"