Compare commits

..

2 Commits

5 changed files with 119 additions and 47 deletions

33
ai/Dockerfile Normal file
View File

@@ -0,0 +1,33 @@
FROM debian:13.4
# Install system dependencies for Hermes agent with headless browser automation
# Single RUN layer to minimize image size.
# PR #7 base: curl, poppler-utils, imagemagick for document processing
# New: chromium + xvfb for headless browser automation
# New: fonts for comprehensive Unicode/CJK/emoji coverage
# New: runtime libs required by Chromium
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
poppler-utils \
imagemagick \
chromium \
xvfb \
fonts-noto-color-emoji fonts-unifont fonts-liberation fonts-ipafont-gothic fonts-wqy-zenhei fonts-tlwg-loma-otf fonts-freefont-ttf \
libasound2t64 libatk-bridge2.0-0t64 libatk1.0-0t64 libatspi2.0-0t64 libcairo2 libcups2t64 libdbus-1-3 libdrm2 libgbm1 libglib2.0-0t64 libnspr4 libnss3 libpango-1.0-0 libx11-6 libxcb1 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 && \
rm -rf /var/lib/apt/lists/*
# Install uv (Python package manager)
COPY --from=ghcr.io/astral-sh/uv:latest /usr/local/bin/uv /usr/local/bin/uv
RUN uv --version
# Verify all expected tools are available
RUN curl --version && \
pdftotext -v 2>&1 | head -1 && \
pdfinfo -v 2>&1 | head -1 && \
pdftoppm -v 2>&1 | head -1 && \
convert --version | head -1 && \
identify --version | head -1 && \
chromium --version
CMD ["/bin/bash"]

View File

@@ -31,27 +31,14 @@ services:
ssh: ssh:
- default - default
container_name: hermes container_name: hermes
# Install Matrix bridge deps (mautrix[encryption]) + openai into the venv at startup.
# Ensures the venv is usable even on first boot with empty persistent volume.
entrypoint: ["/bin/bash", "-c", entrypoint: ["/bin/bash", "-c",
"bash /opt/data/hermes-tools/install.sh && \ "bash /opt/data/hermes-tools/install.sh && exec /usr/bin/tini -g -- /opt/hermes/docker/entrypoint.sh \"$@\"",
if [ ! -f /opt/hermes/.venv/bin/python3 ]; then \
uv venv /opt/hermes/.venv --seed && \
. /opt/hermes/.venv/bin/activate && \
uv pip install --no-cache-dir --no-deps -e /opt/hermes && \
uv pip install --no-cache-dir piper-tts sounddevice numpy; \
else \
. /opt/hermes/.venv/bin/activate; \
fi && \
uv pip install openai 'mautrix[encryption]' -q && \
exec /usr/bin/tini -g -- /opt/hermes/docker/entrypoint.sh \"$@\"",
"hermes-entrypoint"] "hermes-entrypoint"]
restart: always restart: always
# Gateway run enables the internal API server on port 8642 # Gateway run enables the internal API server on port 8642
command: gateway run command: gateway run
environment: environment:
- OLLAMA_HOST=http://ollama:11434 - OLLAMA_HOST=http://ollama:11434
- HERMES_DASHBOARD=1
- API_SERVER_ENABLED=true - API_SERVER_ENABLED=true
- API_SERVER_PORT=8642 - API_SERVER_PORT=8642
- API_SERVER_HOST=0.0.0.0 - API_SERVER_HOST=0.0.0.0
@@ -71,8 +58,6 @@ services:
- /mnt/HoardingCow_docker_data/Syncthing/telos-ro:/opt/data/telos-ro:ro - /mnt/HoardingCow_docker_data/Syncthing/telos-ro:/opt/data/telos-ro:ro
# Syncthing-shared inbox — write tasks here, they sync to user's laptop # Syncthing-shared inbox — write tasks here, they sync to user's laptop
- /mnt/HoardingCow_docker_data/Syncthing/telos-rw:/opt/data/telos-rw:rw - /mnt/HoardingCow_docker_data/Syncthing/telos-rw:/opt/data/telos-rw:rw
# Persist Python venv across container recreation (Matrix bridge deps, etc.)
- /mnt/HoardingCow_docker_data/Hermes/venv:/opt/hermes/.venv
devices: devices:
- /dev/kfd:/dev/kfd - /dev/kfd:/dev/kfd
- /dev/dri:/dev/dri - /dev/dri:/dev/dri
@@ -81,30 +66,6 @@ services:
- "26" - "26"
networks: networks:
- ai_backend - 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: syncthing:
image: syncthing/syncthing:latest image: syncthing/syncthing:latest

View File

@@ -20,10 +20,16 @@ RUN --mount=type=ssh \
GIT_SSH_COMMAND='ssh -p 2222 -o StrictHostKeyChecking=no' \ GIT_SSH_COMMAND='ssh -p 2222 -o StrictHostKeyChecking=no' \
git clone --depth 1 --branch main \ git clone --depth 1 --branch main \
git@code.lazyworkhorse.net:gortium/hermes-agent.git fork && \ git@code.lazyworkhorse.net:gortium/hermes-agent.git fork && \
rm -rf fork/node_modules fork/.venv fork/.git && \ rsync -a --delete fork/ /opt/hermes/ \
cp -a fork/. /opt/hermes/ && \ --exclude node_modules \
--exclude .venv \
--exclude .git && \
rm -rf /tmp/fork /root/.ssh/ 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) ---------- # ---------- Reinstall Python package (editable) ----------
# Picks up source changes from our fork. # Picks up source changes from our fork.
RUN . /opt/hermes/.venv/bin/activate && \ RUN . /opt/hermes/.venv/bin/activate && \
@@ -69,6 +75,10 @@ os.remove(tgz)
print('himalaya v1.2.0 installed') print('himalaya v1.2.0 installed')
PYEOF PYEOF
# ---------- Install himalaya-ro wrapper ----------
COPY --chmod=0755 himalaya-ro.sh /usr/local/bin/himalaya-ro
# ---------- Runtime ---------- # ---------- Runtime ----------
USER hermes USER hermes
ENV HERMES_HOME=/opt/data ENV HERMES_HOME=/opt/data
@@ -78,7 +88,6 @@ ENV CHROME_EXECUTABLE=/opt/hermes/.playwright/chromium/chrome-linux/chrome
# Ensure tools directory and toolsets.py are writable by the hermes runtime user # 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. # 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 RUN chown -R hermes:hermes /opt/hermes/tools /opt/hermes/toolsets.py
VOLUME [ "/opt/data" ] VOLUME [ "/opt/data" ]

73
ai/hermes/himalaya-ro.sh Normal file
View File

@@ -0,0 +1,73 @@
#!/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] <command> [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" "$@"

View File

@@ -1,4 +0,0 @@
#!/bin/bash
# This file was replaced by inline entrypoint logic in compose.yml
# See ai/compose.yml hermes.entrypoint for the current implementation.
echo "startup.sh is obsolete — logic is inline in compose.yml entrypoint"