From 8e0d85b7f8b8d4180ae09429736c48b0260525c0 Mon Sep 17 00:00:00 2001 From: biondizzle Date: Thu, 23 Apr 2026 21:07:49 +0000 Subject: [PATCH] shove into git --- Jenkinsfile | 77 ++++++ README.md | 132 ++++++++++ hermes.pkr.hcl | 91 +++++++ install-hermes.sh | 567 ++++++++++++++++++++++++++++++++++++++++ scripts/cloud-init.yaml | 167 ++++++++++++ scripts/provision.sh | 194 ++++++++++++++ 6 files changed, 1228 insertions(+) create mode 100644 Jenkinsfile create mode 100644 README.md create mode 100644 hermes.pkr.hcl create mode 100755 install-hermes.sh create mode 100644 scripts/cloud-init.yaml create mode 100755 scripts/provision.sh diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..7d2a8dd --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,77 @@ +pipeline { + agent any + + parameters { + string(name: 'HERMES_VERSION', defaultValue: 'v0.7.0', description: 'Hermes Agent version to install (e.g. v0.7.0)') + string(name: 'VULTR_REGION', defaultValue: 'ewr', description: 'Vultr region for the build instance') + string(name: 'VULTR_PLAN', defaultValue: 'vc2-2c-4gb', description: 'Vultr plan for the build instance') + string(name: 'VULTR_OS_ID', defaultValue: '2284', description: 'Vultr OS ID (2284 = Ubuntu 24.04 LTS)') + } + + stages { + + stage('Validate') { + steps { + script { + if (!params.HERMES_VERSION?.trim()) { + error "HERMES_VERSION parameter is required." + } + } + } + } + + stage('Checkout') { + steps { + checkout([ + $class: 'GitSCM', + branches: [[name: '*/master']], + userRemoteConfigs: [[ + url: 'https://git.vultropenclaw.com/vultr/hermes-agent-marketplace-packer.git' + ]] + ]) + } + } + + stage('Check Packer') { + steps { + sh 'packer --version' + } + } + + stage('Packer Init') { + steps { + sh 'packer init hermes.pkr.hcl' + } + } + + stage('Packer Build') { + steps { + withCredentials([string( + credentialsId: 'VULTR_API_KEY', + variable: 'VULTR_API_KEY' + )]) { + sh """ + packer build \ + -var 'vultr_api_key=${VULTR_API_KEY}' \ + -var 'hermes_version=${params.HERMES_VERSION}' \ + -var 'region_id=${params.VULTR_REGION}' \ + -var 'plan_id=${params.VULTR_PLAN}' \ + -var 'os_id=${params.VULTR_OS_ID}' \ + -color=false \ + hermes.pkr.hcl + """ + } + } + } + } + + post { + success { + echo "✅ Snapshot built for Hermes Agent ${params.HERMES_VERSION}" + echo "✅ Region: ${params.VULTR_REGION} Plan: ${params.VULTR_PLAN}" + } + failure { + echo "❌ Packer build failed for Hermes Agent ${params.HERMES_VERSION}" + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..5a295f5 --- /dev/null +++ b/README.md @@ -0,0 +1,132 @@ +# Hermes Agent Vultr Marketplace — Packer Image Build + +One-shot install for Hermes Agent on Ubuntu 24.04 with Vultr Inference, ttyd, code-server, Docker, Caddy, and Homebrew. + +## Architecture + +Bare-metal install via Packer → Vultr snapshot. + +### File layout + +``` +├── Jenkinsfile # Pipeline: validate → install packer → init → build +├── hermes.pkr.hcl # Packer HCL template (Vultr plugin) +├── scripts/ +│ ├── provision.sh # Bake-time: system deps, hermes, docker, caddy, ttyd +│ └── cloud-init.yaml # Deploy-time: metadata fetch, config, caddy TLS +├── install-hermes.sh # Standalone installer (for manual deploys) +└── README.md +``` + +### Bake-time (Packer snapshot) + +Everything slow and static: +- `apt` packages (build-essential, curl, git, jq, python3-venv, zsh, ttyd, …) +- Hermes Agent from GitHub (v0.7.0) +- Docker CE +- Caddy (disabled until configured) +- code-server +- Oh My Zsh + Homebrew +- UFW with ssh/http/https/7681/8080 allowed +- Dedicated `hermes` system user with `~/.hermes/` pre-created +- ttyd and code-server systemd services created + +### Deploy-time (cloud-init) + +Per-instance secrets and activation: +1. Fetch `app-password`, `app-inf_api_key`, `app-domain` from Vultr metadata +2. Write `/home/hermes/.hermes/.env` with Vultr Inference API key +3. Write `/home/hermes/.hermes/config.yaml` with model/provider config +4. Configure code-server with password +5. Start ttyd and code-server services +6. Write `/etc/caddy/Caddyfile` with domain, ZeroSSL TLS, basic auth +7. Start Caddy and wait for cert issuance +8. Shred the temp env file + +### Access URLs (after deploy) + +- **Hermes Terminal:** `https://your-domain.com/` (via ttyd) +- **VS Code:** `https://your-domain.com/code/` +- **Basic Auth:** Username `hermes`, password from Vultr metadata + +### Jenkins parameters + +| Parameter | Default | Description | +|---|---|---| +| `HERMES_VERSION` | `v0.7.0` | Hermes Agent git tag | +| `VULTR_REGION` | `ewr` | Build region | +| `VULTR_PLAN` | `vc2-2c-4gb` | Build instance size | +| `VULTR_OS_ID` | `2284` | Ubuntu 24.04 LTS | + +### Credentials needed in Jenkins + +| Credential ID | Type | Purpose | +|---|---|---| +| `VULTR_API_KEY` | Secret text | Vultr API key for Packer to provision + snapshot | + +### Vultr Marketplace Metadata Variables + +Configure these in the Vultr marketplace app: + +| Variable | Description | +|----------|-------------| +| `app-password` | Password for basic auth and code-server | +| `app-inf_api_key` | Vultr Inference API key | +| `app-domain` | Domain for TLS certificate | + +## Test VMs + +### VM 1 (Basic - Manual Install) +``` +Host: 137.220.58.94 +User: root +Password: Uv!3XrMsq!45yQe* +``` + +### VM 2 (Full Dev + Caddy) +``` +Host: 66.135.26.126 +User: root +Password: 7_kGSd)5Jr%iK?M- +Domain: hermes-dev-001.vultropenclaw.com +``` + +## Available Models (Vultr Inference) + +| Model ID | Context | Notes | +|----------|---------|-------| +| `zai-org/GLM-5-FP8` | 200K | Default, reasoning-capable | +| `deepseek-ai/DeepSeek-V3.2` | 128K | Reasoning-capable | +| `google/gemma-4-31B-it` | 256K | Reasoning-capable | +| `Qwen/Qwen2.5-Coder-32B-Instruct` | 131K | Code-focused | +| `MiniMaxAI/MiniMax-M2.5` | 128K | General purpose | +| `moonshotai/Kimi-K2.5` | 128K | General purpose | + +## Manual Installation + +For manual deploys without Packer, use the standalone installer: + +```bash +# Without domain (HTTP only) +curl -fsSL https://raw.githubusercontent.com/.../install-hermes.sh | bash + +# With domain (HTTPS via Caddy) +curl -fsSL https://raw.githubusercontent.com/.../install-hermes.sh | bash -s -- your-domain.com +``` + +## Services + +| Service | Port | Description | +|---------|------|-------------| +| `ttyd-hermes` | 7681 | Hermes terminal (web) | +| `code-server-hermes` | 8080 | VS Code (web) | +| `caddy` | 80/443 | Reverse proxy with auto-HTTPS | +| `docker` | - | Container runtime | + +## Notes + +- ttyd runs as root, drops privileges to hermes user via `-u`/`-g` flags +- code-server runs as hermes user +- Homebrew installed at `/home/linuxbrew/.linuxbrew` +- `HOME` and `PATH` environment variables set in systemd services for brew compatibility +- Caddyfile uses ZeroSSL primary (avoids LE rate limits), Let's Encrypt fallback diff --git a/hermes.pkr.hcl b/hermes.pkr.hcl new file mode 100644 index 0000000..79a76b4 --- /dev/null +++ b/hermes.pkr.hcl @@ -0,0 +1,91 @@ +variable "vultr_api_key" { + type = string + default = env("VULTR_API_KEY") + sensitive = true +} + +variable "hermes_version" { + type = string + default = "v0.7.0" + description = "Hermes Agent version to install (e.g. v0.7.0)" +} + +variable "os_id" { + type = string + default = "2284" # Ubuntu 24.04 LTS x64 + description = "Vultr OS ID" +} + +variable "plan_id" { + type = string + default = "vc2-2c-4gb" # 2 vCPU / 4 GB — adjust to your marketplace tier +} + +variable "region_id" { + type = string + default = "ewr" # New Jersey +} + +packer { + required_plugins { + vultr = { + version = ">=v2.3.2" + source = "github.com/vultr/vultr" + } + } +} + +source "vultr" "hermes" { + api_key = var.vultr_api_key + os_id = var.os_id + plan_id = var.plan_id + region_id = var.region_id + snapshot_description = "hermes-agent ${var.hermes_version} ${formatdate("YYYY-MM-DD-hhmm", timestamp())}" + ssh_username = "root" + state_timeout = "25m" +} + +build { + sources = ["source.vultr.hermes"] + + # ── Bake-time provisioning ── + provisioner "shell" { + environment_vars = [ + "DEBIAN_FRONTEND=noninteractive", + "HERMES_VERSION=${var.hermes_version}", + ] + script = "scripts/provision.sh" + } + + # ── Bake cloud-init into the image so it runs on first boot ── + provisioner "file" { + source = "scripts/cloud-init.yaml" + destination = "/etc/cloud/cloud.cfg.d/99-hermes.cfg" + } + + # ── Vultr marketplace snapshot prep (must be last) ── + provisioner "shell" { + inline = [ + "rm -rf /tmp/* /var/tmp/*", + "rm -f /root/.ssh/authorized_keys /etc/ssh/*key*", + "touch /etc/ssh/revoked_keys", + "chmod 600 /etc/ssh/revoked_keys", + "find /var/log -mtime -1 -type f -exec truncate -s 0 {} \\;", + "rm -rf /var/log/*.gz /var/log/*.[0-9] /var/log/*-????????", + "echo '' > /var/log/auth.log", + "cat /dev/null > /var/log/lastlog", + "cat /dev/null > /var/log/wtmp", + "rm -rf /var/lib/cloud/instances/*", + "rm -f /var/lib/systemd/random-seed", + "rm -f /etc/machine-id", + "touch /etc/machine-id", + "cat /dev/null > /root/.bash_history", + "updatedb 2>/dev/null || true", + "dd if=/dev/zero of=/zerofile bs=1M 2>/dev/null || true", + "sync", + "rm -f /zerofile", + "sync", + "fstrim / 2>/dev/null || true", + ] + } +} diff --git a/install-hermes.sh b/install-hermes.sh new file mode 100755 index 0000000..ce7bcfd --- /dev/null +++ b/install-hermes.sh @@ -0,0 +1,567 @@ +#!/bin/bash +# ============================================================================ +# Hermes Agent Full Installer for Ubuntu 24.04 +# ============================================================================ +# Installs Hermes Agent with Vultr Inference provider pre-configured, +# plus Docker, zsh, Oh My Zsh, Homebrew, code-server, Caddy, ttyd, and UFW. +# +# Usage: +# ./install-hermes.sh [DOMAIN] +# +# If DOMAIN is provided, Caddy will be configured with HTTPS for that domain. +# +# ============================================================================ + +set -e + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +NC='\033[0m' +BOLD='\033[1m' + +# Configuration - Vultr Inference +VULTR_API_KEY="55AIOTBABUBU7PAZRZEKORCG4T2H2VVZQ3XQ" +VULTR_BASE_URL="https://api.vultrinference.com/v1" +DEFAULT_MODEL="zai-org/GLM-5-FP8" +TTYD_PORT="${TTYD_PORT:-7681}" +CODE_SERVER_PORT="${CODE_SERVER_PORT:-8080}" + +# Domain for Caddy (optional) +DOMAIN="${1:-}" + +# ============================================================================ +# Helper functions +# ============================================================================ + +log_info() { + echo -e "${CYAN}→${NC} $1" +} + +log_success() { + echo -e "${GREEN}✓${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}⚠${NC} $1" +} + +log_error() { + echo -e "${RED}✗${NC} $1" +} + +print_banner() { + echo "" + echo -e "${CYAN}${BOLD}" + echo "┌─────────────────────────────────────────────────────────┐" + echo "│ Hermes Agent + Dev Environment Installer │" + echo "└─────────────────────────────────────────────────────────┘" + echo -e "${NC}" +} + +# ============================================================================ +# Main installation +# ============================================================================ + +main() { + print_banner + + # Check for Ubuntu/Debian + if [ -f /etc/os-release ]; then + . /etc/os-release + if [ "$ID" != "ubuntu" ] && [ "$ID" != "debian" ]; then + log_warn "This script is designed for Ubuntu/Debian. Proceeding anyway..." + fi + fi + + # Ensure we're running as root or have sudo + if [ "$(id -u)" -ne 0 ] && ! command -v sudo &> /dev/null; then + log_error "This script requires root access or sudo" + exit 1 + fi + + # Run as root if we're not already + if [ "$(id -u)" -ne 0 ]; then + log_info "Requesting sudo for system package installation..." + SUDO="sudo" + else + SUDO="" + fi + + export DEBIAN_FRONTEND=noninteractive + + # ------------------------------------------------------------------------- + # Step 1: Install system dependencies + # ------------------------------------------------------------------------- + log_info "Installing system packages..." + + $SUDO apt-get update -qq + $SUDO apt-get install -y -qq \ + build-essential procps curl file git ca-certificates \ + gnupg software-properties-common ufw jq python3 python3-venv \ + zsh docker.io ttyd ri rgrep ffmpeg > /dev/null 2>&1 || true + + log_success "System packages installed" + + # ------------------------------------------------------------------------- + # Step 2: Install Docker (official repo) + # ------------------------------------------------------------------------- + log_info "Installing Docker..." + + if ! command -v docker &> /dev/null; then + install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc + chmod a+r /etc/apt/keyrings/docker.asc + + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \ + https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \ + | tee /etc/apt/sources.list.d/docker.list > /dev/null + + $SUDO apt-get update -qq + $SUDO apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + systemctl enable docker + systemctl start docker + fi + + log_success "Docker installed ($(docker --version))" + + # ------------------------------------------------------------------------- + # Step 3: Install Caddy + # ------------------------------------------------------------------------- + log_info "Installing Caddy..." + + if ! command -v caddy &> /dev/null; then + curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \ + | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg + curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \ + | tee /etc/apt/sources.list.d/caddy-stable.list + chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg + chmod o+r /etc/apt/sources.list.d/caddy-stable.list + + $SUDO apt-get update -qq + $SUDO apt-get install -y -qq caddy + fi + + log_success "Caddy installed ($(caddy version | head -1))" + + # ------------------------------------------------------------------------- + # Step 4: Install code-server + # ------------------------------------------------------------------------- + log_info "Installing code-server..." + + if ! command -v code-server &> /dev/null; then + curl -fsSL https://code-server.dev/install.sh | sh + fi + + log_success "code-server installed ($(code-server --version))" + + # ------------------------------------------------------------------------- + # Step 5: Create hermes user + # ------------------------------------------------------------------------- + log_info "Creating hermes user..." + + if ! id hermes &>/dev/null; then + useradd -m -s /usr/bin/zsh hermes + else + chsh -s /usr/bin/zsh hermes 2>/dev/null || true + fi + + # Create ttyd group and add hermes to it + groupadd ttyd 2>/dev/null || true + usermod -aG ttyd hermes + usermod -aG docker hermes + + # Allow hermes user's systemd services to run without an active session + loginctl enable-linger hermes 2>/dev/null || true + + log_success "Hermes user created (uid=$(id -u hermes))" + + # ------------------------------------------------------------------------- + # Step 6: Install Oh My Zsh (as hermes user) + # ------------------------------------------------------------------------- + log_info "Installing Oh My Zsh..." + + if [ ! -d /home/hermes/.oh-my-zsh ]; then + su - hermes -c 'sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended' + fi + + log_success "Oh My Zsh installed" + + # ------------------------------------------------------------------------- + # Step 7: Install Homebrew (as hermes user) + # ------------------------------------------------------------------------- + log_info "Installing Homebrew..." + + if [ ! -d /home/linuxbrew/.linuxbrew ]; then + # Allow hermes to sudo without password during brew install + echo 'hermes ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/hermes + su - hermes -c 'NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' + + # Persist Homebrew in all shells + cat >> /home/hermes/.bashrc <<'BREWEOF' +eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" +BREWEOF + cat >> /home/hermes/.profile <<'BREWEOF' +eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" +BREWEOF + cat >> /home/hermes/.zshrc <<'BREWEOF' +eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" +BREWEOF + + # Turn off analytics + su - hermes -c 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" && brew analytics off' + fi + + log_success "Homebrew installed" + + # ------------------------------------------------------------------------- + # Step 8: Clone and install Hermes Agent + # ------------------------------------------------------------------------- + log_info "Installing Hermes Agent..." + + HERMES_HOME="/home/hermes/.hermes" + HERMES_LOCAL="/home/hermes/.local" + + # Ensure directories exist with correct ownership + mkdir -p "$HERMES_LOCAL/bin" + chown -R hermes:hermes /home/hermes + + if [ ! -d "$HERMES_HOME/hermes-agent" ]; then + su - hermes -c "git clone https://github.com/NousResearch/hermes-agent.git $HERMES_HOME/hermes-agent" + fi + + log_success "Hermes Agent source cloned" + + # ------------------------------------------------------------------------- + # Step 9: Set up Python venv for hermes user + # ------------------------------------------------------------------------- + log_info "Setting up Python venv for hermes user..." + + # Remove old venv if exists + rm -rf "$HERMES_HOME/hermes-agent/venv" + + # Create venv and install as hermes user + su - hermes -c "cd $HERMES_HOME/hermes-agent && python3 -m venv venv && ./venv/bin/pip install --upgrade pip --quiet && ./venv/bin/pip install -e '.[all]' --quiet" + + # Create symlink + su - hermes -c "ln -sf $HERMES_HOME/hermes-agent/venv/bin/hermes $HERMES_LOCAL/bin/hermes" + + # Add to hermes user's PATH + grep -q 'export PATH.*.local/bin' /home/hermes/.bashrc 2>/dev/null || \ + echo 'export PATH="$HOME/.local/bin:$PATH"' >> /home/hermes/.bashrc + grep -q 'export PATH.*.local/bin' /home/hermes/.zshrc 2>/dev/null || \ + echo 'export PATH="$HOME/.local/bin:$PATH"' >> /home/hermes/.zshrc + + log_success "Hermes venv configured" + + # ------------------------------------------------------------------------- + # Step 10: Configure Vultr Inference provider + # ------------------------------------------------------------------------- + log_info "Configuring Vultr Inference provider..." + + mkdir -p "$HERMES_HOME"/{cron,sessions,logs,pairing,hooks,image_cache,audio_cache,memories,skills} + + # Create .env with Vultr API key + cat > "$HERMES_HOME/.env" << ENV_EOF +# ============================================================================= +# VULTR INFERENCE PROVIDER (Pre-configured) +# ============================================================================= +VULTR_API_KEY=$VULTR_API_KEY +VULTR_BASE_URL=$VULTR_BASE_URL + +# For OpenAI-compatible custom endpoint mode: +OPENAI_API_KEY=$VULTR_API_KEY +OPENAI_BASE_URL=$VULTR_BASE_URL +ENV_EOF + + # Create config.yaml with Vultr model configured + cat > "$HERMES_HOME/config.yaml" << 'CONFIG_EOF' +# Hermes Agent Configuration - Vultr Inference +# ============================================================================= +# Pre-configured for Vultr Inference API (OpenAI-compatible) + +model: + # Default model (GLM-5-FP8 - 200K context, reasoning-capable) + default: "zai-org/GLM-5-FP8" + # Use custom provider for Vultr Inference (OpenAI-compatible) + provider: "custom" + base_url: "https://api.vultrinference.com/v1" + api_key: "55AIOTBABUBU7PAZRZEKORCG4T2H2VVZQ3XQ" + +# Terminal backend (local execution) +terminal: + backend: "local" + cwd: "." + timeout: 180 + lifetime_seconds: 300 + +# Enable persistent memory +memory: + memory_enabled: true + user_profile_enabled: true + memory_char_limit: 2200 + user_char_limit: 1375 + nudge_interval: 10 + +# Context compression for long conversations +compression: + enabled: true + threshold: 0.50 + target_ratio: 0.20 + protect_last_n: 20 + summary_model: "zai-org/GLM-5-FP8" + +# Agent settings +agent: + max_turns: 60 + verbose: false + reasoning_effort: "medium" + +# Toolsets for CLI +platform_toolsets: + cli: [hermes-cli] + +# Display settings +display: + compact: false + tool_progress: all + streaming: true +CONFIG_EOF + + # Set ownership + chown -R hermes:hermes "$HERMES_HOME" "$HERMES_LOCAL" + + log_success "Vultr Inference configured" + + # ------------------------------------------------------------------------- + # Step 11: Set up ttyd systemd service + # ------------------------------------------------------------------------- + log_info "Setting up ttyd service..." + + HERMES_UID=$(id -u hermes) + HERMES_GID=$(id -g hermes) + + # Disable default ttyd if enabled + systemctl disable ttyd 2>/dev/null || true + systemctl stop ttyd 2>/dev/null || true + + # Create systemd service with Homebrew in PATH + cat > /etc/systemd/system/ttyd-hermes.service << SERVICE +[Unit] +Description=ttyd terminal for Hermes Agent +After=network.target + +[Service] +Type=simple +Environment="HOME=/home/hermes" +Environment="PATH=/home/linuxbrew/.linuxbrew/bin:/home/hermes/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +ExecStart=/usr/bin/ttyd -p $TTYD_PORT -u $HERMES_UID -g $HERMES_GID -W -t fontSize=14 -t theme='{"background":"#1a1a2e","foreground":"#eee"}' /home/hermes/.local/bin/hermes +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target +SERVICE + + systemctl daemon-reload + systemctl enable ttyd-hermes + systemctl start ttyd-hermes + + log_success "ttyd service started on port $TTYD_PORT" + + # ------------------------------------------------------------------------- + # Step 12: Set up code-server for hermes user + # ------------------------------------------------------------------------- + log_info "Setting up code-server..." + + mkdir -p /home/hermes/.config/code-server + + # Create code-server config + cat > /home/hermes/.config/code-server/config.yaml << CODESERVER_EOF +bind-addr: 0.0.0.0:$CODE_SERVER_PORT +auth: password +password: hermes-dev +cert: false +CODESERVER_EOF + + chown -R hermes:hermes /home/hermes/.config + + # Create systemd service for code-server + cat > /etc/systemd/system/code-server-hermes.service << CODESERVICE +[Unit] +Description=code-server for Hermes +After=network.target + +[Service] +Type=simple +User=hermes +Environment="HOME=/home/hermes" +Environment="PATH=/home/linuxbrew/.linuxbrew/bin:/home/hermes/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +WorkingDirectory=/home/hermes +ExecStart=/usr/bin/code-server --config /home/hermes/.config/code-server/config.yaml +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target +CODESERVICE + + systemctl daemon-reload + systemctl enable code-server-hermes + systemctl start code-server-hermes + + log_success "code-server started on port $CODE_SERVER_PORT" + + # ------------------------------------------------------------------------- + # Step 13: Configure Caddy (if domain provided) + # ------------------------------------------------------------------------- + if [ -n "$DOMAIN" ]; then + log_info "Configuring Caddy for $DOMAIN..." + + cat > /etc/caddy/Caddyfile << CADDYFILE +# Hermes Dev Environment - $DOMAIN + +# Main domain - reverse proxy to services +$DOMAIN { + # Hermes terminal (ttyd) + handle /terminal/* { + reverse_proxy localhost:$TTYD_PORT + } + + # code-server + handle /code/* { + reverse_proxy localhost:$CODE_SERVER_PORT + } + + # Default: serve a landing page + handle / { + respond "Hermes Dev Environment + +Routes: + /terminal/ - Hermes Agent (ttyd) + /code/ - VS Code (code-server) +" 200 + } +} + +# Also handle www redirect +www.$DOMAIN { + redir https://$DOMAIN{uri} +} +CADDYFILE + + systemctl enable caddy + systemctl restart caddy + + log_success "Caddy configured for https://$DOMAIN" + else + log_info "No domain provided, skipping Caddy HTTPS setup" + systemctl disable caddy 2>/dev/null || true + systemctl stop caddy 2>/dev/null || true + fi + + # ------------------------------------------------------------------------- + # Step 14: Add Homebrew to system PATH + # ------------------------------------------------------------------------- + log_info "Adding Homebrew to system PATH..." + + cat > /etc/profile.d/homebrew.sh << 'BREWPROFILE' +# Homebrew +eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" +BREWPROFILE + chmod +x /etc/profile.d/homebrew.sh + + log_success "Homebrew added to system PATH" + + # ------------------------------------------------------------------------- + # Step 15: Configure UFW firewall + # ------------------------------------------------------------------------- + log_info "Configuring UFW firewall..." + + ufw default deny incoming + ufw default allow outgoing + ufw allow ssh + ufw allow http + ufw allow https + ufw allow "$TTYD_PORT/tcp" comment "ttyd" + ufw allow "$CODE_SERVER_PORT/tcp" comment "code-server" + ufw --force enable + + log_success "UFW configured" + + # ------------------------------------------------------------------------- + # Step 15: Cleanup + # ------------------------------------------------------------------------- + log_info "Cleaning up..." + + apt-get autoremove -y -qq 2>/dev/null || true + apt-get clean + rm -rf /var/lib/apt/lists/* + + # ------------------------------------------------------------------------- + # Done! + # ------------------------------------------------------------------------- + # Get server IP + SERVER_IP=$(curl -s ifconfig.me 2>/dev/null || curl -s icanhazip.com 2>/dev/null || echo "YOUR_SERVER_IP") + + echo "" + echo -e "${GREEN}${BOLD}" + echo "┌─────────────────────────────────────────────────────────┐" + echo "│ ✓ Installation Complete! │" + echo "└─────────────────────────────────────────────────────────┘" + echo -e "${NC}" + echo "" + + echo -e "${CYAN}${BOLD}📁 Hermes Configuration:${NC}" + echo "" + echo " Config: ~/.hermes/config.yaml" + echo " API Keys: ~/.hermes/.env" + echo " Data: ~/.hermes/cron/, sessions/, logs/" + echo "" + + echo -e "${CYAN}${BOLD}🌐 Access URLs:${NC}" + echo "" + if [ -n "$DOMAIN" ]; then + echo " Hermes: https://$DOMAIN/terminal/" + echo " VS Code: https://$DOMAIN/code/" + else + echo " Hermes: http://$SERVER_IP:$TTYD_PORT" + echo " VS Code: http://$SERVER_IP:$CODE_SERVER_PORT" + fi + echo "" + + echo -e "${CYAN}${BOLD}🔧 Services:${NC}" + echo "" + echo " ttyd-hermes - Hermes terminal (port $TTYD_PORT)" + echo " code-server-hermes - VS Code (port $CODE_SERVER_PORT)" + echo " caddy - Reverse proxy (if domain set)" + echo " docker - Container runtime" + echo "" + + echo -e "${CYAN}${BOLD}🛠️ Tools Installed:${NC}" + echo "" + echo " Docker: $(docker --version | cut -d' ' -f3 | tr -d ',')" + echo " Caddy: $(caddy version 2>/dev/null | head -1 | cut -d' ' -f1 || echo 'installed')" + echo " code-server: $(code-server --version 2>/dev/null | head -1 || echo 'installed')" + echo " Homebrew: /home/linuxbrew/.linuxbrew/bin/brew" + echo " Oh My Zsh: ~/.oh-my-zsh" + echo "" + + echo -e "${CYAN}${BOLD}🤖 Available Models (Vultr Inference):${NC}" + echo "" + echo " zai-org/GLM-5-FP8 (200K context, reasoning)" + echo " deepseek-ai/DeepSeek-V3.2 (128K context, reasoning)" + echo " Qwen/Qwen2.5-Coder-32B-Instruct (131K context, code)" + echo "" + + if [ -n "$DOMAIN" ]; then + echo -e "${YELLOW}⚡ Open https://$DOMAIN/terminal/ to start chatting with Hermes!${NC}" + else + echo -e "${YELLOW}⚡ Open http://$SERVER_IP:$TTYD_PORT to start chatting with Hermes!${NC}" + fi + echo "" +} + +main "$@" diff --git a/scripts/cloud-init.yaml b/scripts/cloud-init.yaml new file mode 100644 index 0000000..bd57ee5 --- /dev/null +++ b/scripts/cloud-init.yaml @@ -0,0 +1,167 @@ +#cloud-config +# ------------------------------------------------------------------ +# Deploy-time cloud-init for Vultr Marketplace +# +# The snapshot already has Hermes Agent, Docker, Caddy, ttyd, code-server +# installed. This script: +# 1. Fetches per-instance metadata from the Vultr metadata service +# 2. Configures Hermes Agent with Vultr Inference provider +# 3. Configures code-server with password +# 4. Starts ttyd and code-server services +# 5. Configures Caddy as a TLS reverse proxy +# - / → ttyd (Hermes terminal) +# - /code/* → code-server +# ------------------------------------------------------------------ + +runcmd: + # ── Fetch Vultr metadata and persist ── + - | + APP_PASSWORD=$(curl -sf -H "METADATA-TOKEN: vultr" http://169.254.169.254/v1/internal/app-password) + APP_INF_API_KEY=$(curl -sf -H "METADATA-TOKEN: vultr" http://169.254.169.254/v1/internal/app-inf_api_key) + APP_DOMAIN=$(curl -sf -H "METADATA-TOKEN: vultr" http://169.254.169.254/v1/internal/app-domain) + printf 'APP_PASSWORD="%s"\nAPP_INF_API_KEY="%s"\nAPP_DOMAIN="%s"\n' \ + "$APP_PASSWORD" "$APP_INF_API_KEY" "$APP_DOMAIN" \ + > /etc/hermes-deploy.env + chmod 600 /etc/hermes-deploy.env + + # ── Configure Hermes Agent with Vultr Inference provider ── + - | + . /etc/hermes-deploy.env + + # Create .env with API keys + cat > /home/hermes/.hermes/.env << ENV_EOF +# ============================================================================= +# VULTR INFERENCE PROVIDER (Pre-configured) +# ============================================================================= +VULTR_API_KEY=${APP_INF_API_KEY} +VULTR_BASE_URL=https://api.vultrinference.com/v1 + +# For OpenAI-compatible custom endpoint mode: +OPENAI_API_KEY=${APP_INF_API_KEY} +OPENAI_BASE_URL=https://api.vultrinference.com/v1 +ENV_EOF + chown hermes:hermes /home/hermes/.hermes/.env + chmod 600 /home/hermes/.hermes/.env + + # Create config.yaml + cat > /home/hermes/.hermes/config.yaml << 'CONFIG_EOF' +# Hermes Agent Configuration - Vultr Inference +# ============================================================================= +# Pre-configured for Vultr Inference API (OpenAI-compatible) + +model: + # Default model (GLM-5-FP8 - 200K context, reasoning-capable) + default: "zai-org/GLM-5-FP8" + # Use custom provider for Vultr Inference (OpenAI-compatible) + provider: "custom" + base_url: "https://api.vultrinference.com/v1" + # api_key will be substituted from .env + +# Terminal backend (local execution) +terminal: + backend: "local" + cwd: "." + timeout: 180 + lifetime_seconds: 300 + +# Enable persistent memory +memory: + memory_enabled: true + user_profile_enabled: true + memory_char_limit: 2200 + user_char_limit: 1375 + nudge_interval: 10 + +# Context compression for long conversations +compression: + enabled: true + threshold: 0.50 + target_ratio: 0.20 + protect_last_n: 20 + summary_model: "zai-org/GLM-5-FP8" + +# Agent settings +agent: + max_turns: 60 + verbose: false + reasoning_effort: "medium" + +# Toolsets for CLI +platform_toolsets: + cli: [hermes-cli] + +# Display settings +display: + compact: false + tool_progress: all + streaming: true +CONFIG_EOF + chown hermes:hermes /home/hermes/.hermes/config.yaml + chmod 600 /home/hermes/.hermes/config.yaml + + # ── Configure and start code-server ── + - | + . /etc/hermes-deploy.env + cat > /home/hermes/.config/code-server/config.yaml << CSCFG +bind-addr: 127.0.0.1:8080 +auth: password +password: ${APP_PASSWORD} +cert: false +CSCFG + chown hermes:hermes /home/hermes/.config/code-server/config.yaml + chmod 600 /home/hermes/.config/code-server/config.yaml + + # ── Start ttyd and code-server services ── + - systemctl enable ttyd-hermes code-server-hermes + - systemctl start ttyd-hermes code-server-hermes + + # ── Write Caddyfile ── + # ttyd at root, code-server at /code/ + # ZeroSSL primary (avoids LE rate limits), Let's Encrypt fallback + - | + . /etc/hermes-deploy.env + HASHED=$(caddy hash-password --plaintext "$APP_PASSWORD") + cat > /etc/caddy/Caddyfile << CADDY +${APP_DOMAIN} { + tls { + issuer acme { + dir https://acme.zerossl.com/v2/DV90 + eab ur6CkPWFhPtp8af1SASQEA dRKaIDk9u4tn7MYrHNczzl_tnKSJ8Hipb2IFVui3oxEHCj_3wkGBA-zG93X7NXaROh-pzDJOugrtn1nxxGasxw + } + issuer acme { + dir https://acme-v02.api.letsencrypt.org/directory + } + } + + basic_auth { + hermes ${HASHED} + } + + # code-server at /code/ + handle_path /code/* { + reverse_proxy 127.0.0.1:8080 + } + + # Default: Hermes terminal (ttyd) at root + handle { + reverse_proxy 127.0.0.1:7681 + } +} +CADDY + + # ── Start Caddy and wait for cert issuance ── + # ZeroSSL can take 30-60s; wait up to 90s for cert before proceeding + - systemctl enable caddy + - systemctl restart caddy + - | + echo "Waiting for TLS certificate issuance (up to 90s)..." + for i in $(seq 1 90); do + if find /var/lib/caddy -name '*.crt' 2>/dev/null | grep -q .; then + echo "Certificate obtained!" + break + fi + sleep 1 + done + + # ── Clean up secrets from disk ── + - rm -f /etc/hermes-deploy.env diff --git a/scripts/provision.sh b/scripts/provision.sh new file mode 100755 index 0000000..53f3621 --- /dev/null +++ b/scripts/provision.sh @@ -0,0 +1,194 @@ +#!/usr/bin/env bash +# ------------------------------------------------------------------ +# provision.sh — Packer bake-time provisioner for Hermes Agent +# +# Runs as root on a fresh Ubuntu 24.04 instance. +# Installs everything that does NOT depend on per-instance secrets: +# • system deps (incl. zsh, python3-venv) +# • Hermes Agent (from GitHub) +# • Docker (hermes user added to docker group) +# • Oh My Zsh + Homebrew (as hermes user) +# • code-server +# • ttyd +# • Caddy (stable) +# • ufw defaults +# ------------------------------------------------------------------ +set -euo pipefail +export DEBIAN_FRONTEND=noninteractive + +HERMES_VERSION="${HERMES_VERSION:-v0.7.0}" + +echo "▶ System packages" +apt-get update +apt-get upgrade -y +apt-get install -y --no-install-recommends \ + build-essential procps curl file git ca-certificates \ + gnupg software-properties-common ufw jq python3 python3-venv \ + zsh systemd-container ttyd ri ffmpeg + +# ── Docker ── +echo "▶ Docker" +install -m 0755 -d /etc/apt/keyrings +curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc +chmod a+r /etc/apt/keyrings/docker.asc +echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \ + https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \ + | tee /etc/apt/sources.list.d/docker.list > /dev/null +apt-get update +apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin +systemctl enable docker +systemctl start docker + +# ── Caddy (stable) ── +echo "▶ Caddy" +curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \ + | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg +curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \ + | tee /etc/apt/sources.list.d/caddy-stable.list +chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg +chmod o+r /etc/apt/sources.list.d/caddy-stable.list +apt-get update +apt-get install -y caddy +systemctl disable caddy # don't start until cloud-init configures it + +# ── Firewall defaults ── +echo "▶ UFW" +ufw default deny incoming +ufw default allow outgoing +ufw allow ssh +ufw allow http +ufw allow https +ufw allow 7681/tcp comment "ttyd" +ufw allow 8080/tcp comment "code-server" +ufw --force enable + +# ── Create hermes user (regular user, zsh as default shell) ── +echo "▶ hermes user" +if ! id hermes &>/dev/null; then + useradd -m -s /usr/bin/zsh hermes +else + chsh -s /usr/bin/zsh hermes +fi + +# Create ttyd group and add hermes to it +groupadd ttyd 2>/dev/null || true +usermod -aG ttyd hermes +usermod -aG docker hermes + +# Allow hermes user's systemd services to run without an active session +loginctl enable-linger hermes + +# Pre-create directories +mkdir -p /home/hermes/.hermes/{cron,sessions,logs,pairing,hooks,image_cache,audio_cache,memories,skills} +mkdir -p /home/hermes/.local/bin +mkdir -p /home/hermes/.config/code-server +chown -R hermes:hermes /home/hermes + +# ── Oh My Zsh (as hermes user) ── +echo "▶ Oh My Zsh" +su - hermes -c 'sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended' + +# ── Homebrew (installed as hermes user) ── +echo "▶ Homebrew" +echo 'hermes ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/hermes +su - hermes -c 'NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' + +# Persist Homebrew in all shells +cat >> /home/hermes/.bashrc <<'BREWEOF' +eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" +BREWEOF +cat >> /home/hermes/.profile <<'BREWEOF' +eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" +BREWEOF +cat >> /home/hermes/.zshrc <<'BREWEOF' +eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" +BREWEOF + +# Add Homebrew to system-wide PATH +cat > /etc/profile.d/homebrew.sh <<'BREWPROFILE' +# Homebrew +eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" +BREWPROFILE +chmod +x /etc/profile.d/homebrew.sh + +chown hermes:hermes /home/hermes/.zshrc /home/hermes/.bashrc /home/hermes/.profile + +su - hermes -c 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" && brew --version && brew analytics off' + +# ── code-server ── +echo "▶ code-server" +# Wait for any background apt/dpkg processes to finish +while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do + echo " Waiting for dpkg lock..." + sleep 5 +done +curl -fsSL https://code-server.dev/install.sh | sh + +chown -R hermes:hermes /home/hermes/.config + +# ── Install Hermes Agent ── +echo "▶ Hermes Agent ${HERMES_VERSION}" +su - hermes -c "git clone --branch ${HERMES_VERSION} --depth 1 https://github.com/NousResearch/hermes-agent.git /home/hermes/.hermes/hermes-agent" +su - hermes -c "cd /home/hermes/.hermes/hermes-agent && python3 -m venv venv && ./venv/bin/pip install --upgrade pip --quiet && ./venv/bin/pip install -e '.[all]' --quiet" +su - hermes -c "ln -sf /home/hermes/.hermes/hermes-agent/venv/bin/hermes /home/hermes/.local/bin/hermes" + +# Add hermes binary to PATH in shells +grep -q 'export PATH.*.local/bin' /home/hermes/.bashrc 2>/dev/null || \ + echo 'export PATH="$HOME/.local/bin:$PATH"' >> /home/hermes/.bashrc +grep -q 'export PATH.*.local/bin' /home/hermes/.zshrc 2>/dev/null || \ + echo 'export PATH="$HOME/.local/bin:$PATH"' >> /home/hermes/.zshrc + +chown -R hermes:hermes /home/hermes + +# ── Create ttyd systemd service ── +echo "▶ ttyd service" +HERMES_UID=$(id -u hermes) +HERMES_GID=$(id -g hermes) + +cat > /etc/systemd/system/ttyd-hermes.service << EOF +[Unit] +Description=ttyd terminal for Hermes Agent +After=network.target + +[Service] +Type=simple +Environment="HOME=/home/hermes" +Environment="PATH=/home/linuxbrew/.linuxbrew/bin:/home/hermes/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +ExecStart=/usr/bin/ttyd -p 7681 -u $HERMES_UID -g $HERMES_GID -W -t fontSize=14 -t theme='{"background":"#1a1a2e","foreground":"#eee"}' /home/hermes/.local/bin/hermes +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target +EOF + +# ── Create code-server systemd service ── +echo "▶ code-server service" +cat > /etc/systemd/system/code-server-hermes.service << 'EOF' +[Unit] +Description=code-server for Hermes +After=network.target + +[Service] +Type=simple +User=hermes +Environment="HOME=/home/hermes" +Environment="PATH=/home/linuxbrew/.linuxbrew/bin:/home/hermes/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +WorkingDirectory=/home/hermes +ExecStart=/usr/bin/code-server --config /home/hermes/.config/code-server/config.yaml +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target +EOF + +systemctl daemon-reload + +# ── Cleanup ── +echo "▶ Cleanup" +apt-get autoremove -y +apt-get clean +rm -rf /var/lib/apt/lists/* + +echo "✅ Packer provisioning complete"