From b1dbdb9f2d20f8ec2a51c777a1068384053852dd Mon Sep 17 00:00:00 2001 From: Hermes Date: Tue, 19 May 2026 14:13:02 -0400 Subject: [PATCH] feat: add Hermes worker anchor and provisioning script for Paperclip employees - Add x-hermes-worker YAML anchor template in compose.yml (CPU-only workers, no GPU passthrough, OpenCode Go provider) - Add commented worker example with env vars placeholder - Create scripts/provision-hermes-worker.sh for automated worker provisioning (generates port, API key, volume dir, appends service) - Workers connect to Discord only, isolated per container - Volumes under /mnt/HoardingCow_docker_data/Hermes// --- ai/compose.yml | 49 ++++++++++ ai/scripts/provision-hermes-worker.sh | 135 ++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100755 ai/scripts/provision-hermes-worker.sh diff --git a/ai/compose.yml b/ai/compose.yml index aca3347..7c06322 100644 --- a/ai/compose.yml +++ b/ai/compose.yml @@ -1,4 +1,35 @@ version: "3.8" + +# ── Hermes Worker Template ────────────────────────────────── +# Used by paperclip-worker-* Hermes containers via YAML anchor. +# Each worker = one isolated Hermes agent for a Paperclip employee. +# Override at service level: container_name, API_SERVER_PORT, +# API_SERVER_KEY, DISCORD_BOT_TOKEN, volumes. +# Workers have NO GPU — they use OpenCode Go or remote providers. +x-hermes-worker: &hermes-worker + build: + context: ./hermes + ssh: + - default + entrypoint: ["/bin/bash", "-c", + "bash /opt/data/hermes-tools/install.sh && exec /usr/bin/tini -g -- /opt/hermes/docker/entrypoint.sh \"$@\"", + "hermes-entrypoint"] + command: gateway run + restart: always + environment: + API_SERVER_ENABLED: "true" + API_SERVER_HOST: "0.0.0.0" + OLLAMA_HOST: "http://ollama:11434" + OPENROUTER_API_KEY: ${OPENROUTER_API_KEY} + # Each worker needs its own OpenCode Go API key in .env + OPENCODE_GO_API_KEY: ${OPENCODE_GO_API_KEY} + GATEWAY_ALLOW_ALL_USERS: "true" + TZ: "America/Montreal" + networks: + ai_backend: + # NO devices — workers are CPU-only, no GPU passthrough +# ───────────────────────────────────────────────────────────── + services: # webui: @@ -96,6 +127,24 @@ services: - "303" - "26" +# ── Paperclip Worker Hermes Agents ────────────────────────── +# Each worker is an isolated Hermes agent for a Paperclip employee. +# Add new workers with: ./scripts/provision-hermes-worker.sh +# The API server key and port are generated automatically. +# Workers are CPU-only — they use OpenCode Go or remote providers. + + # ── Worker Template (commented — uncomment + configure to activate) ── + # hermes-worker-1: + # <<: *hermes-worker + # container_name: hermes-worker-1 + # environment: + # API_SERVER_PORT: "8651" + # API_SERVER_KEY: "generated-by-provision-script" + # DISCORD_BOT_TOKEN: ${WORKER_1_DISCORD_BOT_TOKEN} + # volumes: + # - /mnt/HoardingCow_docker_data/Hermes/worker-1:/opt/data +# ───────────────────────────────────────────────────────────── + networks: ai_net: external: true diff --git a/ai/scripts/provision-hermes-worker.sh b/ai/scripts/provision-hermes-worker.sh new file mode 100755 index 0000000..4b2918c --- /dev/null +++ b/ai/scripts/provision-hermes-worker.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ── Hermes Worker Provisioner ────────────────────────────── +# Adds a new Paperclip Hermes worker to the ai compose stack. +# +# Usage: +# ./provision-hermes-worker.sh +# +# Example: +# ./provision-hermes-worker.sh worker-1 WORKER_1_DISCORD_BOT_TOKEN +# +# The script APPENDS only — never modifies or removes existing +# content, even commented lines. +# +# Post-provision steps (manual): +# 1. Add secrets to agenix .env file +# 2. systemctl restart ai_stack.service +# 3. Configure Paperclip agent +# ───────────────────────────────────────────────────────────── + +NAME="${1:?Usage: $0 }" +TOKEN_VAR="${2:?Usage: $0 }" + +# ── Paths ─────────────────────────────────────────────────── +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +COMPOSE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +COMPOSE_FILE="${COMPOSE_DIR}/compose.yml" + +# Each Hermes worker gets its own volume on the NFS HoardingCow +VOLUME_BASE="/mnt/HoardingCow_docker_data/Hermes" +VOLUME_DIR="${VOLUME_BASE}/${NAME}" + +# The Hermes container runs as UID 10000 (hermes user from Dockerfile) +HERMES_UID=10000 + +# ── Validation ────────────────────────────────────────────── +if ! [ -f "$COMPOSE_FILE" ]; then + echo "❌ compose.yml not found at $COMPOSE_FILE" + exit 1 +fi + +if grep -q "^ ${NAME}:" "$COMPOSE_FILE"; then + echo "❌ Service '${NAME}' already exists in ${COMPOSE_FILE}" + exit 1 +fi + +# ── Generate unique API key ───────────────────────────────── +# Used by Paperclip to authenticate against this worker's +# Hermes API server (/v1/chat/completions) +API_KEY="pc_worker_$(openssl rand -hex 16)" + +# ── Find next available API port ──────────────────────────── +# Workers get sequential ports starting at 8650. +# Scans compose.yml for existing API_SERVER_PORT values and +# picks the next one. +BASE_PORT=8650 +MAX_PORT=0 +while IFS= read -r line; do + port="${line#*API_SERVER_PORT: \"}" + port="${port%%\"*}" + if [ -n "$port" ] && [ "$port" -gt "$MAX_PORT" ]; then + MAX_PORT="$port" + fi +done < <(grep -oP 'API_SERVER_PORT:\s*"\d+"' "$COMPOSE_FILE" 2>/dev/null) + +NEW_PORT=$((MAX_PORT + 1)) +if [ "$NEW_PORT" -lt "$BASE_PORT" ]; then + NEW_PORT=$BASE_PORT +fi + +# ── Create volume directory (on NFS) ──────────────────────── +echo "📁 Creating volume directory: ${VOLUME_DIR}" +mkdir -p "$VOLUME_DIR" + +# Hermes container runs as UID 10000 — set ownership so the +# container can write its config, sessions, skills +if command -v chown &>/dev/null; then + chown -R "${HERMES_UID}:${HERMES_UID}" "$VOLUME_DIR" 2>/dev/null || \ + echo "⚠ Could not chown ${VOLUME_DIR} — run with sudo if needed" +fi + +# Make it group-readable for debugging +chmod 755 "$VOLUME_DIR" 2>/dev/null || true + +# ── Append service to compose.yml ─────────────────────────── +echo "📝 Appending service '${NAME}' to compose.yml ..." + +TMPFILE=$(mktemp) + +awk -v name="$NAME" \ + -v port="$NEW_PORT" \ + -v api_key="$API_KEY" \ + -v token_var="$TOKEN_VAR" \ + ' + # Insert new worker service block just before the networks: section + /^networks:/ { + print "" + print " " name ":" + print " <<: *hermes-worker" + print " container_name: " name + print " environment:" + print " API_SERVER_PORT: \"" port "\"" + print " API_SERVER_KEY: \"" api_key "\"" + print " DISCORD_BOT_TOKEN: ${" token_var "}" + print " volumes:" + print " - /mnt/HoardingCow_docker_data/Hermes/" name ":/opt/data" + print "" + } + { print } +' "$COMPOSE_FILE" > "$TMPFILE" && mv "$TMPFILE" "$COMPOSE_FILE" + +# ── Done ──────────────────────────────────────────────────── +echo "" +echo "✅ Worker '${NAME}' provisioned successfully" +echo "" +echo "────────────────────────────────────────────" +echo " NEXT STEPS" +echo "────────────────────────────────────────────" +echo "" +echo "1. Add secrets to the agenix .env stack file:" +echo "" +echo " # ${NAME}" +echo " ${TOKEN_VAR}=" +echo "" +echo "2. Restart the AI stack:" +echo "" +echo " systemctl restart ai_stack.service" +echo "" +echo "3. In Paperclip, create an agent with HTTP adapter:" +echo "" +echo " Endpoint: http://${NAME}:${NEW_PORT}/v1/chat/completions" +echo " API Key: ${API_KEY}" +echo "" +echo "────────────────────────────────────────────"