Compare commits

..

2 Commits

Author SHA1 Message Date
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 138 additions and 187 deletions

View File

@@ -54,10 +54,6 @@ services:
- TZ=America/Montreal
volumes:
- /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:
- /dev/kfd:/dev/kfd
- /dev/dri:/dev/dri
@@ -67,35 +63,6 @@ services:
networks:
- ai_backend
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:
build:
context: ./ollama
@@ -129,6 +96,62 @@ services:
- "303"
- "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:
build:
context: ./paperclip
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.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:
ai_net:
external: true

View File

@@ -1,154 +0,0 @@
#!/bin/bash
# ── Hermes Workspace Combined Entrypoint ──
# Waits for the Hermes gateway container (hermes:8642) to become healthy,
# then starts the Hermes Workspace web UI in the foreground.
# Supports graceful shutdown via SIGTERM/SIGINT.
# ──────────────────────────────────────────
set -euo pipefail
# ── Configuration ──────────────────────────────────────────────
GATEWAY_HOST="${GATEWAY_HOST:-hermes}"
GATEWAY_PORT="${GATEWAY_PORT:-8642}"
GATEWAY_URL="http://${GATEWAY_HOST}:${GATEWAY_PORT}"
HEALTH_ENDPOINT="${HEALTH_ENDPOINT:-/health}"
MAX_RETRIES="${HEALTH_MAX_RETRIES:-60}"
RETRY_INTERVAL="${HEALTH_RETRY_INTERVAL:-2}"
WORKSPACE_DIR="${WORKSPACE_DIR:-/workspace}"
WORKSPACE_ENTRY="${WORKSPACE_ENTRY:-server-entry.js}"
PID_FILE="${PID_FILE:-/tmp/workspace.pid}"
# ── Logging ────────────────────────────────────────────────────
log_info() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*"; }
log_warn() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [WARN] $*"; }
log_error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*"; }
# ── Graceful Shutdown ──────────────────────────────────────────
_workspace_pid=""
_shutting_down=false
cleanup() {
if [ "$_shutting_down" = true ]; then
return
fi
_shutting_down=true
log_info "Shutdown signal received, cleaning up..."
# Stop workspace process if running
if [ -n "$_workspace_pid" ] && kill -0 "$_workspace_pid" 2>/dev/null; then
log_info "Stopping workspace (PID: $_workspace_pid)..."
kill -TERM "$_workspace_pid" 2>/dev/null || true
# Give it time to shut down gracefully
local wait_sec=10
while kill -0 "$_workspace_pid" 2>/dev/null && [ "$wait_sec" -gt 0 ]; do
sleep 1
wait_sec=$((wait_sec - 1))
done
# Force kill if still running
if kill -0 "$_workspace_pid" 2>/dev/null; then
log_warn "Workspace did not shut down gracefully, force killing..."
kill -KILL "$_workspace_pid" 2>/dev/null || true
fi
fi
# Clean up PID file
[ -f "$PID_FILE" ] && rm -f "$PID_FILE"
log_info "Shutdown complete."
exit 0
}
# Trap termination signals for graceful shutdown
trap cleanup SIGTERM SIGINT
# ── Gateway Health Check ───────────────────────────────────────
wait_for_gateway() {
local url="${GATEWAY_URL}${HEALTH_ENDPOINT}"
local retries="$MAX_RETRIES"
local interval="$RETRY_INTERVAL"
local attempt=0
log_info "Waiting for Hermes gateway at ${GATEWAY_URL}..."
log_info "Max retries: ${retries}, interval: ${interval}s"
while [ "$attempt" -lt "$retries" ]; do
attempt=$((attempt + 1))
if curl -fsS "${url}" >/dev/null 2>&1; then
log_info "Gateway is healthy after ${attempt} attempt(s) (${GATEWAY_URL})"
return 0
fi
if [ "$attempt" -lt "$retries" ]; then
log_info "Gateway not ready yet (attempt ${attempt}/${retries}), retrying in ${interval}s..."
sleep "$interval"
fi
done
log_error "Gateway did not become healthy after ${retries} attempts ($((retries * interval))s)"
return 1
}
# ── Workspace Startup ──────────────────────────────────────────
start_workspace() {
local entry="${WORKSPACE_DIR}/${WORKSPACE_ENTRY}"
if [ ! -d "$WORKSPACE_DIR" ]; then
log_error "Workspace directory not found: ${WORKSPACE_DIR}"
return 1
fi
if [ ! -f "$entry" ]; then
log_error "Workspace entry point not found: ${entry}"
return 1
fi
log_info "Starting Hermes Workspace web UI..."
log_info " Directory: ${WORKSPACE_DIR}"
log_info " Entry: ${entry}"
cd "$WORKSPACE_DIR"
# Start workspace in background so we can trap signals
node --max-old-space-size=2048 "${entry}" &
_workspace_pid=$!
echo "$_workspace_pid" > "$PID_FILE"
log_info "Workspace started (PID: ${_workspace_pid})"
# Wait for workspace process
wait "$_workspace_pid"
local exit_code=$?
log_info "Workspace exited with code ${exit_code}"
return "$exit_code"
}
# ── Main ───────────────────────────────────────────────────────
main() {
log_info "=== Hermes Workspace Combined Entrypoint ==="
log_info "Gateway: ${GATEWAY_URL}"
log_info "Workspace: ${WORKSPACE_DIR}/${WORKSPACE_ENTRY}"
log_info "PID file: ${PID_FILE}"
# Wait for gateway to be healthy
if ! wait_for_gateway; then
log_warn "Proceeding without confirmed gateway health..."
fi
# Start the workspace
start_workspace
local exit_code=$?
log_info "Entrypoint exiting with code ${exit_code}"
return "$exit_code"
}
# Run main; exit with its return code
main "$@"

47
ai/paperclip/Dockerfile Normal file
View File

@@ -0,0 +1,47 @@
# syntax=docker/dockerfile:1.20
FROM ghcr.io/paperclipai/paperclip:v2026.517.0
# ── Install Hermes adapter npm package into seed directory ──────────
# This seed data gets copied to the persistent volume on first boot
# so the adapter is available without network access.
USER root
RUN npm install --no-save --prefix /opt/paperclip-seed/adapter-plugins \
hermes-paperclip-adapter@0.3.0
# Create adapter-plugins.json metadata (Paperclip reads this on startup
# to discover which external adapters to load)
RUN mkdir -p /opt/paperclip-seed && python3 -c "
import json
record = {
'packageName': 'hermes-paperclip-adapter',
'version': '0.3.0',
'type': 'hermes',
'installedAt': '2026-05-18T00:00:00.000Z',
}
with open('/opt/paperclip-seed/adapter-plugins.json', 'w') as f:
json.dump([record], f, indent=2)
"
# Ensure the adapter-plugins dir has a package.json (Paperclip expects one)
RUN python3 -c "
import json
pkg = {
'name': 'paperclip-adapter-plugins',
'version': '0.0.0',
'private': True,
'description': 'Managed directory for Paperclip external adapter plugins.',
}
with open('/opt/paperclip-seed/adapter-plugins/package.json', 'w') as f:
json.dump(pkg, f, indent=2)
"
# ── Custom entrypoint ──────────────────────────────────────────────
# Seeds the Hermes adapter on fresh volumes, then runs original logic.
COPY docker-entrypoint.sh /usr/local/bin/paperclip-entrypoint.sh
RUN chmod +x /usr/local/bin/paperclip-entrypoint.sh
USER node
ENTRYPOINT ["/usr/local/bin/paperclip-entrypoint.sh"]
CMD ["node", "--import", "./server/node_modules/tsx/dist/loader.mjs", "server/dist/index.js"]

View File

@@ -0,0 +1,35 @@
#!/bin/sh
set -e
# ── Seed Hermes adapter if volume is fresh ──────────────────────────
PAPERCLIP_HOME="${PAPERCLIP_HOME:-/paperclip}"
if [ ! -f "${PAPERCLIP_HOME}/adapter-plugins.json" ]; then
echo "[paperclip] Seeding Hermes adapter plugin..."
cp -r /opt/paperclip-seed/* "${PAPERCLIP_HOME}/"
chown -R "${USER_UID:-1000}:${USER_GID:-1000}" \
"${PAPERCLIP_HOME}/adapter-plugins" \
"${PAPERCLIP_HOME}/adapter-plugins.json"
echo "[paperclip] Hermes adapter seeded. Ready to create Hermes agents."
fi
# ── Original entrypoint logic (UID/GID adjustment) ──────────────────
PUID="${USER_UID:-1000}"
PGID="${USER_GID:-1000}"
changed=0
if [ "$(id -u node)" -ne "$PUID" ]; then
usermod -o -u "$PUID" node
changed=1
fi
if [ "$(id -g node)" -ne "$PGID" ]; then
groupmod -o -g "$PGID" node
usermod -g "$PGID" node
changed=1
fi
if [ "$changed" = "1" ]; then
chown -R node:node "${PAPERCLIP_HOME}"
fi
exec gosu node "$@"