Compare commits

..

4 Commits

Author SHA1 Message Date
1eacc3cd8e fix(paperclip): force Traefik to use ai_net network for routing
Some checks failed
Build Hermes agent / build (pull_request) Has been cancelled
Build ollama (gfx906) / build (pull_request) Has been cancelled
2026-05-18 22:13:59 -04:00
bce403232a Revert custom Dockerfile - not needed, adapter installs on persistent volume
Some checks failed
Build Hermes agent / build (pull_request) Has been cancelled
Build ollama (gfx906) / build (pull_request) Has been cancelled
The Hermes adapter can be installed once via Paperclip's adapter
management API and persists on the Docker volume across restarts.
No custom Dockerfile or build step required.
2026-05-18 18:38:17 -04:00
37bf43c3ea feat: add custom Dockerfile with Hermes adapter baked in
Some checks failed
Build Hermes agent / build (pull_request) Has been cancelled
Build ollama (gfx906) / build (pull_request) Has been cancelled
Creates ai/paperclip/ with:
- Dockerfile: extends upstream paperclip image, pre-installs
  hermes-paperclip-adapter@0.3.0 npm package as seed data
- docker-entrypoint.sh: seeds the adapter plugin on first boot
  if the persistent volume is empty, then runs original startup

This ensures the Hermes adapter is available on first boot without
requiring network access — no npm install needed at runtime. The
adapter persists on the Docker volume across restarts.
2026-05-18 18:37:31 -04:00
563ccc5632 feat: add Paperclip agent orchestrator to AI compose stack
Some checks failed
Build Hermes agent / build (pull_request) Has been cancelled
Build ollama (gfx906) / build (pull_request) Has been cancelled
Paperclip (ghcr.io/paperclipai/paperclip:v2026.517.0) is an open-source
agent management dashboard. Adds paperclip-db (PostgreSQL 17) and
paperclip services with Traefik reverse proxy on
paperclip.lazyworkhorse.net.

Requires .env: PAPERCLIP_DB_PASSWORD, PAPERCLIP_AUTH_SECRET.
2026-05-18 18:17:15 -04:00
4 changed files with 142 additions and 110 deletions

120
ai/compose.yml Executable file → Normal file
View File

@@ -32,18 +32,13 @@ services:
- default - default
container_name: hermes container_name: hermes
entrypoint: ["/bin/bash", "-c", entrypoint: ["/bin/bash", "-c",
"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 \"$@\"", "bash /opt/data/hermes-tools/install.sh && 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
# 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_ENABLED=true
- API_SERVER_PORT=8642 - API_SERVER_PORT=8642
- API_SERVER_HOST=0.0.0.0 - API_SERVER_HOST=0.0.0.0
@@ -59,10 +54,6 @@ services:
- TZ=America/Montreal - TZ=America/Montreal
volumes: volumes:
- /mnt/HoardingCow_docker_data/Hermes/data:/opt/data - /mnt/HoardingCow_docker_data/Hermes/data:/opt/data
# Syncthing-shared org files — read-only view of user's agenda
- /mnt/HoardingCow_docker_data/Syncthing/telos-ro:/opt/data/telos-ro:ro
# Syncthing-shared inbox — write tasks here, they sync to user's laptop
- /mnt/HoardingCow_docker_data/Syncthing/telos-rw:/opt/data/telos-rw:rw
devices: devices:
- /dev/kfd:/dev/kfd - /dev/kfd:/dev/kfd
- /dev/dri:/dev/dri - /dev/dri:/dev/dri
@@ -71,59 +62,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:
image: syncthing/syncthing:latest
container_name: syncthing
hostname: syncthing
restart: always
ports:
- "8384:8384"
- "22000:22000"
- "21027:21027/udp"
environment:
- TZ=America/Montreal
volumes:
- /mnt/HoardingCow_docker_data/Syncthing/config:/var/syncthing/config
- /mnt/HoardingCow_docker_data/Syncthing/telos-ro:/telos-ro
- /mnt/HoardingCow_docker_data/Syncthing/telos-rw:/telos-rw
networks:
- ai_backend
- ai_net
labels:
- "traefik.enable=true"
- "traefik.http.routers.syncthing-http.rule=Host(`syncthing.lazyworkhorse.net`)"
- "traefik.http.routers.syncthing-http.entrypoints=web"
- "traefik.http.routers.syncthing-http.middlewares=redirect-to-https"
- "traefik.http.routers.syncthing-https.rule=Host(`syncthing.lazyworkhorse.net`)"
- "traefik.http.routers.syncthing-https.entrypoints=websecure"
- "traefik.http.routers.syncthing-https.tls=true"
- "traefik.http.routers.syncthing-https.tls.certresolver=njalla"
- "traefik.http.services.syncthing.loadbalancer.server.port=8384"
ollama: ollama:
build: build:
@@ -158,6 +96,62 @@ services:
- "303" - "303"
- "26" - "26"
paperclip-db:
image: postgres:17-alpine
container_name: paperclip-db
restart: always
environment:
POSTGRES_USER: paperclip
POSTGRES_PASSWORD: ${PAPERCLIP_DB_PASSWORD:?PAPERCLIP_DB_PASSWORD must be set}
POSTGRES_DB: paperclip
healthcheck:
test: ["CMD-SHELL", "pg_isready -U paperclip -d paperclip"]
interval: 5s
timeout: 5s
retries: 10
volumes:
- /mnt/HoardingCow_docker_data/Paperclip/pgdata:/var/lib/postgresql/data
networks:
- ai_backend
paperclip:
image: ghcr.io/paperclipai/paperclip:v2026.517.0
container_name: paperclip
restart: always
ports:
- "127.0.0.1:3100:3100"
environment:
- HOST=0.0.0.0
- PORT=3100
- SERVE_UI=true
- DATABASE_URL=postgres://paperclip:${PAPERCLIP_DB_PASSWORD}@paperclip-db:5432/paperclip
- BETTER_AUTH_SECRET=${PAPERCLIP_AUTH_SECRET:?PAPERCLIP_AUTH_SECRET must be set}
- PAPERCLIP_PUBLIC_URL=https://paperclip.lazyworkhorse.net
- PAPERCLIP_DEPLOYMENT_MODE=authenticated
- PAPERCLIP_DEPLOYMENT_EXPOSURE=private
volumes:
- /mnt/HoardingCow_docker_data/Paperclip/data:/paperclip
depends_on:
paperclip-db:
condition: service_healthy
networks:
- ai_net
- ai_backend
labels:
- "traefik.enable=true"
- "traefik.docker.network=ai_net"
- "traefik.http.routers.paperclip-http.rule=Host(`paperclip.lazyworkhorse.net`)"
- "traefik.http.routers.paperclip-http.entrypoints=web"
- "traefik.http.routers.paperclip-http.middlewares=redirect-to-https"
- "traefik.http.routers.paperclip-https.rule=Host(`paperclip.lazyworkhorse.net`)"
- "traefik.http.routers.paperclip-https.entrypoints=websecure"
- "traefik.http.routers.paperclip-https.tls=true"
- "traefik.http.routers.paperclip-https.tls.certresolver=njalla"
- "traefik.http.services.paperclip.loadbalancer.server.port=3100"
networks: networks:
ai_net: ai_net:
external: true external: true

View File

@@ -18,13 +18,18 @@ RUN --mount=type=ssh \
ssh-keyscan -p 2222 code.lazyworkhorse.net >> /root/.ssh/known_hosts 2>/dev/null && \ ssh-keyscan -p 2222 code.lazyworkhorse.net >> /root/.ssh/known_hosts 2>/dev/null && \
cd /tmp && \ cd /tmp && \
GIT_SSH_COMMAND='ssh -p 2222 -o StrictHostKeyChecking=no' \ GIT_SSH_COMMAND='ssh -p 2222 -o StrictHostKeyChecking=no' \
# Branch feat/drop-multi-memory: upstream + Piper TTS patch only (no multi-memory patch) git clone --depth 1 --branch main \
git clone --depth 1 --branch feat/drop-multi-memory \
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 && \
@@ -35,7 +40,6 @@ USER root
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
libportaudio2 ca-certificates poppler-utils imagemagick \ libportaudio2 ca-certificates poppler-utils imagemagick \
libolm-dev \
texlive-latex-base texlive-latex-extra texlive-fonts-recommended \ texlive-latex-base texlive-latex-extra texlive-fonts-recommended \
texlive-xetex texlive-science \ texlive-xetex texlive-science \
qemu-user-static binfmt-support emacs-nox && \ qemu-user-static binfmt-support emacs-nox && \
@@ -44,12 +48,6 @@ RUN apt-get update && \
# ---------- UV ---------- # ---------- UV ----------
COPY --chmod=0755 --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/ COPY --chmod=0755 --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/
# ---------- Matrix bridge + extra pip deps ----------
# Previously installed inline at container startup and persisted via volume mount.
# Now baked into the image so the fragile venv volume mount can be removed.
RUN . /opt/hermes/.venv/bin/activate && \
uv pip install --no-cache-dir 'mautrix[encryption]' openai
# ---------- Piper TTS ---------- # ---------- Piper TTS ----------
RUN . /opt/hermes/.venv/bin/activate && \ RUN . /opt/hermes/.venv/bin/activate && \
uv pip install --no-cache-dir piper-tts sounddevice numpy && \ uv pip install --no-cache-dir piper-tts sounddevice numpy && \
@@ -77,9 +75,9 @@ os.remove(tgz)
print('himalaya v1.2.0 installed') print('himalaya v1.2.0 installed')
PYEOF PYEOF
# ---------- Install multi-gateway launcher ---------- # ---------- Install himalaya-ro wrapper ----------
# Launches one gateway process per profile (HERMES_PROFILES env var) COPY --chmod=0755 himalaya-ro.sh /usr/local/bin/himalaya-ro
COPY --chmod=0755 run-multi-gateways.sh /usr/local/bin/run-multi-gateways.sh
# ---------- Runtime ---------- # ---------- Runtime ----------
USER hermes USER hermes
@@ -90,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,32 +0,0 @@
#!/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}"