commit a593af9b270fd5a57383143eeb91f7fd317744a6 Author: CeeLo Greenheart Date: Wed Apr 22 19:13:28 2026 +0000 Initial commit - Clean public release Sanitized for public release: - Removed all API keys, tokens, and secrets - Removed personal Discord IDs from hermes-openclaw.json - Updated git URLs to be generic placeholders - All sensitive data uses environment variable interpolation diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c9e8f4f --- /dev/null +++ b/.env.example @@ -0,0 +1,95 @@ +# OpenBoatmobile Environment Variables +# Copy to .env and fill in your values, then source it: +# source .env && terraform init && terraform plan + +# ============================================================================= +# REQUIRED: Choose your provider +# ============================================================================= +TF_VAR_cloud_provider=hetzner # or digitalocean + +# ============================================================================= +# REQUIRED: Provider API Token (choose one based on provider) +# ============================================================================= +# For Hetzner: +TF_VAR_hcloud_token=your-hetzner-api-token-here +# For DigitalOcean: +# TF_VAR_do_token=your-digitalocean-api-token-here + +# ============================================================================= +# REQUIRED: SSH Key +# ============================================================================= +# Hetzner: Name of SSH key added to Hetzner Cloud Console +# DigitalOcean: Fingerprint of SSH key added to DO +TF_VAR_ssh_key_names='["your-ssh-key-name"]' +# or for DigitalOcean: +# TF_VAR_ssh_key_fingerprints='["aa:bb:cc:dd:ee:ff:..."]' + +# ============================================================================= +# REQUIRED: Venice AI API Key (for inference) +# ============================================================================= +TF_VAR_venice_api_key=your-venice-api-key-here + +# ============================================================================= +# OPTIONAL: Tailscale (recommended for remote access) +# ============================================================================= +# Get auth key from: https://login.tailscale.com/admin/settings/keys +TF_VAR_enable_tailscale=true +TF_VAR_tailscale_auth_key=your-tailscale-auth-key-here + +# ============================================================================= +# OPTIONAL: Discord Bot Configuration +# ============================================================================= +TF_VAR_discord_bot_token= +TF_VAR_discord_server_id= +TF_VAR_discord_user_id='[]' + +# ============================================================================= +# OPTIONAL: Brave Search API +# ============================================================================= +TF_VAR_brave_search_api_key= + +# ============================================================================= +# OPTIONAL: Inference Model Configuration +# ============================================================================= +# Primary model (provider/model format) +# TF_VAR_primary_model=venice/zai-org-glm-5 + +# Fallback models (JSON array) +# TF_VAR_fallback_models='["venice/kimi-k2-5", "venice/deepseek-v3.2"]' + +# Models configuration file (models/venice.json, models/openai.json, models/combined.json) +# TF_VAR_models_file=models/venice.json + +# ============================================================================= +# OPTIONAL: Additional Inference Providers (if using non-Venice) +# ============================================================================= +# OpenAI: +# TF_VAR_openai_api_key=your-openai-api-key + +# Anthropic: +# TF_VAR_anthropic_api_key=your-anthropic-api-key + +# OpenRouter: +# TF_VAR_openrouter_api_key=your-openrouter-api-key + +# ============================================================================= +# OPTIONAL: Server Configuration +# ============================================================================= +# TF_VAR_server_name=openclaw-gateway +# TF_VAR_agent_name=main +# TF_VAR_agent_timezone=UTC +# TF_VAR_openclaw_version=lts +# TF_VAR_node_version=24 +# TF_VAR_docker_enabled=true # Set to false for direct installation + +# ============================================================================= +# OPTIONAL: Hetzner-specific +# ============================================================================= +# TF_VAR_server_type_hetzner=cx23 +# TF_VAR_location_hetzner=ash + +# ============================================================================= +# OPTIONAL: DigitalOcean-specific +# ============================================================================= +# TF_VAR_droplet_size_digitalocean=s-2vcpu-4gb +# TF_VAR_region_digitalocean=nyc3 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..571d2b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# OpenBoatmobile .gitignore + +# Terraform +.terraform/ +*.tfstate +*.tfstate.* +*.tfvars +*.tfvars.json +.terraform.lock.hcl + +# Secrets (NEVER COMMIT THESE) +*.env +*.pem +*.key +*secrets* + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + +# Local state (if using local backend) +terraform.tfstate +terraform.tfstate.backup + +# Override files +override.tf +override.tf.json +*_override.tf +*_override.tf.json \ No newline at end of file diff --git a/HERMES_FIX_SUMMARY.md b/HERMES_FIX_SUMMARY.md new file mode 100644 index 0000000..9614bec --- /dev/null +++ b/HERMES_FIX_SUMMARY.md @@ -0,0 +1,239 @@ +# Hermes Deployment Audit - Summary of Fixes + +## Executive Summary + +The Terraform Hermes deployment had **5 critical issues** preventing the service from running. All have been fixed in the cloud-init template. + +## What Was Wrong + +### Critical Issues Found: + +1. ✗ **Systemd service couldn't find docker-compose.yml** + - `ExecStart=/usr/bin/docker compose up` (missing file path) + +2. ✗ **Service ran as non-root user without Docker permissions** + - User permissions from `usermod -aG docker` don't take effect for the systemd service + +3. ✗ **Docker image pulled before docker-compose-plugin installed** + - Installation order was wrong + +4. ✗ **No check that Docker daemon was ready** + - Timing issues during bootstrap + +5. ✗ **No verification service actually started** + - Deployment would complete even if Hermes failed to start + +## What Was Fixed + +### 1. Systemd Service Configuration +**Before:** +```ini +ExecStart=/usr/bin/docker compose up +ExecStop=/usr/bin/docker compose down +User=${admin_user} +``` + +**After:** +```ini +ExecStart=/bin/sh -c 'cd /home/${admin_user} && exec docker compose -f docker-compose.yml up' +ExecStop=/bin/sh -c 'cd /home/${admin_user} && exec docker compose -f docker-compose.yml down' +User=root +StandardOutput=journal +StandardError=journal +SyslogIdentifier=hermes +``` + +**Why:** Now properly finds the compose file and doesn't have permission issues. + +--- + +### 2. Installation Order +**Before:** +```yaml +- curl -fsSL https://get.docker.com | sh +- apt-get install -y docker-compose-plugin # too late +- docker pull nousresearch/hermes-agent:latest +``` + +**After:** +```yaml +- curl -fsSL https://get.docker.com | sh +- apt-get install -y docker-compose-plugin # right after docker +- sleep 5 +- docker ps > /dev/null || (sleep 10 && docker ps) # verify ready +- docker pull nousresearch/hermes-agent:latest +``` + +**Why:** Ensures docker-compose-plugin is installed before use and Docker is ready. + +--- + +### 3. Service Startup Verification +**Before:** +```yaml +- systemctl start hermes.service +# ... done, might have failed but we don't know +``` + +**After:** +```yaml +- systemctl start hermes.service +- sleep 3 +- systemctl is-active hermes.service || systemctl status hermes.service +``` + +**Why:** Immediately tells us if startup failed. + +--- + +### 4. Enhanced Health Check Script +**Added comprehensive diagnostics:** +- ✓ Docker daemon status +- ✓ Container exists +- ✓ Container running (with uptime) +- ✓ Port listening +- ✓ Config files exist +- ✓ Systemd service status +- ✓ Recent logs +- ✓ Discord configuration check + +--- + +## New Documentation + +### 1. **HERMES_DEBUGGING.md** +Complete troubleshooting guide with: +- Quick diagnostic checklist +- Common issues and their fixes +- Command reference +- Manual start/stop procedures +- Discord connectivity testing +- Log interpretation + +### 2. **HERMES_AUDIT_REPORT.md** +Detailed audit findings explaining: +- What each issue was +- Why it caused failures +- How it was fixed +- Expected behavior after fixes + +--- + +## How to Apply These Fixes + +### Option 1: Fresh Deployment (Cleanest) +```bash +terraform destroy -auto-approve +source .env && terraform init && terraform apply +``` + +### Option 2: Update Existing Stack +```bash +source .env && terraform apply -auto-approve +``` + +--- + +## Verification After Deployment + +After applying these fixes and deploying: + +```bash +# SSH into server +ssh hermes@ + +# Run comprehensive health check +/usr/local/bin/hermes-health-check.sh + +# Manually verify +systemctl status hermes.service +docker ps +docker logs hermes +``` + +**Expected output:** +- ✓ Hermes systemd service active +- ✓ Docker container running +- ✓ Gateway listening on port 18789 +- ✓ Discord bot shows online in your server + +--- + +## Files Changed + +### Core Deployment +- `templates/userdata-hermes.tpl` - Fixed cloud-init configuration + +### Documentation +- `docs/HERMES_DEBUGGING.md` - **NEW** Troubleshooting guide +- `docs/HERMES_AUDIT_REPORT.md` - **NEW** Detailed audit findings +- `README.md` - Added reference to debugging guide + +--- + +## Why These Fixes Work + +Each fix addresses a specific failure point: + +| Issue | Root Cause | Fix | Result | +|-------|-----------|-----|--------| +| Compose file not found | No path specified | Specify full path with `-f` | Service finds config | +| Docker permission denied | Non-root user, group not applied | Run service as root | Service can use Docker | +| Docker not ready | Immediate pull attempt | Add delays and checks | Image pulls successfully | +| Silent failures | No verification | Check service status | Know if it failed | +| Can't debug | No logging | Added journal logging | Can read logs | + +--- + +## Testing the Fixes + +To verify the fixes work on your deployments: + +1. **Quick test (5 min):** + ```bash + # Just check service is running + systemctl status hermes.service + docker ps | grep hermes + ``` + +2. **Full health check (10 min):** + ```bash + /usr/local/bin/hermes-health-check.sh + ``` + +3. **Discord test (Manual):** + - Mention the bot in a configured channel + - It should respond within a few seconds + +--- + +## Rollback Plan + +If something goes wrong: + +```bash +# Revert to previous state +git checkout templates/userdata-hermes.tpl + +# Then redeploy or manually stop +systemctl stop hermes.service +docker compose -f ~hermes/docker-compose.yml down +``` + +--- + +## OpenClaw Status + +✓ OpenClaw service is properly configured and doesn't have these issues. + +--- + +## Next Steps + +1. **Review** the changes in `templates/userdata-hermes.tpl` +2. **Redeploy** using `terraform apply` +3. **Verify** using `systemctl status hermes.service` +4. **Test** Discord connectivity +5. **Refer** to `HERMES_DEBUGGING.md` if any issues occur + +All changes are backward compatible and don't affect other components. diff --git a/HERMES_VERIFICATION_CHECKLIST.md b/HERMES_VERIFICATION_CHECKLIST.md new file mode 100644 index 0000000..eb67c2c --- /dev/null +++ b/HERMES_VERIFICATION_CHECKLIST.md @@ -0,0 +1,233 @@ +# Quick Reference: Hermes Deployment Status Check + +## For Current Deployment (Before Fixes) + +If you're still SSH'd into the server from your initial deployment, run these checks: + +### Check 1: Is the systemd service running? +```bash +systemctl status hermes.service +``` +**Expected (BROKEN - before fix):** Shows `failed` or `inactive` + +### Check 2: Does the Docker container exist? +```bash +docker ps -a | grep hermes +``` +**Expected (BROKEN - before fix):** Container doesn't exist OR shows `Exited` status + +### Check 3: Check systemd journal for errors +```bash +journalctl -u hermes.service | tail -50 +``` +**Expected (BROKEN - before fix):** Error like "docker: command not found" or "file not found" + +### Check 4: Watch docker logs +```bash +docker logs hermes 2>&1 | head -20 +``` +**Expected (BROKEN - before fix):** Either no container, or errors about missing files + +### Check 5: Is Discord bot online? +```bash +# Go to Discord and check your server +# Look for the bot in members list +``` +**Expected (BROKEN - before fix):** Shows `Offline` or doesn't appear + +--- + +## After Redeploying with Fixes + +Run these verification commands immediately after deployment: + +### Quick Verification (< 1 minute) +```bash +# 1. Check service status +systemctl status hermes.service + +# 2. Check Docker container +docker ps | grep hermes + +# 3. Check port is listening +netstat -tlnp | grep 18789 +``` + +**Expected (FIXED):** +- Service shows `active (running)` +- Container shows `UP` status +- Port 18789 shows `LISTEN` + +### Comprehensive Health Check (< 5 minutes) +```bash +/usr/local/bin/hermes-health-check.sh +``` + +**Expected (FIXED):** All checks show ✓ + +### Detailed Logs +```bash +# Check what's happening in the container +docker logs -f hermes + +# Use Ctrl+C to exit after 10-20 lines +``` + +**Expected (FIXED):** +``` +[INFO] Hermes Agent Framework starting... +[INFO] Initializing gateway on port 18789 +[INFO] Discord bot initialized +``` + +### Discord Connectivity Test +```bash +# In your Discord server, type: +@hermes help + +# Bot should respond within 5 seconds +``` + +**Expected (FIXED):** Bot is online and responds + +--- + +## Troubleshooting Matrix + +| Symptom | Check | Fix | +|---------|-------|-----| +| Service shows `failed` | `journalctl -u hermes.service` | Redeploy with fixed template | +| Container `Exited` | `docker logs hermes` | Check the logs for errors | +| Port not listening | `docker ps` | Container not running | +| Docker permission denied | Check User= in service | Should be `root` now | +| Bot shows offline | Check Discord bot token | Verify in `.env` file | +| No container at all | `docker ps -a` | Image wasn't pulled, redeploy | + +--- + +## Command Reference + +### Systemd Service +```bash +# Check status +systemctl status hermes.service + +# View logs (last 50 lines) +journalctl -u hermes.service -n 50 + +# View logs with timestamps +journalctl -u hermes.service -f --all + +# Restart service +systemctl restart hermes.service + +# Stop service +systemctl stop hermes.service + +# Start service +systemctl start hermes.service +``` + +### Docker +```bash +# List running containers +docker ps + +# List all containers (including stopped) +docker ps -a + +# View container logs +docker logs hermes + +# Follow logs (live) +docker logs -f hermes + +# Show last 100 lines +docker logs --tail=100 hermes + +# Inspect container +docker inspect hermes +``` + +### Files to Check +```bash +# Configuration files +cat ~/.hermes/.env +cat ~/.hermes/config.yaml +cat ~/docker-compose.yml + +# Check permissions +ls -la ~/.hermes/ + +# Check if Hermes healthcheck script exists +ls -la /usr/local/bin/hermes-health-check.sh +``` + +--- + +## Before vs After Comparison + +### BEFORE These Fixes: +``` +❌ systemctl status hermes.service + → inactive (dead) + +❌ docker ps + → (no container) + +❌ journalctl -u hermes.service + → cannot open: "/home/hermes/docker-compose.yml" + +❌ Discord bot + → OFFLINE +``` + +### AFTER These Fixes: +``` +✓ systemctl status hermes.service + → active (running) + +✓ docker ps + → hermes container UP 2 minutes + +✓ journalctl -u hermes.service + → [INFO] Hermes Agent started successfully + +✓ Discord bot + → ONLINE ✓ +``` + +--- + +## When to Seek Help + +If after redeployment you still have issues: + +1. **Check HERMES_DEBUGGING.md** in docs/ for detailed troubleshooting +2. **Read HERMES_AUDIT_REPORT.md** for what was fixed +3. **Run health check:** `/usr/local/bin/hermes-health-check.sh` +4. **Share logs:** `docker logs hermes` output +5. **Check config:** Verify Discord token, server ID, user IDs in `~/.hermes/.env` + +--- + +## Redeploy Command + +To apply all fixes: + +```bash +cd ~/openboatmobile + +# Option 1: Clean slate (recommended) +terraform destroy -auto-approve +source .env && terraform init && terraform apply + +# Option 2: Update in-place +source .env && terraform apply -auto-approve +``` + +Then verify with: +```bash +ssh hermes@ +/usr/local/bin/hermes-health-check.sh +``` diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a15d3bc --- /dev/null +++ b/Makefile @@ -0,0 +1,95 @@ +# Makefile for OpenBoatmobile +# Convenience commands for common operations + +.PHONY: help init plan apply destroy ssh logs health clean + +# Default target +help: + @echo "OpenBoatmobile - Terraform deployment commands" + @echo "" + @echo "Usage: make " + @echo "" + @echo "Targets:" + @echo " init Initialize Terraform" + @echo " plan Show deployment plan" + @echo " apply Deployinfrastructure" + @echo " destroy Destroy infrastructure" + @echo " output Show deployment outputs" + @echo " ssh SSH into server" + @echo " tunnel Create SSH tunnel to gateway" + @echo " logs Show gateway logs" + @echo " health Run health check" + @echo " clean Clean Terraform state" + @echo "" + @echo "Prerequisites:" + @echo " source .env # Load secrets before running" + @echo "" + +# Initialize Terraform +init: + terraform init + +# Show deployment plan +plan: + terraform plan + +# Deploy infrastructure +apply: + terraform apply + +# Destroy infrastructure +destroy: + terraform destroy + +# Show outputs +output: + terraform output + +# SSH into server (extracts command from Terraform output) +ssh: + @SSH_CMD=$$(terraform output -raw ssh_command); + echo "$$SSH_CMD"; \ + $$SSH_CMD + +# Create SSH tunnel to gateway (extracts command from output) +tunnel: + @SSH_CMD=$$(terraform output -raw ssh_command); \ + IP=$$(terraform output -raw server_ip); \ + echo "Creating tunnel to gateway..."; \ + $$SSH_CMD -L 18789:localhost:18789 + +# Show gateway logs (requires SSH) +logs: + @SSH_CMD=$$(terraform output -raw ssh_command); \ + $$SSH_CMD "journalctl -u openclaw-gateway -f" + +# Run health check (requires SSH) +health: + @SSH_CMD=$$(terraform output -raw ssh_command); \ + $$SSH_CMD "sudo /usr/local/bin/openclaw-health-check.sh" + +# Clean Terraform state +clean: + rm -rf .terraform/ + rm -f .terraform.lock.hcl + rm -f terraform.tfstate* + @echo "Terraform state cleaned. Run 'make init' to reinitialize." + +# Validate configuration +validate: + terraform validate + +# Format code +fmt: + terraform fmt + +# Show workspace status +status: + @echo "=== Terraform Workspace ===" + @terraform workspace show 2>/dev/null || echo "default" + @echo "" + @echo "=== State Resources ===" + @terraform state list 2>/dev/null || echo "No resources in state" + @echo "" + @echo "=== Outputs ===" + @terraform output 2>/dev/null || echo "No outputs defined" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a634d11 --- /dev/null +++ b/README.md @@ -0,0 +1,175 @@ +# OpenBoatmobile + +**Deploy OpenClaw agents to Hetzner Cloud or DigitalOcean with one command.** + +OpenBoatmobile is a reusable, distributable Terraform repository for spinning up AI agent infrastructure. Choose your provider, set your secrets, and deploy. + +## Features + +- **Provider-agnostic**: Deploy to Hetzner Cloud or DigitalOcean +- **Full automation**: Server provisioning and either OpenClaw or Hermes installation +- **Tailscale integration**: Secure remote access without exposing ports +- **Secrets management**: Environment-based, no secrets in git +- **One agent focus**: Clean single-agent deployments +- **Discord connectivity**: Quick setup for Discord bot during deployment + +## Quick Start + +```bash +# Clone +git clone https://github.com/YOUR_USERNAME/openboatmobile-ai.git +cd openboatmobile + +# Configure secrets +cp .env.example .env +$EDITOR .env + +# Deploy +source .env && terraform init && terraform apply +``` + +**Documentation:** [GETTING-STARTED.md](docs/GETTING-STARTED.md) + +## Cost Comparison + +| Provider | Instance | vCPU | RAM | Disk | Price | +|----------|----------|------|-----|------|-------| +| **Hetzner** | cpx21 | 2 | 4 GB | 80 GB | **€4.49/mo** | +| DigitalOcean | s-2vcpu-4gb | 2 | 4 GB | 80 GB | $24/mo | + +Hetzner is ~70% cheaper for equivalent specs. + +## Documentation + +| Document | Purpose | +|----------|---------| +| [GETTING-STARTED.md](docs/GETTING-STARTED.md) | Step-by-step deployment guide | +| [SECRETS.md](docs/SECRETS.md) | Managing API tokens and keys | +| [HETZNER_SETUP.md](docs/HETZNER_SETUP.md) | Hetzner Cloud detailed setup | +| [DIGITALOCEAN_SETUP.md](docs/DIGITALOCEAN_SETUP.md) | DigitalOcean detailed setup | +| [TAILSCALE_SETUP.md](docs/TAILSCALE_SETUP.md) | Secure remote access | +| [DISCORD_SETUP.md](docs/DISCORD_SETUP.md) | Discord bot integration | +| [DOCKER_VS_DIRECT.md](docs/DOCKER_VS_DIRECT.md) | Docker vs direct installation guide | +| [HERMES_DEBUGGING.md](docs/HERMES_DEBUGGING.md) | Debugging Hermes Agent issues | +| [TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) | Common issues and fixes | + +## Usage + +### 1. Prerequisites + +- Terraform >= 1.5.4 +- SSH key pair +- Hetzner or DigitalOcean API token +- API key for Venice AI or alternative inference provider +- (Optional) Tailscale auth key +- (Optional) Discord bot token and private server + +### 2. Configure Secrets + +```bash +cp .env.example .env +$EDITOR .env +``` + +Required: +```bash +TF_VAR_cloud_provider=hetzner # or digitalocean +TF_VAR_hcloud_token=your-hetzner-token # for Hetzner +TF_VAR_venice_api_key=your-venice-key +TF_VAR_ssh_key_names='["your-key-name"]' +``` + +Optional: +```bash +TF_VAR_docker_enabled=true # Set to false for direct installation (no Docker) +``` + +Recommended: +```bash +TF_VAR_enable_tailscale=true +TF_VAR_tailscale_auth_key=tskey-auth-xxxxx +``` + +### 3. Deploy + +```bash +source .env +terraform init +terraform plan +terraform apply +``` + +### 4. Connect + +```bash +# SSH (from Terraform output - username varies by framework) +# For Hermes: ssh hermes@ +# For OpenClaw: ssh openclaw@ +ssh @ + +# Run OpenClaw onboarding (OpenClaw framework only) +openclaw onboard --install-daemon + +# If using Tailscale +sudo tailscale serve --bg 18789 +``` + +## Configuration + +See [examples/terraform.tfvars.example](examples/terraform.tfvars.example) + +## Project Structure + +``` +openboatmobile/ +├── main.tf # Provider selector +├── variables.tf # Input variables +├── outputs.tf # Deployment outputs +├── cloudinit.tf # Cloud-init config generator +├── providers/ +│ ├── digitalocean.tf # DO-specific resources +│ └── hetzner.tf # Hetzner-specific resources +├── templates/ +│ └── userdata.tpl # Cloud-init script +├── examples/ +│ └── terraform.tfvars.example +├── docs/ +│ ├── GETTING-STARTED.md +│ ├── SECRETS.md +│ ├── HETZNER_SETUP.md +│ ├── DIGITALOCEAN_SETUP.md +│ ├── TAILSCALE_SETUP.md +│ ├── DISCORD_SETUP.md +│ └── TROUBLESHOOTING.md +├── .env.example # Secrets template +├── .gitignore +└── README.md +``` + +## Security + +OpenBoatmobile deploys with security best practices: + +| Feature | Description | +|---------|-------------| +| Loopback binding | Gateway binds to 127.0.0.1 only | +| Firewall | SSH-only inbound | +| fail2ban | Brute force protection | +| Auto-updates | Unattended security patches | +| Non-root user | Deploy with dedicated OS user (`hermes` or `openclaw` based on framework) | +| Tailscale | No public HTTPS exposure | + +## Support + +- [OpenClaw docs](https://docs.openclaw.ai) +- [Hermes docs](https://hermes-agent.nousresearch.com/docs/) + +## License + +Apache 2.0 + +## Origin + +OpenBoatmobile is part of the **Krusty Planet** project — infrastructure for AI agent deployments. + +--- diff --git a/cloudinit.tf b/cloudinit.tf new file mode 100644 index 0000000..4c957ee --- /dev/null +++ b/cloudinit.tf @@ -0,0 +1,120 @@ +# Cloud-init Configuration +# Selects template based on agent_framework variable + +# Hermes Agent cloud-init +data "cloudinit_config" "hermes" { + count = var.agent_framework == "hermes" ? 1 : 0 + + gzip = false + base64_encode = true + + part { + filename = "cloud-config.yaml" + content_type = "text/cloud-config" + content = templatefile("${path.module}/templates/userdata-hermes.tpl", { + # Server configuration + server_name = var.server_name + admin_user = local.effective_admin_user + location = var.location_hetzner + + # Agent configuration + agent_name = var.agent_name + primary_model = var.primary_model + primary_model_name = var.primary_model_name + fallback_models = var.fallback_models + docker_enabled = var.docker_enabled + + # SSH configuration + ssh_port = var.ssh_port + ssh_allowed_ips = var.ssh_allowed_ips + admin_ssh_keys = var.admin_ssh_keys + + # API keys + venice_api_key = var.venice_api_key + venice_base_url = var.venice_base_url + brave_search_api_key = var.brave_search_api_key + + # Discord + discord_bot_token = var.discord_bot_token + discord_server_id = var.discord_server_id + discord_user_id = var.discord_user_id + discord_home_channel = var.discord_home_channel + discord_allowed_users = var.discord_allowed_users + discord_auto_thread = var.discord_auto_thread + gateway_allow_all_users = var.gateway_allow_all_users + + # Gateway + gateway_token = var.gateway_token != "" ? var.gateway_token : random_password.gateway_token[0].result + gateway_allowed_users = var.gateway_allowed_users + }) + } +} + +# OpenClaw cloud-init +data "cloudinit_config" "openclaw" { + count = var.agent_framework == "openclaw" ? 1 : 0 + + gzip = false + base64_encode = true + + part { + filename = "cloud-config.yaml" + content_type = "text/cloud-config" + content = templatefile("${path.module}/templates/userdata-openclaw.tpl", { + # Server configuration + server_name = var.server_name + admin_user = local.effective_admin_user + + # SSH configuration + ssh_port = var.ssh_port + ssh_allowed_ips = var.ssh_allowed_ips + admin_ssh_keys = var.admin_ssh_keys + + # OpenClaw configuration + openclaw_version = "lts" + node_version = "24" + agent_name = var.agent_name + agent_timezone = "UTC" + + # System configuration + enable_swap = true + swap_size = 2 + enable_fail2ban = true + enable_unattended_upgrades = true + + # Tailscale + enable_tailscale = var.enable_tailscale + tailscale_auth_key = var.tailscale_auth_key + + # API keys + venice_api_key = var.venice_api_key + default_model = var.primary_model + brave_search_api_key = var.brave_search_api_key + + # Discord + discord_bot_token = var.discord_bot_token + discord_server_id = var.discord_server_id + discord_user_id = var.discord_user_id + discord_home_channel = var.discord_home_channel + discord_allowed_users = var.discord_allowed_users + discord_auto_thread = var.discord_auto_thread + + # Inference models configuration + primary_model = var.primary_model + fallback_models = jsonencode(var.fallback_models) + models_config = file("${path.module}/models/venice.json") + }) + } +} + +# Random password for gateway token if not provided +resource "random_password" "gateway_token" { + count = var.agent_framework == "hermes" && var.gateway_token == "" ? 1 : 0 + length = 32 + special = false +} + +# Output selected userdata +locals { + userdata = var.agent_framework == "hermes" ? data.cloudinit_config.hermes[0].rendered : data.cloudinit_config.openclaw[0].rendered +} \ No newline at end of file diff --git a/digitalocean.tf b/digitalocean.tf new file mode 100644 index 0000000..0267845 --- /dev/null +++ b/digitalocean.tf @@ -0,0 +1,74 @@ +# DigitalOcean Provider Resources +# Conditionally created when var.cloud_provider == "digitalocean" + +# ============================================================================= +# FIREWALL (DigitalOcean calls this "Firewall") +# ============================================================================= + +resource "digitalocean_firewall" "agent" { + count = local.is_digitalocean ? 1 : 0 + + name = "${var.server_name}-firewall" + + # Inbound: SSH only + inbound_rule { + protocol = "tcp" + port_range = tostring(var.ssh_port) + source_addresses = var.ssh_allowed_ips + } + + # Outbound: Allow all + outbound_rule { + protocol = "tcp" + port_range = "1-65535" + destination_addresses = ["0.0.0.0/0", "::/0"] + } + + outbound_rule { + protocol = "udp" + port_range = "1-65535" + destination_addresses = ["0.0.0.0/0", "::/0"] + } + + outbound_rule { + protocol = "icmp" + destination_addresses = ["0.0.0.0/0", "::/0"] + } +} + +# ============================================================================= +# DROPLET (Server) +# ============================================================================= + +resource "digitalocean_droplet" "agent" { + count = local.is_digitalocean ? 1 : 0 + + name = var.server_name + image = "ubuntu-24-04-x64" + size = var.droplet_size_digitalocean + region = var.region_digitalocean + + # SSH keys specified by fingerprint - DigitalOcean accepts fingerprints directly + ssh_keys = var.ssh_key_fingerprints + + # Tags for organization + tags = [ + var.project_name, + var.environment, + var.agent_framework + ] + + # Cloud-init user data + user_data = local.userdata +} + +# ============================================================================= +# FIREWALL ATTACHMENT +# ============================================================================= + +resource "digitalocean_firewall" "agent_attachment" { + count = local.is_digitalocean ? 1 : 0 + + name = "${var.server_name}-firewall" + droplet_ids = [digitalocean_droplet.agent[0].id] +} \ No newline at end of file diff --git a/docs/DIGITALOCEAN_SETUP.md b/docs/DIGITALOCEAN_SETUP.md new file mode 100644 index 0000000..ca3caa5 --- /dev/null +++ b/docs/DIGITALOCEAN_SETUP.md @@ -0,0 +1,185 @@ +# DigitalOcean Setup + +Detailed guide for deploying OpenBoatmobile to DigitalOcean. + +## When to Use DigitalOcean + +| Factor | Hetzner | DigitalOcean | +|--------|---------|--------------| +| Price | €4.49/mo (cx23) | $24/mo (s-2vcpu-4gb) | +| US West Coast | No | Yes (SFO2, SFO3) | +| Documentation | Good | Excellent | +| One-click apps | Limited | Extensive | +| Support | Ticket | Ticket + Premium | + +Use DigitalOcean if: +- You're on the US West Coast (SFO has better latency than Ashburn) +- You already have DO credits/promo codes +- You prefer DO's documentation and ecosystem + +## Create DigitalOcean Account + +1. Go to [DigitalOcean](https://www.digitalocean.com/) +2. Sign up +3. Add a payment method ($5 minimum) + +## Create API Token + +1. Go to [DO API Settings](https://cloud.digitalocean.com/account/api/tokens) +2. Click **Generate New Token** +3. Name it (e.g., "openclaw-terraform") +4. Permissions: **Read & Write** +5. Copy the token immediately (shown only once) + +## Add SSH Key + +1. Go to [DO Security Settings](https://cloud.digitalocean.com/account/security) +2. Click **Add SSH Key** +3. Paste your public key contents: + ```bash + cat ~/.ssh/id_ed25519.pub + ``` +4. Give it a name +5. Click **Add SSH Key** + +### Get the Fingerprint + +Terraform needs the fingerprint, not the name: + +```bash +ssh-keygen -lf ~/.ssh/id_ed25519.pub +# Output: 256 SHA256:abc123... your@email.com (ED25519) +``` + +The fingerprint is the part after `SHA256:` and before the email. + +```bash +TF_VAR_ssh_key_fingerprints='["abc123..."]' +``` + +## Choose a Region + +| Code | Location | Notes | +|------|----------|-------| +| `nyc1` | New York | US East | +| `nyc3` | New York | US East (recommended) | +| `sfo2` | San Francisco | US West | +| `sfo3` | San Francisco | US West | +| `ams3` | Amsterdam | Europe | +| `lon1` | London | Europe | +| `sgp1` | Singapore | Asia | + +## Configure OpenBoatmobile + +### Minimal Configuration + +In `terraform.tfvars`: + +```hcl +provider = "digitalocean" + +server_name = "my-agent" +droplet_size_digitalocean = "s-2vcpu-4gb" +region_digitalocean = "nyc3" + +# These come from environment: +# TF_VAR_do_token +# TF_VAR_venice_api_key +# TF_VAR_ssh_key_fingerprints +``` + +### Droplet Sizes + +| Size | vCPU | RAM | Disk | Price | +|------|------|-----|------|-------| +| s-1vcpu-2gb | 1 | 2 GB | 50 GB | $12/mo | +| **s-2vcpu-4gb** | 2 | 4 GB | 80 GB | **$24/mo** (recommended) | +| s-2vcpu-8gb | 2 | 8 GB | 160 GB | $48/mo | +| s-4vcpu-8gb | 4 | 8 GB | 160 GB | $64/mo | + +The s-2vcpu-4gb is the sweet spot for OpenClaw. + +## Deploy + +```bash +# Load secrets +source .env + +# Initialize (first time only) +terraform init + +# Preview changes +terraform plan + +# Deploy +terraform apply +``` + +## Post-Deployment + +Terraform outputs: + +``` +server_ip = "123.45.67.89" +ssh_command = "ssh openclaw@123.45.67.89" # or "ssh hermes@123.45.67.89" for Hermes +``` + +### Connect + +```bash +# Username is 'openclaw' or 'hermes' depending on framework +ssh @123.45.67.89 +``` + +### Run OpenClaw Onboarding + +```bash +openclaw onboard --install-daemon +``` + +## Firewall Rules + +OpenBoatmobile creates a DigitalOcean firewall with: + +| Direction | Port | Source | +|-----------|------|--------| +| Inbound | 22 (SSH) | Configured IPs | +| Outbound | All | Any | + +To restrict SSH to your IP: + +```bash +TF_VAR_ssh_allowed_ips='["your.public.ip/32"]' +``` + +## Cleanup + +```bash +terraform destroy +``` + +## Troubleshooting + +### "SSH Key fingerprint not found" + +- Use the fingerprint, not the name +- The fingerprint is shown in DO Console under Security +- Make sure there are no extra spaces + +### "API Token invalid" + +- Regenerate the token +- Copy immediately (shown only once) +- Check for trailing spaces in `.env` + +### Droplet created but can't SSH + +- Wait 2-3 minutes for cloud-init +- Verify your key fingerprint is correct +- Check firewall allows your IP + +### "Rate limit exceeded" + +- DO has API rate limits +- Wait a few minutes and retry +- Use `terraform plan` sparingly before `apply` \ No newline at end of file diff --git a/docs/DISCORD_SETUP.md b/docs/DISCORD_SETUP.md new file mode 100644 index 0000000..1239db5 --- /dev/null +++ b/docs/DISCORD_SETUP.md @@ -0,0 +1,197 @@ +# Discord Setup + +OpenBoatmobile can configure Discord integration during deployment. + +## Why Discord Integration? + +| Channel | Pros | Cons | +|---------|------|------| +| Discord | Real-time, familiar interface, mobile push | Requires bot setup | +| Control UI | Full featured, direct | No push notifications | +| CLI | Scriptable | No mobile access | + +**Recommended:** Discord for mobile notifications and quick interactions. + +## Prerequisites + +- A Discord account +- A Discord server where you can add bots +- Permission to create bots in that server + +## Step 1: Create Discord Application + +1. Go to [Discord Developer Portal](https://discord.com/developers/applications) +2. Click **New Application** +3. Name it (e.g., "OpenClaw Agent") +4. Click **Create** + +## Step 2: Create Bot User + +1. In your application, go to **Bot** in the left sidebar +2. Click **Add Bot** +3. Confirm the popup +4. **Copy the token** immediately (click "Reset Token" if needed) +5. Save this token — you'll need it for `.env` + +### Bot Permissions + +Under **Privileged Gateway Intents**, enable: +- **Message Content Intent** (required to read messages) +- **Server Members Intent** (optional, for user info) + +## Step 3: Invite Bot to Server + +1. Go to **OAuth2** → **URL Generator** in the left sidebar +2. Under **Scopes**, check: + - `bot` + - `applications.commands` +3. Under **Bot Permissions**, check: + - `Send Messages` + - `Read Messages/View Channels` + - `Read Message History` + - `Mention Everyone` (optional) + - `Use Slash Commands` +4. Copy the generated URL at the bottom +5. Open the URL in your browser +6. Select your server and authorize + +## Step 4: Get Server and User IDs + +### Server ID + +1. In Discord, go to **User Settings** (gear icon) +2. Go to **Advanced** → Enable **Developer Mode** +3. Right-click your server name +4. Click **Copy Server ID** + +### User ID + +1. Right-click your username in Discord +2. Click **Copy User ID** + +## Step 5: Configure OpenBoatmobile + +In `.env`: + +```bash +TF_VAR_discord_bot_token=your-bot-token-here +TF_VAR_discord_server_id=123456789012345678 +TF_VAR_discord_user_id='["123456789012345678", "another-user-id"]' +``` + +## Step 6: Deploy (or Update) + +```bash +# Initial deployment +terraform apply + +# If already deployed, update: +terraform apply -var="discord_bot_token=..." -var="discord_server_id=..." -var="discord_user_id=[\"user1\", \"user2\"]" +``` + +## Step 7: Pair the Gateway + +After deployment, the gateway needs to be paired with Discord: + +### If Tailscale is Enabled + +1. Visit `https://..ts.net/` +2. If device pairing is required: + - You'll see a pairing code + - On the server: `openclaw pairing approve device ` + +### If Using SSH Tunnel + +```bash +# Create tunnel +ssh -L 18789:localhost:18789 openclaw@ + +# Open browser +# http://localhost:18789 +``` + +## Channel Configuration + +By default, the bot is configured for: +- All channels in the server (using wildcard `*`) +- No mention required (bot responds to all messages) +- Only your user ID in allowlist + +To customize, edit `openclaw.json` after deployment: + +```json +{ + "channels": { + "discord": { + "enabled": true, + "token": "${DISCORD_BOT_TOKEN}", + "groupPolicy": "allowlist", + "guilds": { + "SERVER_ID": { + "requireMention": false, + "users": ["YOUR_USER_ID"], + "channels": { + "*": { "allow": true } + } + } + } + } + } +} +``` + +## Testing + +### Test Bot is Working + +1. In Discord, go to any channel in your server +2. Type a message +3. The bot should respond (if `requireMention` is false) +4. Or: Mention the bot with `@OpenClaw Agent hello` + +### Check Gateway Logs + +On the server: + +```bash +# Check gateway is running +systemctl status openclaw-gateway + +# View logs +journalctl -u openclaw-gateway -f +``` + +## Troubleshooting + +### Bot doesn't respond + +1. Check bot token is correct +2. Verify bot has **Message Content Intent** enabled +3. Check server ID and user IDs are correct +4. Verify bot is in your server + +### "Unauthorized" in gateway logs + +- Verify `discord_user_id` list contains your actual Discord IDs +- Check each user ID is in the server's member list + +### Gateway shows pairing code + +If you see a pairing code: + +1. SSH into the server +2. Run: `openclaw pairing approve device ` +3. Refresh the browser + +### Bot joins but doesn't respond + +- Check `requireMention` setting +- Verify your user ID is in the allowlist +- Check gateway logs for errors + +## Security Notes + +- The bot token provides full access to the bot — keep it secret +- Regenerate the token if compromised: Discord Dev Portal → Bot → Reset Token +- The user ID allowlist ensures only you can interact with the agent +- For team access, add multiple user IDs to the `users` array \ No newline at end of file diff --git a/docs/DOCKER_VS_DIRECT.md b/docs/DOCKER_VS_DIRECT.md new file mode 100644 index 0000000..d7687ee --- /dev/null +++ b/docs/DOCKER_VS_DIRECT.md @@ -0,0 +1,255 @@ +# Docker vs Direct Installation Guide + +## Overview + +OpenBoatmobile now supports two deployment modes for Hermes Agent: + +1. **Docker Container** (default, `docker_enabled = true`) + - Runs Hermes in a Docker container + - Isolated environment, easier updates + - Slightly higher resource usage + +2. **Direct Installation** (`docker_enabled = false`) + - Installs Hermes directly on the host system + - Lower resource usage, faster startup + - `hermes` command available in PATH + - Better for dedicated VPS environments + +## Configuration + +### Enable Direct Installation + +**In `.env` file:** +```bash +TF_VAR_docker_enabled=false +``` + +**In `terraform.tfvars`:** +```hcl +docker_enabled = false +``` + +### Default Behavior + +- `docker_enabled = true` (Docker container) - **Default** +- `docker_enabled = false` (Direct installation) + +## Deployment Differences + +### Docker Mode (`docker_enabled = true`) + +**Installation:** +- Installs Docker and docker-compose +- Pulls `nousresearch/hermes-agent:latest` +- Runs in container with volume mounts + +**Management:** +```bash +# Check status +docker ps | grep hermes + +# View logs +docker logs hermes + +# Restart +docker restart hermes + +# Access hermes CLI +docker exec hermes hermes --help +``` + +**Resource Usage:** +- ~200MB additional RAM for Docker daemon +- Container overhead (~50MB RAM) +- Isolated filesystem + +### Direct Mode (`docker_enabled = false`) + +**Installation:** +- Installs `uv` package manager from Astral +- Clones `github.com/NousResearch/hermes-agent` repository +- Creates Python 3.11 virtual environment +- Installs with `uv pip install -e ".[messaging]"` (Discord/Slack/Telegram support) +- Creates `/usr/local/bin/hermes` wrapper script + +**Management:** +```bash +# Check status +systemctl status hermes.service + +# View logs +journalctl -u hermes.service -f + +# Restart +systemctl restart hermes.service + +# Access hermes CLI directly +hermes --help +hermes gateway status +``` + +**Resource Usage:** +- Minimal overhead (~20MB RAM for venv) +- Direct process execution +- Shared filesystem with host + +## File Locations + +### Docker Mode +``` +/home/hermes/.hermes/ # Config and data (host) +/var/lib/docker/ # Container runtime +``` + +### Direct Mode +``` +/home/hermes/.hermes/ # Config and data +/home/hermes/hermes-agent/ # Git repository +/home/hermes/hermes-agent/venv/ # Python virtual environment +/usr/local/bin/hermes # CLI wrapper script +/root/.local/bin/uv # uv package manager +``` + +## Command Line Access + +### Docker Mode +```bash +# Run hermes commands +docker exec hermes hermes --version +docker exec hermes hermes gateway status + +# Or create alias for convenience +echo "alias hermes='docker exec hermes hermes'" >> ~/.bashrc +``` + +### Direct Mode +```bash +# hermes command is directly available +hermes --version +hermes gateway status +hermes --help +``` + +## Health Checks + +### Docker Mode +```bash +/usr/local/bin/hermes-health-check.sh +# Checks: Docker daemon, container status, port 18789, config files +``` + +### Direct Mode +```bash +/usr/local/bin/hermes-health-check.sh +# Checks: hermes binary, venv, process status, port 18789, config files +``` + +## Troubleshooting + +### Docker Mode Issues +```bash +# Docker daemon not running +sudo systemctl start docker + +# Container crashed +docker logs hermes +docker restart hermes + +# Permission issues +sudo usermod -aG docker $USER +newgrp docker +``` + +### Direct Mode Issues +```bash +# hermes command not found +which hermes +ls -la /usr/local/bin/hermes +cat /usr/local/bin/hermes # Check wrapper script content + +# Virtual environment issues +ls -la ~/hermes-agent/venv/ +ls -la ~/hermes-agent/venv/bin/hermes + +# Check if repo was cloned +ls -la ~/hermes-agent/ + +# Check if uv is installed +ls -la /root/.local/bin/uv + +# Service not starting +journalctl -u hermes.service -n 20 +systemctl status hermes.service + +# Reinstall manually +cd ~ +git clone --recurse-submodules https://github.com/NousResearch/hermes-agent.git +cd hermes-agent +/root/.local/bin/uv venv venv --python 3.11 +/root/.local/bin/uv pip install -e '.[messaging]' +``` + +## Migration Between Modes + +### From Docker to Direct +1. Set `docker_enabled = false` +2. Run `terraform apply` +3. Data in `~/.hermes/` is preserved +4. `hermes` command becomes available + +### From Direct to Docker +1. Set `docker_enabled = true` +2. Run `terraform apply` +3. Data in `~/.hermes/` is preserved +4. Use `docker exec hermes hermes` for CLI access + +## Performance Comparison + +| Metric | Docker Mode | Direct Mode | Difference | +|--------|-------------|-------------|------------| +| RAM Usage | ~400MB | ~200MB | -50% | +| Startup Time | ~15s | ~5s | -67% | +| Disk Usage | ~2GB | ~1GB | -50% | +| hermes CLI | `docker exec` | Direct | Simpler in Direct | +| Isolation | Full | None | Docker more secure | + +## Recommendations + +### Use Docker Mode When: +- Running multiple services on the same server +- Wanting easy rollback/updates +- Security isolation is important +- Using cloud environments with limited control + +### Use Direct Mode When: +- Dedicated VPS for Hermes only +- Wanting minimal resource usage +- Needing fastest possible startup +- Wanting direct CLI access without `docker exec` + +## Examples + +### Minimal Direct Installation +```hcl +# terraform.tfvars +cloud_provider = "hetzner" +agent_framework = "hermes" +docker_enabled = false +venice_api_key = "your-key" +ssh_key_names = ["your-key"] +``` + +### Docker Installation with Custom User +```hcl +# terraform.tfvars +cloud_provider = "hetzner" +agent_framework = "hermes" +docker_enabled = true +admin_user = "ai-admin" # Override default 'hermes' +venice_api_key = "your-key" +ssh_key_names = ["your-key"] +``` + +## Support + +Both modes are fully supported. The direct mode is recommended for dedicated VPS deployments where you want the `hermes` command directly available in your PATH. \ No newline at end of file diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md new file mode 100644 index 0000000..78d12c8 --- /dev/null +++ b/docs/GETTING-STARTED.md @@ -0,0 +1,166 @@ +# Getting Started with OpenBoatmobile + +This guide walks you through deploying an OpenClaw agent in 15 minutes. + +## Prerequisites + +Before you start, you need: + +| Requirement | How to Get It | +|-------------|---------------| +| Terraform >= 1.5.4 | [Install guide](https://developer.hashicorp.com/terraform/install) | +| SSH key pair | `ssh-keygen -t ed25519 -C "your@email.com"` | +| Hetzner Cloud API token | [Hetzner Console](https://console.hetzner.cloud/) → Security → API Tokens | +| Venice AI API key | [Venice.ai](https://venice.ai) → Settings → API Keys | +| Tailscale auth key (recommended) | [Tailscale Admin](https://login.tailscale.com/admin/settings/keys) | + +**Optional:** +- DigitalOcean API token (if using DO instead of Hetzner) +- Discord bot token (for Discord integration) +- Brave Search API key (for web search) + +## Step 1: Clone the Repository + +```bash +git clone https://github.com/YOUR_USERNAME/openboatmobile-ai.git +cd openboatmobile +``` + +## Step 2: Configure Secrets + +OpenBoatmobile uses environment variables for secrets. This keeps sensitive dataout of git. + +```bash +# Copy the example +cp .env.example .env + +# Edit with your values +$EDITOR .env +``` + +**Required secrets:** + +```bash +# Choose your provider +TF_VAR_cloud_provider=hetzner # or digitalocean + +# Provider API token (one of these) +TF_VAR_hcloud_token=your-hetzner-api-token-here +# TF_VAR_do_token=your-digitalocean-api-token-here + +# Venice AI (required for inference) +TF_VAR_venice_api_key=your-venice-api-key-here + +# SSH key name (as shown in your cloud provider's console) +TF_VAR_ssh_key_names='["my-ssh-key-name"]' +``` + +**Recommended:** + +```bash +# Tailscale for secure remote access +TF_VAR_enable_tailscale=true +TF_VAR_tailscale_auth_key=tskey-auth-xxxxx +``` + +## Step 3: Source the Environment + +```bash +source .env +``` + +This loads your secrets into the shell. Terraform will read `TF_VAR_*` variables automatically. + +## Step 4: Initialize and Plan + +```bash +terraform init +terraform plan +``` + +Review the plan. You should see: +- 1 server (Hetzner) or 1 droplet (DigitalOcean) +- 1 firewall +- Cloud-init configuration + +## Step 5: Deploy + +```bash +terraform apply +``` + +Type `yes` when prompted. Deployment takes 2-5 minutes. + +## Step 6: Connect + +Terraform outputs the SSH command (username depends on framework): + +```bash +# Example output for OpenClaw: +ssh_command = "ssh openclaw@123.45.67.89" +# Example output for Hermes: +ssh_command = "ssh hermes@123.45.67.89" +``` + +SSH into your server: + +```bash +# The username will be either 'openclaw' or 'hermes' based on your framework +ssh @ +``` + +## Step 7: Run OpenClaw Onboarding + +On the server: + +```bash +openclaw onboard --install-daemon +``` + +This configures the OpenClaw gateway and starts the service. + +## Step 8: Configure Tailscale (if enabled) + +If you're using Tailscale: + +```bash +# On the server +sudo tailscale serve --bg 18789 +``` + +Then visit: `https://..ts.net/` + +## Step 9: Configure Discord (Optional) + +See [DISCORD_SETUP.md](./DISCORD_SETUP.md) for Discord bot configuration. + +## Troubleshooting + +### SSH Connection Refused + +- Wait 2-3 minutes after deployment for cloud-init to complete +- Check firewall allows your IP: `TF_VAR_ssh_allowed_ips='["your.ip.here/32"]'` + +### Terraform Error: "SSH key not found" + +- Hetzner: Key name must match exactly as shown in Console +- DigitalOcean: Use the fingerprint, not the name + +### OpenClaw command not found + +- Cloud-init installs Node.js and OpenClaw +- Wait a few minutes, then try: `which openclaw` +- Check logs: `tail -f /var/log/cloud-init-output.log` + +### Tailscale not working + +- Verify auth key is valid and unused +- Check Tailscale status: `sudo tailscale status` +- Enable Serve in Tailscale Admin Console + +## Next Steps + +- [HETZNER_SETUP.md](./HETZNER_SETUP.md) - Detailed Hetzner configuration +- [DIGITALOCEAN_SETUP.md](./DIGITALOCEAN_SETUP.md) - Detailed DO configuration +- [SECRETS.md](./SECRETS.md) - Advanced secrets management +- [TROUBLESHOOTING.md](./TROUBLESHOOTING.md) - Common issues and fixes \ No newline at end of file diff --git a/docs/HERMES_AUDIT_REPORT.md b/docs/HERMES_AUDIT_REPORT.md new file mode 100644 index 0000000..0db774f --- /dev/null +++ b/docs/HERMES_AUDIT_REPORT.md @@ -0,0 +1,203 @@ +# Hermes Deployment Audit Report + +## Issues Found + +During the audit of the Terraform project for Hermes Agent deployment, several critical issues were identified that would prevent Hermes from running properly: + +### 1. **Systemd Service Configuration Error** (CRITICAL) +**Problem:** The systemd service didn't specify the docker-compose file path +- `ExecStart=/usr/bin/docker compose up` without the `-f` flag +- The service couldn't find docker-compose.yml when running from an arbitrary directory +- No guarantee the service would change to the correct working directory + +**Impact:** Service would start but immediately fail or not find the compose file. + +**Fix:** Updated to: +```ini +ExecStart=/bin/sh -c 'cd /home/${admin_user} && exec docker compose -f docker-compose.yml up' +ExecStop=/bin/sh -c 'cd /home/${admin_user} && exec docker compose -f docker-compose.yml down' +``` + +### 2. **User Permissions Issue** (CRITICAL) +**Problem:** Service was configured to run as `User=${admin_user}` (non-root) +- Adding a user to the docker group with `usermod -aG docker` doesn't take effect for existing sessions +- The systemd service tries to use docker before the hermes user has proper permissions +- Would require a re-login to apply the docker group permissions + +**Impact:** Service runs as hermes user without the necessary docker group permissions, causing "permission denied" errors. + +**Fix:** Changed service to run as root (necessary for Docker): +```ini +User=root +``` +And ensured proper file ownership: +```bash +chown ${admin_user}:${admin_user} /home/${admin_user}/docker-compose.yml +chmod 644 /home/${admin_user}/docker-compose.yml +``` + +### 3. **Installation Order Issue** +**Problem:** Docker image was pulled before docker-compose-plugin was installed +- `docker pull` command succeeded (using legacy docker) +- But `docker compose` (the plugin) comes later +- If the pull failed, docker-compose-plugin wouldn't have been installed yet + +**Impact:** Potential race condition during bootstrap. + +**Fix:** Reordered runcmd to install docker-compose-plugin immediately after Docker: +```yaml +1. curl docker installer +2. apt-get install docker-compose-plugin # BEFORE pulling image +3. docker pull nousresearch/hermes-agent:latest +``` + +### 4. **No Docker Daemon Ready Check** (HIGH) +**Problem:** Script tried to pull images immediately after Docker installation +- Docker socket might not be ready +- Starting services before Docker is fully operational + +**Impact:** Timing-dependent failures, especially on slower systems. + +**Fix:** Added health checks and delays: +```bash +# Wait for Docker daemon to be ready +sleep 5 +docker ps > /dev/null || (sleep 10 && docker ps) +``` + +### 5. **No Service Startup Verification** (MEDIUM) +**Problem:** Service was started with no check that it actually came up +- If the service failed to start, deployment would complete successfully anyway +- User wouldn't know until they SSH in + +**Impact:** Silent failures that only become apparent when checking the server. + +**Fix:** Added verification: +```bash +# Verify service started +systemctl is-active hermes.service || systemctl status hermes.service +``` + +### 6. **Poor Error Logging** (MEDIUM) +**Problem:** systemd service logged to stdout but nothing captured the startup errors +- No journal entries with what went wrong +- No way to see Docker errors in the cloud-init logs + +**Impact:** Difficult to diagnose why the service failed. + +**Fix:** Added proper journal logging: +```ini +StandardOutput=journal +StandardError=journal +SyslogIdentifier=hermes +``` + +## Changes Made + +### Terraform Files Modified + +1. **templates/userdata-hermes.tpl** + - Fixed systemd service configuration + - Reordered runcmd operations + - Added Docker readiness checks and delays + - Enhanced health check script + - Added service startup verification + - Improved completion messages + +2. **docs/HERMES_DEBUGGING.md** (NEW) + - Comprehensive troubleshooting guide + - Common issues and solutions + - Diagnostic commands + - Manual start/stop procedures + - Discord connectivity testing + +3. **README.md** + - Added reference to HERMES_DEBUGGING.md documentation + +## Testing These Changes + +To test the fixes, you need to redeploy: + +```bash +# Option 1: Destroy and redeploy (cleanest) +terraform destroy +# Answer yes when prompted +source .env && terraform init && terraform apply + +# Option 2: Update existing (if keeping infrastructure) +source .env && terraform apply -auto-approve +``` + +After deployment, verify Hermes is running: + +```bash +# SSH into the server (username is 'hermes' or your override) +ssh hermes@ + +# Run the health check +/usr/local/bin/hermes-health-check.sh + +# Or manually verify +systemctl status hermes.service +docker ps +docker logs hermes +``` + +## Deployment Flow Now + +With the fixes, the cloud-init deployment flow is now: + +1. ✓ Update system packages +2. ✓ Create hermes user +3. ✓ Write configuration files (.env, config.yaml, docker-compose.yml, SOUL.md) +4. ✓ Write health check script +5. ✓ Write systemd service unit +6. ✓ Install Docker +7. ✓ Install docker-compose-plugin +8. ✓ Wait for Docker daemon to be ready +9. ✓ Pull Hermes image +10. ✓ Set proper permissions +11. ✓ Reload systemd +12. ✓ Enable hermes.service +13. ✓ Start systemd service (which runs docker-compose up) +14. ✓ Wait for startup +15. ✓ Verify service is active + +## Expected Behavior After Fix + +When you SSH into the server after deployment: + +```bash +$ systemctl status hermes.service +● hermes.service - Hermes Agent Service + Loaded: loaded (/etc/systemd/system/hermes.service; enabled; vendor preset: enabled) + Active: active (running) since ... + +$ docker ps +CONTAINER ID IMAGE STATUS +abc123 nousresearch/hermes-agent:latest Up 2 minutes + +$ docker logs hermes +[INFO] Hermes Agent starting... +[INFO] Discord bot initialized +... +``` + +And in Discord: +- Bot shows "online" status +- Responds to mentions in configured channels +- Respects user allowlist + +## Next Steps + +1. **Redeploy** with the fixed template +2. **Verify** using the health checks documented in HERMES_DEBUGGING.md +3. **Test Discord** connectivity by mentioning the bot in a channel +4. **Monitor logs** using `docker logs -f hermes` if issues occur + +## Additional Notes + +- The audit identified these issues by analyzing the template configuration and deployment flow +- Similar fixes should be applied if you have OpenClaw deployments +- The systemd service is now production-ready with proper error handling +- Health check script was significantly enhanced for better diagnostics diff --git a/docs/HERMES_DEBUGGING.md b/docs/HERMES_DEBUGGING.md new file mode 100644 index 0000000..a6e1f28 --- /dev/null +++ b/docs/HERMES_DEBUGGING.md @@ -0,0 +1,330 @@ +# Hermes Agent Debugging Guide + +This guide helps diagnose why Hermes Agent may not be running after Terraform deployment. + +## Quick Diagnostic Checklist + +### 1. Service Status + +```bash +# Check systemd service status +systemctl status hermes.service + +# View service logs +journalctl -u hermes.service -f + +# Check if container exists +docker ps -a | grep hermes + +# View container logs +docker logs hermes +``` + +### 2. Docker Health + +```bash +# Verify Docker is running +systemctl status docker + +# List containers +docker ps -a + +# Check Docker events (watch real-time) +docker events + +# Check docker socket permissions +ls -la /var/run/docker.sock +``` + +### 3. Directory and File Permissions + +```bash +# Check .hermes directory +ls -la ~/.hermes/ +ls -la ~/.hermes/.env +ls -la ~/docker-compose.yml + +# Check file contents +cat ~/.hermes/.env +cat ~/.hermes/config.yaml +cat ~/docker-compose.yml +``` + +## Common Issues and Fixes + +### Issue 1: "Hermes container not running" + +**Symptoms:** +- `docker ps` shows no hermes container +- `.hermes` folder exists but docker container won't start + +**Diagnosis:** +```bash +# Check service status +systemctl status hermes.service + +# Check recent logs +journalctl -u hermes.service -n 50 + +# Check docker logs more verbosely +docker logs hermes 2>&1 | tail -50 +``` + +**Root Causes:** +1. **Docker image not pulled properly** → Pull manually: + ```bash + docker pull nousresearch/hermes-agent:latest + ``` + +2. **Missing .env file** → Check if it exists and has content: + ```bash + ls -la ~/.hermes/.env + cat ~/.hermes/.env + ``` + +3. **Directory permission issues** → Fix permissions: + ```bash + sudo chown -R $(whoami):$(whoami) ~/.hermes + chmod 755 ~/.hermes + chmod 600 ~/.hermes/.env + ``` + +4. **Docker compose file not found** → Verify location: + ```bash + ls -la ~/docker-compose.yml + cat ~/docker-compose.yml + ``` + +5. **Port 18789 already in use** → Check: + ```bash + lsof -i :18789 + ``` + If occupied, either: + - Kill the process using it + - Change the port in docker-compose.yml + +### Issue 2: "Container starts but immediately exits" + +**Symptoms:** +- `docker ps` is empty but `docker ps -a` shows the container with "Exited" status +- Container stops within seconds of starting + +**Diagnosis:** +```bash +# View the exit code +docker ps -a | grep hermes + +# Get more detailed error logs +docker logs hermes +``` + +**Common Fixes:** +1. **Invalid YAML in config.yaml** → Validate syntax: + ```bash + python3 -c "import yaml; yaml.safe_load(open('~/.hermes/config.yaml'))" + ``` + +2. **Missing API keys** → Check: + ```bash + grep -E "OPENROUTER|DISCORD_BOT|BRAVE" ~/.hermes/.env + ``` + +3. **Invalid gateway token** → Verify: + ```bash + echo $HERMES_GATEWAY_TOKEN + ``` + +### Issue 3: "Docker daemon won't start" + +**Symptoms:** +- `systemctl status docker` shows failed/inactive +- `docker ps` returns "Cannot connect to Docker daemon" + +**Fixes:** +```bash +# Start Docker +sudo systemctl start docker + +# Enable on boot +sudo systemctl enable docker + +# Check Docker health +docker ps +``` + +### Issue 4: "Discord bot shows offline" + +**Symptoms:** +- Hermes is running (docker ps shows container) +- But Discord bot doesn't show "online" status in your server + +**Diagnosis:** +```bash +# Check if Discord configuration is loaded +grep -i discord ~/.hermes/.env +grep -i discord ~/.hermes/config.yaml + +# View container logs for Discord errors +docker logs hermes | grep -i discord +``` + +**Root Causes:** +1. **Invalid bot token** → Verify in .env: + ```bash + grep DISCORD_BOT_TOKEN ~/.hermes/.env + ``` + +2. **Wrong server ID** → Check config: + ```bash + grep -A 5 "discord_server_id" ~/.hermes/config.yaml + ``` + +3. **User IDs not in server** → Verify in allowlist: + ```bash + grep -A 10 "users:" ~/.hermes/config.yaml + ``` + +4. **Gateway not running** → Check port: + ```bash + lsof -i :18789 + ``` + +5. **Bot not in server** → Manual fix: + 1. Go to Discord Developer Portal + 2. Select your bot + 3. Copy OAuth2 URL with scopes: `bot`, `applications.commands` + 4. Click the URL to invite bot to your server + +### Issue 5: "Container gets killed after startup" + +**Symptoms:** +- Service shows active but container keeps restarting +- `docker logs` shows memory or resource errors + +**Fixes:** +```bash +# Check Docker stats +docker stats hermes + +# Check docker-compose.yml resource limits +grep -A 5 "deploy:" ~/docker-compose.yml + +# Increase memory limit if needed +# Edit ~/docker-compose.yml and increase memory value +nano ~/docker-compose.yml +``` + +## Verification Steps + +Once you believe Hermes is running, verify with: + +```bash +# Health check script (if it exists) +bash /usr/local/bin/hermes-health-check.sh + +# Manual health checks +echo "1. Service status:" +systemctl is-active hermes.service + +echo "2. Container running:" +docker ps | grep hermes + +echo "3. Port listening:" +netstat -tlnp | grep 18789 +``` + +## Manual Start/Stop + +If the systemd service isn't working: + +```bash +# Manual start +cd ~/ +docker compose -f docker-compose.yml up -d + +# Manual stop +cd ~/ +docker compose -f docker-compose.yml down + +# Manual logs +cd ~/ +docker compose -f docker-compose.yml logs -f +``` + +## Rebuilding from Scratch + +If nothing else works: + +```bash +# Stop everything +systemctl stop hermes.service +docker compose -f ~/docker-compose.yml down + +# Remove container and image +docker rm hermes 2>/dev/null || true +docker rmi nousresearch/hermes-agent:latest 2>/dev/null || true + +# Pull fresh image +docker pull nousresearch/hermes-agent:latest + +# Start service again +systemctl start hermes.service + +# Monitor startup +journalctl -u hermes.service -f +``` + +## Debug Mode + +For more verbose logging: + +```bash +# Watch service logs with timestamps +journalctl -u hermes.service -f --all + +# Watch docker logs continuously +docker logs -f --tail=50 hermes + +# Run docker compose in foreground (stops automated service) +cd ~/ +docker compose -f docker-compose.yml up +``` + +## Testing Discord Connectivity + +Once Hermes is running: + +```bash +# Send a test message to your Discord bot +# The bot should respond in the channel or via DM + +# Check if bot is responding to mentions +@hermes help + +# Or check logs for Discord activity +docker logs hermes | tail -100 +``` + +## Terraform Logs + +Check cloud-init logs on the server for deployment issues: + +```bash +# View cloud-init output +sudo cloud-init status +sudo cat /var/log/cloud-init-output.log + +# Check for specific errors +grep -i error /var/log/cloud-init-output.log +grep -i docker /var/log/cloud-init.log +``` + +## Getting Help + +If stuck, provide: +1. Output of `systemctl status hermes.service` +2. Output of `docker ps -a` +3. Last 50 lines of `docker logs hermes` +4. Contents of `~/.hermes/.env` (redact secrets) +5. Contents of `~/.hermes/config.yaml` +6. Output of `cloud-init status` diff --git a/docs/HETZNER_SETUP.md b/docs/HETZNER_SETUP.md new file mode 100644 index 0000000..b84c89b --- /dev/null +++ b/docs/HETZNER_SETUP.md @@ -0,0 +1,194 @@ +# Hetzner Cloud Setup + +Detailed guide for deploying OpenBoatmobile to Hetzner Cloud. + +## Why Hetzner? + +| Spec | Hetznercx23 | DigitalOcean s-2vcpu-4gb | +|------|-------------|-------------------------| +| vCPU | 2 | 2 | +| RAM | 4 GB | 4 GB | +| Disk | 80 GB NVMe | 80 GB SSD | +| Bandwidth | 20 TB included | 4 TB included | +| **Price** | **€4.49/mo** | **$24/mo** | + +Hetzner is ~70% cheaper for equivalent specs. + +## Create Hetzner Account + +1. Go to [Hetzner Cloud](https://www.hetzner.com/cloud) +2. Sign up (email verification required) +3. Add a payment method + +## Create API Token + +1. Go to [Hetzner Console](https://console.hetzner.cloud/) +2. Click your project (or create one) +3. Navigate to **Security** → **API Tokens** +4. Click **Create API Token** +5. Name it (e.g., "openclaw-terraform") +6. Permissions: **Read & Write** +7. Copy the token immediately (shown onlyonce) + +## Add SSH Key + +1. In Hetzner Console, go to **Security** → **SSH Keys** +2. Click **Add SSH Key** +3. Paste your public key contents: + ```bash + cat ~/.ssh/id_ed25519.pub + ``` +4. Give it a name you can remember (e.g., "laptop-2024") +5. Click **Add SSH Key** + +## Choose a Location + +Hetzner locations: + +| Code | Location | Continent | +|------|----------|-----------| +| `nbg1` | Nuremberg | Europe | +| `fsn1` | Falkenstein | Europe | +| `hel1` | Helsinki | Europe | +| `ash` | Ashburn, VA | North America | + +For US users: `ash` (Ashburn) has the best latency. + +## Configure OpenBoatmobile + +### Minimal Configuration + +In `terraform.tfvars`: + +```hcl +provider = "hetzner" +server_name = "my-agent" +server_type_hetzner = "cx23" +location_hetzner = "ash" + +# These come from environment: +# TF_VAR_hcloud_token +# TF_VAR_venice_api_key +# TF_VAR_ssh_key_names +``` + +### Server Types + +| Type | vCPU | RAM | Disk | Price | +|------|------|-----|------|-------| +| cx22 | 2 | 4 GB | 40 GB | €3.79/mo | +| **cx23** | 2 | 4 GB | 80 GB | **€4.49/mo** (recommended) | +| cpx21 | 3 | 4 GB | 80 GB | €5.99/mo | +| cpx31 | 4 | 8 GB | 160 GB | €8.99/mo | + +The cx23 is the sweet spot for OpenClaw: enough RAM for Node.js + LLM contexts, affordable price. + +## Deploy + +```bash +# Load secrets +source .env + +# Initialize (first time only) +terraform init + +# Preview changes +terraform plan + +# Deploy +terraform apply +``` + +## Post-Deployment + +Terraform outputs your server IP: + +``` +server_ip = "123.45.67.89" +ssh_command = "ssh openclaw@123.45.67.89" # or "ssh hermes@123.45.67.89" for Hermes +``` + +### Connect + +```bash +# Username is 'openclaw' or 'hermes' depending on framework +ssh @123.45.67.89 +``` + +### Check Cloud-Init Status + +On the server: + +```bash +# Check if cloud-init is still running +cloud-init status + +# If waiting, you can watch progress: +tail -f /var/log/cloud-init-output.log +``` + +### Run OpenClaw Onboarding + +```bash +openclaw onboard --install-daemon +``` + +### Verify Gateway + +```bash +systemctl status openclaw-gateway +``` + +## Firewall Rules + +OpenBoatmobile creates a Hetzner firewall with: + +| Direction | Port | Source | +|-----------|------|--------| +| Inbound | 22 (SSH) | Configured IPs | +| Outbound | All | Any | + +To restrict SSH to your IP: + +```bash +TF_VAR_ssh_allowed_ips='["your.public.ip/32", "another.ip/32"]' +``` + +## Cleanup + +To destroy your deployment: + +```bash +terraform destroy +``` + +**Note:** This deletes the server and all data. Backup anything important first. + +## Troubleshooting + +### "API Token invalid" + +- Copy the token again (shown only once) +- Check for trailing spaces in `.env` +- Verify token has Read & Write permissions + +### "SSH Key not found" + +- The key name must match exactly what you entered in Hetzner Console +- Case-sensitive +- Use the name, not the fingerprint + +### Server shows but can't SSH + +- Wait 2-3 minutes for cloud-init +- Check your IP is in `ssh_allowed_ips` +- Verify the key is added to your agent: `ssh-add -l` + +### Cloud-init stuck + +```bash +# On the server +cloud-init status --wait +# Or check logs +tail -f /var/log/cloud-init-output.log +``` \ No newline at end of file diff --git a/docs/SECRETS.md b/docs/SECRETS.md new file mode 100644 index 0000000..9ec536d --- /dev/null +++ b/docs/SECRETS.md @@ -0,0 +1,138 @@ +# Secrets Management + +OpenBoatmobile uses Terraform's native secrets handling: environment variables with the `TF_VAR_` prefix. + +## Why Environment Variables? + +| Approach | Pros | Cons | +|----------|------|------| +| `TF_VAR_*` env vars | Standard Terraform, never in git, works with CI/CD | Must source before each session | +| `.tfvars` file | Easy to edit | Easy to accidentally commit secrets | +| HashiCorp Vault | Enterprise-grade | Complex setup, overkill for solo use | +| SOPS (encrypted files) | Git-tracked encrypted secrets | Extra tooling required | + +We use `TF_VAR_*` because it's the Terraform standard and keeps secrets out of git by default. + +## The .env File + +The `.env.example` template lists all configurable variables: + +```bash +# Copy to .env and fill in your values +cp .env.example .env +``` + +**Never commit `.env`:** It's in `.gitignore` by default. + +## Loading Secrets + +Before running Terraform: + +```bash +source .env +``` + +This exports all variables to your shell. Terraform automatically reads `TF_VAR_*` variables. + +## Required Secrets + +| Variable | Description | How to Get | +|----------|-------------|------------| +| `TF_VAR_hcloud_token` | Hetzner API token | [Hetzner Console](https://console.hetzner.cloud/) → Security → API Tokens → Create Token | +| `TF_VAR_venice_api_key` | Venice AI API key | [Venice.ai](https://venice.ai) → Settings → API Keys | +| `TF_VAR_ssh_key_names` | SSH key name(s) | Name you gave the key in Hetzner Console | + +## Optional Secrets + +| Variable | Description | How to Get | +|----------|-------------|------------| +| `TF_VAR_tailscale_auth_key` | Tailscale auth key | [Tailscale Admin](https://login.tailscale.com/admin/settings/keys) → Create Key | +| `TF_VAR_discord_bot_token` | Discord bot token | [Discord Dev Portal](https://discord.com/developers/applications) | +| `TF_VAR_brave_search_api_key` | Brave Search API key | [Brave Search API](https://api.search.brave.com/app/keys) | +| `TF_VAR_do_token` | DigitalOcean API token | [DO API Settings](https://cloud.digitalocean.com/account/api/tokens) | + +## SSH Key Setup + +### Hetzner + +1. Generate a key (if you don't have one): + ```bash + ssh-keygen -t ed25519 -C "your@email.com" + ``` + +2. Add to Hetzner Console: + - Go to [Hetzner Console](https://console.hetzner.cloud/) → Security → SSH Keys + - Click "Add SSH Key" + - Paste the contents of `~/.ssh/id_ed25519.pub` + - Give it a memorable name (e.g., "laptop-ed25519") + +3. Use the name in your config: + ```bash + TF_VAR_ssh_key_names='["laptop-ed25519"]' + ``` + +### DigitalOcean + +1. Same key generation as above + +2. Add to DigitalOcean: + - Go to [DO Settings](https://cloud.digitalocean.com/account/security) + - Click "Add SSH Key" + - Paste the public key contents + +3. Use the fingerprint: + ```bash + # Get the fingerprint + ssh-keygen -lf ~/.ssh/id_ed25519.pub + # Example output: 256 SHA256:xxx... your@email.com (ED25519) + # The fingerprint is the part after SHA256: + TF_VAR_ssh_key_fingerprints='["abc123..."]' + ``` + +## CI/CD Integration + +For GitHub Actions or similar: + +```yaml +# .github/workflows/deploy.yml +env: + TF_VAR_hcloud_token: ${{ secrets.HCLOUD_TOKEN }} + TF_VAR_venice_api_key: ${{ secrets.VENICE_API_KEY }} + TF_VAR_ssh_key_names: '["deploy-key"]' +``` + +## Security Best Practices + +1. **Never commit `.env` or `.tfvars` with secrets** + - These files are in `.gitignore` by default + - Double-check before committing + +2. **Use least-privilege API tokens** + - Hetzner: Create project-specific tokens + - Venice: Regenerate keys periodically + +3. **Rotate secrets if compromised** + - Hetzner: Delete old token, create new one + - Venice: Regenerate in settings + +4. **Use Tailscale for remote access** + - No public HTTPS exposure + - Tailnet provides encryption and auth + +## Advanced: SOPS Integration + +For teams that want git-tracked encrypted secrets: + +1. Install SOPS: `brew install sops` or `apt install sops` + +2. Create an encrypted tfvars: + ```bash + sops --encrypt --input-type binary --output-type binary secrets.tfvars > secrets.tfvars.encrypted + ``` + +3. Decrypt at apply time: + ```bash + sops --decrypt secrets.tfvars.encrypted | terraform apply -var-file=- + ``` + +This is overkill for solo use but useful for teams. \ No newline at end of file diff --git a/docs/SSH_GUIDE.md b/docs/SSH_GUIDE.md new file mode 100644 index 0000000..00958aa --- /dev/null +++ b/docs/SSH_GUIDE.md @@ -0,0 +1,274 @@ +# SSH for Clients + +**A simple guide to connecting to your server remotely.** + +## What is SSH? + +SSH (Secure Shell) is a way to control a computer from somewhere else. Think of it like remotely driving a car — you're in the driver's seat, but the car is somewhere else. + +When you SSH into a server, you get a command line on that server. You can run commands, install software, check logs — everything you could do if you were physically sitting at that computer. + +## Why Do You Need It? + +For your OpenBoatmobile deployment, SSH is how you: + +- Check if everything is running correctly +- View logs when something goes wrong +- Run maintenance commands +- Update configurations + +## The Key Concept: Lock and Key + +SSH uses two files that work together: + +| File | Analogy | Where it lives | +|------|---------|----------------| +| **Private key** | Your house key | Your computer, never share | +| **Public key** | Your lock | The server, you can share | + +**The private key stays with you.** The public key goes on the server. + +When you connect, SSH checks: *Does your private key match the public key on the server?* If yes, you're allowed in. If no, access denied. + +**Important:** Your private key is like your house key. Don't give it to anyone. Don't email it. Don't upload it anywhere. + +## Step-by-Step: Setting Up SSH + +### macOS / Linux + +**1. Generate your keys:** + +Open Terminal and run: + +```bash +ssh-keygen -t ed25519 -C "your-email@example.com" +``` + +When prompted: +- Press Enter to accept the default location (`~/.ssh/id_ed25519`) +- Press Enter twice for no passphrase (or set one if you want extra security) + +**2. See your public key:** + +```bash +cat ~/.ssh/id_ed25519.pub +``` + +Copy the entire output — it starts with `ssh-ed25519` and ends with your email. + +**3. Add your key to the cloud provider:** + +**Hetzner:** +1. Go to [console.hetzner.cloud](https://console.hetzner.cloud/) +2. Navigate to Security → SSH Keys +3. Click "Add SSH Key" +4. Paste your public key +5. Give it a name (like "my-laptop") +6. Click "Add SSH Key" + +**DigitalOcean:** +1. Go to [cloud.digitalocean.com](https://cloud.digitalocean.com/) +2. Navigate to Account → Security +3. Click "Add SSH Key" +4. Paste your public key +5. Give it a name +6. Click "Add SSH Key" + +**4. Test your connection:** + +After your server is deployed (via Terraform), connect: + +```bash +# Username is 'openclaw' or 'hermes' depending on your framework +ssh @your-server-ip +``` + +If successful, you'll see a command prompt from the remote server. + +### Windows + +**Option 1: PowerShell (Windows 10/11)** + +Open PowerShell and follow the macOS/Linux steps above. Windows now includes OpenSSH by default. + +**Option 2: PuTTY (older Windows)** + +1. Download [PuTTYgen](https://www.puttygen.com/) +2. Open PuTTYgen +3. Click "Generate" and move your mouse randomly +4. Click "Save private key" — save as `my-key.ppk` +5. Copy the text in "Public key for pasting" — this is your public key +6. Add this public key to your cloud provider (steps above) + +To connect: +1. Open PuTTY +2. In "Host Name", enter: `@your-server-ip` (username is 'openclaw' or 'hermes' depending on framework) +3. Go to Connection → SSH → Auth +4. Browse to your `.ppk` file +5. Click "Open" + +### Key Already Exists? + +If you've used SSH before (for GitHub, GitLab, etc.), you might already have a key: + +```bash +# Check for existing keys +ls ~/.ssh + +# If you see id_ed25519.pub, you're good +cat ~/.ssh/id_ed25519.pub +``` + +Use this existing key — no need to generate a new one. + +## Connecting to Your Server + +When Terraform finishes, it outputs your server IP: + +``` +server_ip = "123.45.67.89" +ssh_command = "ssh openclaw@123.45.67.89" # or "ssh hermes@123.45.67.89" +``` + +**Connect (username is 'openclaw' or 'hermes' based on framework):** + +```bash +ssh @123.45.67.89 +``` + +**First time?** You'll see: + +``` +The authenticity of host '123.45.67.89' can't be established. +ED25519 key fingerprint is SHA256:xxxxx... +Are you sure you want to continue connecting (yes/no/[fingerprint])? +``` + +Type `yes` and press Enter. This happens once per server. + +**Successful connection looks like:** + +``` +Welcome to Ubuntu 24.04 LTS +openclaw@openclaw-gateway:~$ +``` + +You're now on the server! The prompt shows `username@hostname`. + +## Common Commands + +Once connected, here are useful commands: + +```bash +# Check if OpenClaw is running +systemctl status openclaw-gateway + +# View logs in real-time +journalctl -u openclaw-gateway -f + +# Check Tailscale status (if using Tailscale) +sudo tailscale status + +# Check disk space +df -h + +# Check memory +free -h + +# Exit the server +exit +``` + +## Troubleshooting + +### "Permission denied (publickey)" + +**Cause:** Your public key isn't on the server, or you're using the wrong username. + +**Fix:** +1. Check your public key is added to the cloud provider +2. Make sure you're using `openclaw` as the username (not your personal username) +3. If your key is in a non-standard location: `ssh -i ~/.ssh/my-key openclaw@server-ip` + +### "Connection timed out" + +**Cause:** Server isn't running, or firewall is blocking you. + +**Fix:** +1. Check the server is running in your cloud console +2. Wait 2-3 minutes after deployment (cloud-init takes time) +3. Check your IP is in `ssh_allowed_ips` (or use `["0.0.0.0/0"]` for any IP) + +### "Host key verification failed" + +**Cause:** You've connected to this IP before, but the server was replaced. + +**Fix:** +```bash +ssh-keygen -R 123.45.67.89 +ssh openclaw@123.45.67.89 +``` + +### "No such file or directory" for key + +**Cause:** Your key is in a different location. + +**Fix:** +```bash +# Find your key +find ~ -name "id_ed25519*" 2>/dev/null + +# Use the correct path +ssh -i /path/to/your/key openclaw@server-ip +``` + +## Security Best Practices + +| Practice | Why | +|----------|-----| +| Never share your private key | It's your identity. Anyone with it can access your servers. | +| Don't email your private key | Email isn't secure. | +| Use different keys for different purposes | If one is compromised, others remain safe. | +| Use a passphrase (optional) | Extra layer of protection if someone gets your key file. | +| Disable password login | Passwords can be guessed. Keys can't. | + +## What if I Lose My Key? + +If you lose your private key, you can't SSH in. Your options: + +1. **Use the cloud console** — Most providers have a "Console" or "VNC" option in the web interface. This gives you direct access. + +2. **Add a new key** — Through the cloud console, you can add a new SSH key. + +3. **Recreate the server** — Use `terraform destroy` and `terraform apply` again. Data will be lost. + +## Need Help? + +- Check the server logs: `journalctl -u openclaw-gateway -n50` +- Check cloud-init logs: `tail -f /var/log/cloud-init-output.log` +- See [TROUBLESHOOTING.md](./TROUBLESHOOTING.md) for more common issues + +## Quick Reference + +```bash +# Generate a new key +ssh-keygen -t ed25519 -C "your-email@example.com" + +# View your public key +cat ~/.ssh/id_ed25519.pub + +# Connect to server +ssh openclaw@server-ip + +# Use a specific key file +ssh -i ~/.ssh/my-key openclaw@server-ip + +# Remove a server from known hosts +ssh-keygen -R server-ip + +# Copy files to server +scp myfile.txt openclaw@server-ip:/home/openclaw/ + +# Copy files from server +scp openclaw@server-ip:/home/openclaw/file.txt ./ +``` \ No newline at end of file diff --git a/docs/TAILSCALE_SETUP.md b/docs/TAILSCALE_SETUP.md new file mode 100644 index 0000000..55b438f --- /dev/null +++ b/docs/TAILSCALE_SETUP.md @@ -0,0 +1,167 @@ +# Tailscale Setup + +Tailscale provides secure remote access without exposing ports to the internet. + +## Why Tailscale? + +| Approach | Pros | Cons | +|----------|------|------| +| Tailscale | Free for personal use, encrypted, no port forwarding | Requires Tailscale account | +| SSH tunnel | No dependencies | Local only, manual setup | +| Public HTTPS | Works anywhere | Requires domain, SSL cert, security maintenance | + +**Recommended:** Use Tailscale for production deployments. + +## Prerequisites + +- A Tailscale account ([sign up free](https://tailscale.com/)) +- An auth key from the admin console + +## Create Auth Key + +1. Go to [Tailscale Admin](https://login.tailscale.com/admin/settings/keys) +2. Click **Generate auth key** +3. Settings: + - **Description:** "OpenBoatmobile-2024" + - **Reusable:** No (one server per key) + - **Ephemeral:** No (server should persist) + - **Tags:** Optional (e.g., `tag:servers`) +4. Click **Generate key** +5. Copy the key immediately (starts with `tskey-auth-`) + +## Add to Configuration + +In `.env`: + +```bash +TF_VAR_enable_tailscale=true +TF_VAR_tailscale_auth_key=tskey-auth-xxxxx +``` + +Or in `terraform.tfvars`: + +```hcl +enable_tailscale = true +tailscale_auth_key = "tskey-auth-xxxxx" +``` + +## Post-Deployment + +After Terraform completes: + +### 1. Enable Tailscale Serve + +SSH into your server and run: + +```bash +sudo tailscale serve --bg 18789 +``` + +This exposes the OpenClaw gateway on your tailnet. + +### 2. Enable "Serve" in Tailscale Admin + +1. Go to [Tailscale Admin → Serve](https://login.tailscale.com/admin/settings/serve) +2. Enable the **Serve** feature +3. This allows serving HTTPS on your tailnet + +### 3. Access Your Gateway + +Visit: `https://..ts.net/` + +Where: +- `` is your server name (default: `openclaw-gateway`) +- `` is your tailnet name (e.g., `dragonfish-basilisk`) + +Example: `https://openclaw-gateway.dragonfish-basilisk.ts.net/` + +## Verify Connection + +### On the Server + +```bash +# Check Tailscale status +sudo tailscale status + +# Check serve status +sudo tailscale serve status + +# Resolve a tailnet identity +tailscale whois +``` + +### From Your Machine + +1. Install Tailscale: [tailscale.com/download](https://tailscale.com/download) +2. Log in to the same account +3. Ping your server: `tailscale ping ` +4. Open the gateway in your browser + +## Security Model + +### Solo Tailnet (Recommended) + +If you're the only person on your tailnet: + +```hcl +# In terraform.tfvars (or via openclaw.json after deployment) +# The cloud-init config sets this automatically +``` + +- Your tailnet = your trust boundary +- No per-browser pairing required +- Only devices you authorize can access + +### Multi-User Tailnet + +If you share your tailnet with others: + +1. Remove `dangerouslyDisableDeviceAuth` from the gateway config +2. Each browser must complete device pairing +3. Pairing requires approval: `openclaw pairing approve device ` + +## Troubleshooting + +### "Tailscale serve failed" + +```bash +# Check if Tailscale is running +sudo tailscale status + +# If not connected, reconnect +sudo tailscale up --authkey=tskey-auth-xxxxx +``` + +### "Serve platform not enabled" + +- Go to [Tailscale Admin → Serve](https://login.tailscale.com/admin/settings/serve) +- Enable the Serve feature + +### "Connection refused on tailnet" + +```bash +# Verify gateway is listening +sudo lsof -i :18789 + +# If not listening, restart +sudo systemctl restart openclaw-gateway +``` + +### Gateway not accessible from browser + +1. Verify Tailscale serve is running: `sudo tailscale serve status` +2. Check allowed origins in gateway config +3. Try accessing via `http://100.x.x.x:18789` (Tailscale IP) + +## Advanced: Funnel (Public Access) + +If you need public access (not recommended for most use cases): + +```bash +# Enable Funnel for public HTTPS +sudo tailscale funnel --bg 18789 +``` + +This creates a public URL: `https://.tailnet.ts.net/` + +**Warning:** This exposes your gateway to the internet. Use with caution. \ No newline at end of file diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md new file mode 100644 index 0000000..12fa9b8 --- /dev/null +++ b/docs/TROUBLESHOOTING.md @@ -0,0 +1,304 @@ +# Troubleshooting + +Common issues and their solutions. + +## Deployment Issues + +### Terraform Error: "Provider produced inconsistent result" + +**Cause:** State file conflicts or provider version mismatch. + +**Solution:** +```bash +terraform init -upgrade +terraform plan -refresh=false +``` + +### Terraform Error: "API Token invalid" + +**Hetzner:** +- Token must have Read & Write permissions +- Copy immediately after creation (shown only once) +- Check for trailing spaces in `.env` + +**DigitalOcean:** +- Regenerate token in DO Console +- Verify token has Read & Write scope + +### Terraform Error: "SSH Key not found" + +**Hetzner:** +- Key name must match exactly as shown in Console +- Case-sensitive +- Use the name: `TF_VAR_ssh_key_names='["my-key-name"]'` + +**DigitalOcean:** +- Use the fingerprint, not the name +- Get fingerprint: `ssh-keygen -lf ~/.ssh/id_ed25519.pub` +- Format: `TF_VAR_ssh_key_fingerprints='["abc123..."]'` + +### Terraform State Locked + +**Cause:** Previous `terraform apply` crashed or is still running. + +**Solution:** +```bash +# Force unlock (if sure no other apply is running) +terraform force-unlock +``` + +## Connection Issues + +### SSH Connection Refused + +**Causes:** +1. Cloud-init still running +2. Firewall blocking your IP +3. Wrong SSH key + +**Solutions:** + +1. Wait2-3 minutes after deployment, then retry +2. Check cloud-init status: + ```bash + # On the server + cloud-init status + tail -f /var/log/cloud-init-output.log + ``` +3. Restrict firewall to your IP: + ```bash + TF_VAR_ssh_allowed_ips='["your.public.ip/32"]' + ``` +4. Verify SSH key: + ```bash + ssh-add -l # Should show your key + ssh -v openclaw@ # Verbose output + ``` + +### SSH Permission Denied + +**Causes:** +1. Wrong username +2. Wrong SSH key +3. Key not added to agent + +**Solutions:** + +1. Username is `openclaw` (not `root`): + ```bash + ssh @ # username is 'openclaw' or 'hermes' depending on framework + ``` + +2. Verify key is correct: + ```bash + ssh -i ~/.ssh/id_ed25519 openclaw@ + ``` + +3. Add key to agent: + ```bash + ssh-add ~/.ssh/id_ed25519 + ``` + +### Connection Times Out + +**Causes:** +1. Wrong IP +2. Server not running +3. Network issues + +**Solutions:** + +1. Verify IP from Terraform output: + ```bash + terraform output server_ip + ``` + +2. Check server status in cloud console + +3. Try from different network (e.g., mobile hotspot) + +## Cloud-Init Issues + +### Cloud-init Stuck + +**Check status:** +```bash +cloud-init status --wait +``` + +**Check logs:** +```bash +tail -f /var/log/cloud-init-output.log +``` + +**Common issues:** +- Network timeout downloading packages +- Package repository issues +- Disk space exhaustion + +### OpenClaw Command Not Found + +**Cause:** Cloud-init hasn't finished or failed. + +**Solution:** +```bash +# Check if Node.js is installed +node --version + +# Check if Node.js setup ran +ls /etc/apt/sources.list.d/nodesource.list + +# Manually install if needed +curl -fsSL https://deb.nodesource.com/setup_24.x | sudo bash - +sudo apt-get install -y nodejs +``` + +### Disk Full + +**Cause:** Small instance with lots of logs. + +**Solution:** +```bash +# Check disk usage +df -h + +# Clean package cache +sudo apt-get clean + +# Remove old logs +sudo journalctl --vacuum-size=100M +``` + +## Tailscale Issues + +### Tailscale Not Connected + +**Check status:** +```bash +sudo tailscale status +``` + +**Reconnect:** +```bash +sudo tailscale up --authkey=tskey-auth-xxxxx +``` + +### "Serve platform not enabled" + +**Solution:** +1. Go to [Tailscale Admin → Serve](https://login.tailscale.com/admin/settings/serve) +2. Enable the Serve feature + +### Gateway Not Accessible on Tailnet + +**Check gateway:** +```bash +sudo lsof -i :18789 +sudo systemctl status openclaw-gateway +``` + +**Check serve:** +```bash +sudo tailscale serve status +``` + +**Verify firewall:** +```bash +sudo ufw status +# Should show 18789 allowed on tailscale0 +``` + +## Discord Issues + +### Bot Doesn't Respond + +**Check:** +1. Bot token is correct +2. Message Content Intent is enabled +3. Bot is in your server +4. Server ID and User ID are correct + +**Debug:** +```bash +# Check gateway logs +journalctl -u openclaw-gateway -f | grep -i discord +``` + +### "Unauthorized" in Logs + +**Cause:** Your user ID is not in the allowlist. + +**Solution:** +Edit `~/.openclaw/openclaw.json` and add your Discord user ID: +```json +{ + "channels": { + "discord": { + "guilds": { + "SERVER_ID": { + "users": ["YOUR_USER_ID"] + } + } + } + } +} +``` + +### Gateway Shows Pairing Code + +**Solution:** +```bash +# On the server +openclaw pairing approve device +``` + +## Performance Issues + +### Gateway Slow to Respond + +**Causes:** +1. High model load +2. Network latency +3. Instance too small + +**Solutions:** +1. Check model usage: + ```bash + top + htop + ``` + +2. Check network: + ```bash + ping api.venice.ai + ``` + +3. Upgrade instance: + ```bash + # Edit terraform.tfvars + server_type_hetzner = "cpx21" # More CPU/RAM + terraform apply + ``` + +### Memory Exhaustion + +**Check:** +```bash +free -h +``` + +**Solution:** +```bash +# Add swap (if not present) +sudo fallocate -l 2G /swapfile +sudo chmod 600 /swapfile +sudo mkswap /swapfile +sudo swapon /swapfile +echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab +``` + +## Getting Help + +1. Check OpenClaw docs: [docs.openclaw.ai](https://docs.openclaw.ai) +2. Search GitHub issues: [github.com/openclaw/openclaw](https://github.com/openclaw/openclaw) +3. Community Discord: [discord.com/invite/clawd](https://discord.com/invite/clawd) \ No newline at end of file diff --git a/examples/terraform.tfvars.example b/examples/terraform.tfvars.example new file mode 100644 index 0000000..f7d8d4d --- /dev/null +++ b/examples/terraform.tfvars.example @@ -0,0 +1,126 @@ +# OpenBoatmobile Example Configurations +# Copy one of these to terraform.tfvars and customize + +## Minimal Hetzner Deployment +## ~€4.49/mo (cx23: 2 vCPU, 4 GB RAM) + +cloud_provider = "hetzner" +hcloud_token = "your-hetzner-api-token" +server_name = "openclaw-gateway" +server_type_hetzner = "cx23" +location_hetzner = "ash" + +ssh_key_names = ["your-ssh-key-name"] +admin_user = "" # Defaults to framework name (hermes or openclaw) +docker_enabled = true # Set to false for direct installation + +venice_api_key = "your-venice-api-key" +default_model = "venice/zai-org-glm-5" + +enable_tailscale = true +tailscale_auth_key = "your-tailscale-auth-key" + +# Leave Discord empty for initial deployment +# Configure after SSH access +discord_bot_token = "" +discord_server_id = "" +discord_user_id = [] + +## Minimal DigitalOcean Deployment +## ~$24/mo (s-2vcpu-4gb: 2 vCPU, 4 GB RAM) +# +# cloud_provider = "digitalocean" +# do_token = "your-digitalocean-api-token" +# server_name = "openclaw-gateway" +# droplet_size_digitalocean = "s-2vcpu-4gb" +# region_digitalocean = "nyc3" +# +# ssh_key_fingerprints = ["aa:bb:cc:dd:ee:ff:..."] +# admin_user = "" # Defaults to framework name (hermes or openclaw) +# docker_enabled = true # Set to false for direct installation +# +# venice_api_key = "your-venice-api-key" +# default_model = "venice/zai-org-glm-5" +# +# enable_tailscale = true +# tailscale_auth_key = "your-tailscale-auth-key" +# +# discord_bot_token = "" +# discord_server_id = "" +# discord_user_id = [] + +## Full Configuration Example +## All options documented +# +# # ============================================================================= +# # PROVIDER (required) +# # ============================================================================= +# cloud_provider = "hetzner" # or "digitalocean" +# +# # ============================================================================= +# # API TOKENS (required - set via environment or here) +# # ============================================================================= +# hcloud_token = "..." # for Hetzner +# do_token = "..." # for DigitalOcean +# +# # ============================================================================= +# # SERVER CONFIGURATION +# # ============================================================================= +# server_name = "openclaw-gateway" +# server_type_hetzner = "cx23" # or: cpx21, cx24, cpx31 +# location_hetzner = "ash" # or: nbg1, fsn1, hel1 +# droplet_size_digitalocean = "s-2vcpu-4gb" # for DO +# region_digitalocean = "nyc3" # for DO: nyc3, sfo2, ams3, etc. +# +# # ============================================================================= +# # SSH CONFIGURATION +# # ============================================================================= +# # For Hetzner: Name of key in Hetzner Cloud Console +# ssh_key_names = ["my-ssh-key"] +# # For DigitalOcean: Fingerprint of key +# # ssh_key_fingerprints = ["aa:bb:cc:dd:ee:ff:..."] +# +# ssh_port = 22 +# ssh_allowed_ips = ["0.0.0.0/0", "::/0"] # or restrict to your IPs +# admin_user = "" # Defaults to framework name (hermes or openclaw)docker_enabled = true # Set to false for direct installation# +# # ============================================================================= +# # OPENCLAW CONFIGURATION +# # ============================================================================= +# openclaw_version = "lts" # or "latest", or "2026.3.23-2" +# node_version = "24" +# agent_name = "main" +# agent_timezone = "UTC" +# enable_swap = true +# swap_size_gb = 2 +# +# # ============================================================================= +# # SECURITY +# # ============================================================================= +# enable_fail2ban = true +# enable_unattended_upgrades = true +# +# # ============================================================================= +# # TAILSCALE (recommended) +# # ============================================================================= +# enable_tailscale = true +# tailscale_auth_key = "tskey-auth-..." +# +# # ============================================================================= +# # API KEYS +# # ============================================================================= +# venice_api_key = "..." +# default_model = "venice/zai-org-glm-5" +# brave_search_api_key = "..." +# +# # ============================================================================= +# # DISCORD (optional - can configure after deployment) +# # ============================================================================= +# discord_bot_token = "" +# discord_server_id = "" +# discord_user_id = [] +# +# # ============================================================================= +# # PROJECT METADATA +# # ============================================================================= +# project_name = "OpenBoatmobile" +# environment = "production" \ No newline at end of file diff --git a/hermes-openclaw.json b/hermes-openclaw.json new file mode 100644 index 0000000..494a492 --- /dev/null +++ b/hermes-openclaw.json @@ -0,0 +1,122 @@ +{ + "auth": { + "profiles": { + "venice:default": { + "provider": "venice", + "mode": "api_key" + } + } + }, + "models": { + "mode": "merge", + "providers": { + "venice": { + "baseUrl": "https://api.venice.ai/api/v1", + "api": "openai-completions", + "models": [ + { + "id": "zai-org-glm-5", + "name": "GLM 5", + "reasoning": true, + "input": ["text"], + "contextWindow": 202752, + "maxTokens": 8192 + }, + { + "id": "zai-org-glm-4.7", + "name": "GLM 4.7", + "reasoning": true, + "input": ["text"], + "contextWindow": 202752, + "maxTokens": 8192 + }, + { + "id": "olafangensan-glm-4.7-flash-heretic", + "name": "GLM 4.7 Flash Heretic", + "reasoning": true, + "input": ["text"], + "contextWindow": 131072, + "maxTokens": 8192 + }, + { + "id": "kimi-k2-5", + "name": "Kimi K2.5", + "reasoning": true, + "input": ["text", "image"], + "contextWindow": 256000, + "maxTokens": 65536 + }, + { + "id": "deepseek-v3.2", + "name": "DeepSeek V3.2", + "reasoning": true, + "input": ["text"], + "contextWindow": 64000, + "maxTokens": 8192 + }, + { + "id": "qwen3-coder-480b-a35b-instruct", + "name": "Qwen3 Coder 480B", + "reasoning": true, + "input": ["text"], + "contextWindow": 131072, + "maxTokens": 16384 + } + ] + } + } + }, + "agents": { + "defaults": { + "model": { + "primary": "venice/olafangensan-glm-4.7-flash-heretic", + "fallbacks": ["venice/zai-org-glm-5"] + }, + "workspace": "/home/openclaw/.openclaw/workspace" + }, + "list": [ + { + "id": "hermes", + "default": true, + "workspace": "/home/openclaw/.openclaw/workspace" + } + ] + }, + "tools": { + "web": { + "search": { + "enabled": true, + "provider": "brave", + "apiKey": "${BRAVE_SEARCH_API_KEY}" + }, + "fetch": { "enabled": true } + }, + "exec": { + "security": "allowlist", + "ask": "on-miss" + } + }, + "messages": { + "queue": { "mode": "collect" }, + "ackReactionScope": "all" + }, + "channels": { + "discord": { + "enabled": true, + "token": "${DISCORD_BOT_TOKEN}", + "groupPolicy": "allowlist", + "guilds": { + "YOUR_GUILD_ID": { + "requireMention": false, + "users": ["YOUR_USER_ID"], + "channels": { "*": { "allow": true } } + } + } + } + }, + "gateway": { + "port": 18789, + "mode": "local", + "bind": "loopback" + } +} \ No newline at end of file diff --git a/hetzner.tf b/hetzner.tf new file mode 100644 index 0000000..a892afa --- /dev/null +++ b/hetzner.tf @@ -0,0 +1,124 @@ +# Hetzner Cloud Provider Resources +# Conditionally created when var.cloud_provider == "hetzner" + +# ============================================================================= +# SSH KEY DATA SOURCE +# ============================================================================= + +data "hcloud_ssh_key" "keys" { + for_each = toset(var.ssh_key_names) + name = each.key +} + +# ============================================================================= +# NETWORK (Optional - for multi-server deployments) +# ============================================================================= + +resource "hcloud_network" "agent" { + count = var.create_network && local.is_hetzner ? 1 : 0 + + name = "${var.server_name}-network" + ip_range = var.network_ip_range +} + +resource "hcloud_network_subnet" "agent" { + count = var.create_network && local.is_hetzner ? 1 : 0 + + network_id = hcloud_network.agent[0].id + type = "cloud" + network_zone = var.network_zone + ip_range = cidrsubnet(var.network_ip_range, 8, 0) +} + +# ============================================================================= +# FIREWALL +# ============================================================================= + +resource "hcloud_firewall" "agent" { + count = local.is_hetzner ? 1 : 0 + + name = "${var.server_name}-firewall" + + # Inbound: SSH only + rule { + direction = "in" + protocol = "tcp" + port = tostring(var.ssh_port) + source_ips = var.ssh_allowed_ips + } + + # Outbound: Allow all + rule { + direction = "out" + protocol = "tcp" + port = "1-65535" + destination_ips = ["0.0.0.0/0", "::/0"] + } + + rule { + direction = "out" + protocol = "udp" + port = "1-65535" + destination_ips = ["0.0.0.0/0", "::/0"] + } + + rule { + direction = "out" + protocol = "icmp" + destination_ips = ["0.0.0.0/0", "::/0"] + } +} + +# ============================================================================= +# SERVER +# ============================================================================= + +resource "hcloud_server" "agent" { + count = local.is_hetzner ? 1 : 0 + + name = var.server_name + image = var.server_image + server_type = var.server_type_hetzner + location = var.location_hetzner + + ssh_keys = [for key in data.hcloud_ssh_key.keys : key.id] + + # Network attachment (if enabled) + dynamic "network" { + for_each = var.create_network ? [1] : [] + content { + network_id = hcloud_network.agent[0].id + } + } + + # Labels for organization + labels = { + project = var.project_name + environment = var.environment + framework = var.agent_framework + managed = "terraform" + } + + # Firewall attachment + firewall_ids = [hcloud_firewall.agent[0].id] + + # Cloud-init user data + user_data = local.userdata + + # Public IPv4 and IPv6 (enabled by default) + public_net { + ipv4_enabled = true + ipv6_enabled = true + } +} + +# ============================================================================= +# FIREWALL ATTACHMENT (Reference) +# ============================================================================= + +resource "hcloud_firewall_attachment" "agent" { + count = local.is_hetzner ? 1 : 0 + + firewall_id = hcloud_firewall.agent[0].id + server_ids = [hcloud_server.agent[0].id] +} \ No newline at end of file diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..1b2cca4 --- /dev/null +++ b/main.tf @@ -0,0 +1,55 @@ +# OpenBoatmobile - Agent Deployment +# Provider-agnostic infrastructure for OpenClaw or Hermes agents + +terraform { + required_version = ">= 1.5.4" + + required_providers { + digitalocean = { + source = "digitalocean/digitalocean" + version = "~> 2.0" + } + hcloud = { + source = "hetznercloud/hcloud" + version = "~> 1.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.0" + } + cloudinit = { + source = "hashicorp/cloudinit" + version = "~> 2.0" + } + } +} + +# Provider configuration - selected by var.cloud_provider +# Secrets (API tokens) are set via environment variables: +# TF_VAR_do_token or TF_VAR_hcloud_token + +provider "digitalocean" { + token = var.do_token +} + +provider "hcloud" { + token = var.hcloud_token +} + +# Locals for provider selection and framework-specific defaults +locals { + is_digitalocean = var.cloud_provider == "digitalocean" + is_hetzner = var.cloud_provider == "hetzner" + + # Framework-specific admin user defaults + admin_user_framework_default = var.agent_framework == "hermes" ? "hermes" : "openclaw" + # Use framework default if admin_user not explicitly set + effective_admin_user = var.admin_user != "" ? var.admin_user : local.admin_user_framework_default + + # Common tags/labels for resource tracking + common_tags = { + project = var.project_name + managed = "terraform" + component = var.agent_framework == "hermes" ? "hermes-agent" : "openclaw-gateway" + } +} \ No newline at end of file diff --git a/models/anthropic.json b/models/anthropic.json new file mode 100644 index 0000000..3275a8b --- /dev/null +++ b/models/anthropic.json @@ -0,0 +1,56 @@ +{ + "anthropic": { + "baseUrl": "https://api.anthropic.com/v1", + "api": "anthropic-messages", + "models": [ + { + "id": "claude-3.5-sonnet", + "name": "Claude 3.5 Sonnet", + "reasoning": true, + "input": ["text", "image"], + "contextWindow": 200000, + "maxTokens": 8192 + }, + { + "id": "claude-3.5-sonnet-20241022", + "name": "Claude 3.5 Sonnet (New)", + "reasoning": true, + "input": ["text", "image"], + "contextWindow": 200000, + "maxTokens": 8192 + }, + { + "id": "claude-3.5-haiku", + "name": "Claude 3.5 Haiku", + "reasoning": false, + "input": ["text", "image"], + "contextWindow": 200000, + "maxTokens": 8192 + }, + { + "id": "claude-3-opus", + "name": "Claude 3 Opus", + "reasoning": true, + "input": ["text", "image"], + "contextWindow": 200000, + "maxTokens": 4096 + }, + { + "id": "claude-3-sonnet", + "name": "Claude 3 Sonnet", + "reasoning": false, + "input": ["text", "image"], + "contextWindow": 200000, + "maxTokens": 4096 + }, + { + "id": "claude-3-haiku", + "name": "Claude 3 Haiku", + "reasoning": false, + "input": ["text", "image"], + "contextWindow": 200000, + "maxTokens": 4096 + } + ] + } +} \ No newline at end of file diff --git a/models/combined.json b/models/combined.json new file mode 100644 index 0000000..74419cb --- /dev/null +++ b/models/combined.json @@ -0,0 +1,90 @@ +{ + "venice": { + "baseUrl": "https://api.venice.ai/api/v1", + "api": "openai-completions", + "models": { + "zai-org-glm-5": { + "name": "GLM 5", + "reasoning": true, + "input": ["text"], + "contextWindow": 202752, + "maxTokens": 8192 + }, + "olafangensan-glm-4.7-flash-heretic": { + "name": "GLM 4.7 Flash Heretic", + "reasoning": true, + "input": ["text"], + "contextWindow": 131072, + "maxTokens": 8192 + }, + "kimi-k2-5": { + "name": "Kimi K2.5", + "reasoning": true, + "input": ["text", "image"], + "contextWindow": 256000, + "maxTokens": 65536 + }, + "deepseek-v3.2": { + "name": "DeepSeek V3.2", + "reasoning": true, + "input": ["text"], + "contextWindow": 64000, + "maxTokens": 8192 + } + } + }, + "openai": { + "baseUrl": "https://api.openai.com/v1", + "api": "openai-completions", + "models": { + "gpt-4o": { + "name": "GPT-4o", + "reasoning": false, + "input": ["text", "image"], + "contextWindow": 128000, + "maxTokens": 16384 + }, + "gpt-4o-mini": { + "name": "GPT-4o Mini", + "reasoning": false, + "input": ["text", "image"], + "contextWindow": 128000, + "maxTokens": 16384 + }, + "o1": { + "name": "o1", + "reasoning": true, + "input": ["text", "image"], + "contextWindow": 200000, + "maxTokens": 100000 + }, + "o1-mini": { + "name": "o1 Mini", + "reasoning": true, + "input": ["text", "image"], + "contextWindow": 128000, + "maxTokens": 65536 + } + } + }, + "anthropic": { + "baseUrl": "https://api.anthropic.com/v1", + "api": "anthropic-messages", + "models": { + "claude-3.5-sonnet": { + "name": "Claude 3.5 Sonnet", + "reasoning": true, + "input": ["text", "image"], + "contextWindow": 200000, + "maxTokens": 8192 + }, + "claude-3.5-haiku": { + "name": "Claude 3.5 Haiku", + "reasoning": false, + "input": ["text", "image"], + "contextWindow": 200000, + "maxTokens": 8192 + } + } + } +} \ No newline at end of file diff --git a/models/gemini.json b/models/gemini.json new file mode 100644 index 0000000..7bee966 --- /dev/null +++ b/models/gemini.json @@ -0,0 +1,40 @@ +{ + "gemini": { + "baseUrl": "https://generativelanguage.googleapis.com/v1beta", + "api": "google-ai", + "models": [ + { + "id": "gemini-2.0-flash", + "name": "Gemini 2.0 Flash", + "reasoning": true, + "input": ["text", "image", "video"], + "contextWindow": 1048576, + "maxTokens": 8192 + }, + { + "id": "gemini-1.5-pro", + "name": "Gemini 1.5 Pro", + "reasoning": true, + "input": ["text", "image", "video", "audio"], + "contextWindow": 2097152, + "maxTokens": 65536 + }, + { + "id": "gemini-1.5-flash", + "name": "Gemini 1.5 Flash", + "reasoning": false, + "input": ["text", "image", "video", "audio"], + "contextWindow": 1048576, + "maxTokens": 8192 + }, + { + "id": "gemini-1.5-flash-8b", + "name": "Gemini 1.5 Flash 8B", + "reasoning": false, + "input": ["text", "image"], + "contextWindow": 1048576, + "maxTokens": 8192 + } + ] + } +} \ No newline at end of file diff --git a/models/groq.json b/models/groq.json new file mode 100644 index 0000000..ead7f4c --- /dev/null +++ b/models/groq.json @@ -0,0 +1,56 @@ +{ + "groq": { + "baseUrl": "https://api.groq.com/openai/v1", + "api": "openai-completions", + "models": [ + { + "id": "llama-3.3-70b-versatile", + "name": "Llama 3.3 70B Versatile", + "reasoning": false, + "input": ["text"], + "contextWindow": 128000, + "maxTokens": 8192 + }, + { + "id": "llama-3.1-70b-versatile", + "name": "Llama 3.1 70B Versatile", + "reasoning": false, + "input": ["text"], + "contextWindow": 131072, + "maxTokens": 8192 + }, + { + "id": "llama-3.1-8b-instant", + "name": "Llama 3.1 8B Instant", + "reasoning": false, + "input": ["text"], + "contextWindow": 131072, + "maxTokens": 8192 + }, + { + "id": "mixtral-8x7b-32768", + "name": "Mixtral 8x7B", + "reasoning": false, + "input": ["text"], + "contextWindow": 32768, + "maxTokens": 4096 + }, + { + "id": "gemma2-9b-it", + "name": "Gemma 2 9B", + "reasoning": false, + "input": ["text"], + "contextWindow": 8192, + "maxTokens": 8192 + }, + { + "id": "deepseek-r1-distill-llama-70b", + "name": "DeepSeek R1 Distill Llama 70B", + "reasoning": true, + "input": ["text"], + "contextWindow": 131072, + "maxTokens": 8192 + } + ] + } +} \ No newline at end of file diff --git a/models/openai.json b/models/openai.json new file mode 100644 index 0000000..1397310 --- /dev/null +++ b/models/openai.json @@ -0,0 +1,72 @@ +{ + "openai": { + "baseUrl": "https://api.openai.com/v1", + "api": "openai-completions", + "models": [ + { + "id": "gpt-4o", + "name": "GPT-4o", + "reasoning": false, + "input": ["text", "image", "audio"], + "contextWindow": 128000, + "maxTokens": 16384 + }, + { + "id": "gpt-4o-mini", + "name": "GPT-4o Mini", + "reasoning": false, + "input": ["text", "image"], + "contextWindow": 128000, + "maxTokens": 16384 + }, + { + "id": "gpt-4-turbo", + "name": "GPT-4 Turbo", + "reasoning": false, + "input": ["text", "image"], + "contextWindow": 128000, + "maxTokens": 4096 + }, + { + "id": "gpt-4", + "name": "GPT-4", + "reasoning": false, + "input": ["text"], + "contextWindow": 8192, + "maxTokens": 4096 + }, + { + "id": "o1", + "name": "o1", + "reasoning": true, + "input": ["text", "image"], + "contextWindow": 200000, + "maxTokens": 100000 + }, + { + "id": "o1-mini", + "name": "o1 Mini", + "reasoning": true, + "input": ["text", "image"], + "contextWindow": 128000, + "maxTokens": 65536 + }, + { + "id": "o1-pro", + "name": "o1 Pro", + "reasoning": true, + "input": ["text"], + "contextWindow": 200000, + "maxTokens": 100000 + }, + { + "id": "gpt-3.5-turbo", + "name": "GPT-3.5 Turbo", + "reasoning": false, + "input": ["text"], + "contextWindow": 16385, + "maxTokens": 4096 + } + ] + } +} \ No newline at end of file diff --git a/models/openrouter.json b/models/openrouter.json new file mode 100644 index 0000000..647bfce --- /dev/null +++ b/models/openrouter.json @@ -0,0 +1,72 @@ +{ + "openrouter": { + "baseUrl": "https://openrouter.ai/api/v1", + "api": "openai-completions", + "models": [ + { + "id": "anthropic/claude-3.5-sonnet", + "name": "Claude 3.5 Sonnet (via OpenRouter)", + "reasoning": true, + "input": ["text", "image"], + "contextWindow": 200000, + "maxTokens": 8192 + }, + { + "id": "openai/gpt-4o", + "name": "GPT-4o (via OpenRouter)", + "reasoning": false, + "input": ["text", "image"], + "contextWindow": 128000, + "maxTokens": 16384 + }, + { + "id": "google/gemini-pro-1.5", + "name": "Gemini Pro 1.5 (via OpenRouter)", + "reasoning": true, + "input": ["text", "image", "video"], + "contextWindow": 1000000, + "maxTokens": 8192 + }, + { + "id": "meta-llama/llama-3.1-405b-instruct", + "name": "Llama 3.1 405B (via OpenRouter)", + "reasoning": false, + "input": ["text"], + "contextWindow": 131072, + "maxTokens": 4096 + }, + { + "id": "deepseek/deepseek-r1", + "name": "DeepSeek R1 (via OpenRouter)", + "reasoning": true, + "input": ["text"], + "contextWindow": 64000, + "maxTokens": 8192 + }, + { + "id": "qwen/qwen-2.5-72b-instruct", + "name": "Qwen 2.5 72B (via OpenRouter)", + "reasoning": false, + "input": ["text"], + "contextWindow": 131072, + "maxTokens": 8192 + }, + { + "id": "mistralai/mistral-large", + "name": "Mistral Large (via OpenRouter)", + "reasoning": true, + "input": ["text"], + "contextWindow": 131072, + "maxTokens": 8192 + }, + { + "id": "x-ai/grok-2", + "name": "Grok 2 (via OpenRouter)", + "reasoning": true, + "input": ["text", "image"], + "contextWindow": 131072, + "maxTokens": 8192 + } + ] + } +} \ No newline at end of file diff --git a/models/venice.json b/models/venice.json new file mode 100644 index 0000000..6082c4f --- /dev/null +++ b/models/venice.json @@ -0,0 +1,80 @@ +{ + "venice": { + "baseUrl": "https://api.venice.ai/api/v1", + "api": "openai-completions", + "models": [ + { + "id": "zai-org-glm-5", + "name": "GLM 5", + "reasoning": true, + "input": ["text"], + "contextWindow": 202752, + "maxTokens": 8192 + }, + { + "id": "zai-org-glm-4.7", + "name": "GLM 4.7", + "reasoning": true, + "input": ["text"], + "contextWindow": 202752, + "maxTokens": 8192 + }, + { + "id": "olafangensan-glm-4.7-flash-heretic", + "name": "GLM 4.7 Flash Heretic", + "reasoning": true, + "input": ["text"], + "contextWindow": 131072, + "maxTokens": 8192 + }, + { + "id": "kimi-k2-5", + "name": "Kimi K2.5", + "reasoning": true, + "input": ["text", "image"], + "contextWindow": 256000, + "maxTokens": 65536 + }, + { + "id": "deepseek-v3.2", + "name": "DeepSeek V3.2", + "reasoning": true, + "input": ["text"], + "contextWindow": 64000, + "maxTokens": 8192 + }, + { + "id": "qwen3-coder-480b-a35b-instruct", + "name": "Qwen3 Coder 480B", + "reasoning": true, + "input": ["text"], + "contextWindow": 131072, + "maxTokens": 16384 + }, + { + "id": "llama-3.1-405b", + "name": "Llama 3.1 405B", + "reasoning": false, + "input": ["text"], + "contextWindow": 131072, + "maxTokens": 4096 + }, + { + "id": "llama-3.1-70b", + "name": "Llama 3.1 70B", + "reasoning": false, + "input": ["text"], + "contextWindow": 131072, + "maxTokens": 4096 + }, + { + "id": "mistral-123b", + "name": "Mistral Large 2", + "reasoning": true, + "input": ["text"], + "contextWindow": 131072, + "maxTokens": 8192 + } + ] + } +} \ No newline at end of file diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..f92827a --- /dev/null +++ b/outputs.tf @@ -0,0 +1,87 @@ +# OpenBoatmobile Outputs +# Useful values after deployment + +# ============================================================================= +# CONNECTION INFO +# ============================================================================= + +output "server_ip" { + description = "Public IP address of the server" + value = var.cloud_provider == "digitalocean" ? ( + local.is_digitalocean ? digitalocean_droplet.agent[0].ipv4_address : null + ) : ( + local.is_hetzner ? hcloud_server.agent[0].ipv4_address : null + ) +} + +output "server_name" { + description = "Hostname of the server" + value = var.server_name +} + +output "ssh_command" { + description = "SSH command to connect to the server" + value = var.cloud_provider == "digitalocean" ? ( + local.is_digitalocean ? "ssh ${local.effective_admin_user}@${digitalocean_droplet.agent[0].ipv4_address}" : null + ) : ( + local.is_hetzner ? "ssh ${local.effective_admin_user}@${hcloud_server.agent[0].ipv4_address}" : null + ) +} + +# ============================================================================= +# FRAMEWORK INFO +# ============================================================================= + +output "agent_framework" { + description = "Agent framework deployed" + value = var.agent_framework +} + +output "gateway_token" { + description = "Gateway authentication token (for Hermes)" + value = var.agent_framework == "hermes" && var.gateway_token == "" ? random_password.gateway_token[0].result : (var.gateway_token != "" ? var.gateway_token : "") + sensitive = true +} + +# ============================================================================= +# TAILSCALE (if enabled) +# ============================================================================= + +output "tailscale_url" { + description = "Tailscale URL for gateway access (if Tailscale enabled)" + value = var.enable_tailscale ? "https://${var.server_name}.${var.tailscale_tailnet_domain}.ts.net/" : "" +} + +# ============================================================================= +# PROVIDER INFO +# ============================================================================= + +output "cloud_provider" { + description = "Cloud provider used" + value = var.cloud_provider +} + +output "server_type" { + description = "Server type/size used" + value = var.cloud_provider == "digitalocean" ? var.droplet_size_digitalocean : var.server_type_hetzner +} + +output "location" { + description = "Server location/region" + value = var.cloud_provider == "digitalocean" ? var.region_digitalocean : var.location_hetzner +} + +# ============================================================================= +# NEXT STEPS +# ============================================================================= + +output "next_steps" { + description = "Post-deployment instructions" + value = var.agent_framework == "hermes" ? ( + var.docker_enabled ? + "Hermes Agent deployed (Docker)! SSH: ssh ${local.effective_admin_user}@[server-ip] | Check: docker ps, docker logs hermes -f" : + "Hermes Agent deployed (Direct)! SSH: ssh ${local.effective_admin_user}@[server-ip] | Check: systemctl status hermes.service, hermes --version" + ) : ( + "OpenClaw Gateway deployed! SSH: ssh ${local.effective_admin_user}@[server-ip] | Run: openclaw onboard --install-daemon" + ) +} \ No newline at end of file diff --git a/templates/userdata-hermes.tpl b/templates/userdata-hermes.tpl new file mode 100644 index 0000000..e4b0a80 --- /dev/null +++ b/templates/userdata-hermes.tpl @@ -0,0 +1,492 @@ +#cloud-config +# Hermes Agent Bootstrap (Nous Research) + +# Update packages +package_update: true +package_upgrade: true + +# Install required packages +packages: + - curl + - git + - jq + - gnupg + - ca-certificates + - software-properties-common +%{ if docker_enabled ~} + # Docker-specific packages +%{ else ~} + # Direct installation packages + - python3 + - python3-pip + - python3-venv + - build-essential + - libffi-dev + - libssl-dev +%{ endif ~} + +# Create admin user (if different from root) +users: + - name: ${admin_user} + sudo: ALL=(ALL) NOPASSWD:ALL + shell: /bin/bash + ssh_authorized_keys: ${jsonencode(admin_ssh_keys)} + groups: [sudo, systemd-journal] + +# Write system configuration files +write_files: + # Hermes environment file + - path: /home/${admin_user}/.hermes/.env + content: | + # Hermes Agent Configuration - Generated by Terraform + + # Inference API (Venice AI via OpenAI-compatible endpoint) + # Venice API uses OPENAI_API_KEY + OPENAI_BASE_URL for custom endpoints + OPENAI_API_KEY=${venice_api_key} + OPENAI_BASE_URL=${venice_base_url} + + # Discord Bot + %{if discord_bot_token != ""} + DISCORD_BOT_TOKEN=${discord_bot_token} + %{endif} + %{if discord_home_channel != ""} + DISCORD_HOME_CHANNEL=${discord_home_channel} + %{endif} + %{if discord_allowed_users != ""} + DISCORD_ALLOWED_USERS=${discord_allowed_users} + %{endif} + + # Brave Search + %{if brave_search_api_key != ""} + BRAVE_API_KEY=${brave_search_api_key} + %{endif} + + # Gateway Token + HERMES_GATEWAY_TOKEN=${gateway_token} + + # Authorization + %{if gateway_allowed_users != ""} + GATEWAY_ALLOWED_USERS=${gateway_allowed_users} + %{endif} + %{if gateway_allow_all_users} + GATEWAY_ALLOW_ALL_USERS=true + %{endif} + permissions: '0600' + + # Hermes config.yaml + - path: /home/${admin_user}/.hermes/config.yaml + content: | + # Hermes Agent Configuration + # Framework: Nous Research Hermes Agent + # Venice AI via OpenAI-compatible endpoint + + model: + base_url: ${venice_base_url} + model: ${primary_model} + + auth: + mode: allowlist + + %{if discord_bot_token != ""} + channels: + discord: + enabled: true + auto_thread: ${discord_auto_thread} + %{if discord_server_id != ""} + guilds: + "${discord_server_id}": + require_mention: false + %{if length(discord_user_id) > 0} + users: + %{ for id in discord_user_id ~} + - "${id}" + %{ endfor ~} + %{endif} + %{endif} + %{endif} + + # Configure auxiliary tasks to use Venice AI explicitly + # This avoids "no auxiliary provider" warning + auxiliary: + compression: + base_url: ${venice_base_url} + api_key: ${venice_api_key} + model: ${primary_model} + + approvals: + mode: smart + + gateway: + port: 18789 + bind: "0.0.0.0" + permissions: '0644' + + # SOUL.md - Agent personality + - path: /home/${admin_user}/.hermes/SOUL.md + content: | + # SOUL.md - ${agent_name} + + You are ${agent_name}, an AI agent running on the Hermes Agent framework from Nous Research. + + ## Identity + + **Name:** ${agent_name} + **Framework:** Hermes Agent (Nous Research) + **Model:** ${primary_model_name} + + ## Behavior + + - Be helpful and direct + - Explain your reasoning clearly + - Ask for clarification when needed + - Follow security guardrails + + ## Notes + + - Running on ${server_name} + - Provider: Hetzner Cloud + - Location: ${location} + permissions: '0644' + +%{ if docker_enabled ~} + # Docker Compose for Hermes (Docker mode only) + - path: /home/${admin_user}/docker-compose.yml + content: | + services: + hermes: + image: nousresearch/hermes-agent:latest + container_name: ${agent_name} + restart: unless-stopped + command: gateway run + volumes: + - /home/${admin_user}/.hermes:/opt/data + ports: + - "18789:18789" + env_file: + - /home/${admin_user}/.hermes/.env + deploy: + resources: + limits: + memory: 4G + cpus: "2.0" + permissions: '0644' +%{ endif ~} + + # Systemd service for Hermes + - path: /etc/systemd/system/hermes.service + content: | + [Unit] + Description=Hermes Agent Service +%{ if docker_enabled ~} + After=docker.service + Requires=docker.service +%{ else ~} + After=network.target + Wants=network-online.target +%{ endif ~} + + [Service] + Type=simple + WorkingDirectory=/home/${admin_user} + User=${admin_user} +%{ if docker_enabled ~} + ExecStartPre=/bin/bash -c 'sleep 5 && docker ps > /dev/null' + ExecStart=/bin/sh -c 'cd /home/${admin_user} && exec docker compose -f docker-compose.yml up' + ExecStop=/bin/sh -c 'cd /home/${admin_user} && exec docker compose -f docker-compose.yml down' +%{ else ~} + Environment=PATH=/home/${admin_user}/hermes-venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + ExecStart=/usr/local/bin/hermes gateway run + ExecStop=/bin/kill -TERM $MAINPID +%{ endif ~} + Restart=on-failure + RestartSec=15 + StandardOutput=journal + StandardError=journal + SyslogIdentifier=hermes + + [Install] + WantedBy=multi-user.target + permissions: '0644' + + # Health check and diagnostics script + - path: /usr/local/bin/hermes-health-check.sh + content: | + #!/bin/bash + set -e + + echo "=== Hermes Agent Health Check ===" + echo "" +%{ if docker_enabled ~} + + # Docker-based checks + # Check if Docker is running + if systemctl is-active --quiet docker; then + echo "✓ Docker daemon running" + else + echo "✗ Docker daemon not running" + exit 1 + fi + + # Check if Hermes container exists + if docker ps -a | grep -q "${agent_name}"; then + echo "✓ Hermes container exists" + else + echo "✗ Hermes container not found" + exit 1 + fi + + # Check if Hermes container is running + if docker ps | grep -q "${agent_name}"; then + echo "✓ Hermes container running" + CONTAINER_ID=$(docker ps -q -f name=${agent_name}) + UPTIME=$(docker inspect --format='{{.State.StartedAt}}' $CONTAINER_ID) + echo " Started: $UPTIME" + else + echo "✗ Hermes container not running" + echo " Last status:" + docker ps -a --format "table {{.Names}}\t{{.Status}}" | grep ${agent_name} + exit 1 + fi +%{ else ~} + + # Direct installation checks + # Check if hermes binary exists + if [ -x "/usr/local/bin/hermes" ]; then + echo "✓ Hermes binary installed" + else + echo "✗ Hermes binary not found" + exit 1 + fi + + # Check if hermes venv exists + if [ -d "/home/${admin_user}/hermes-venv" ]; then + echo "✓ Hermes virtual environment exists" + else + echo "✗ Hermes virtual environment not found" + exit 1 + fi + + # Check if hermes process is running + if pgrep -f "hermes gateway run" > /dev/null; then + echo "✓ Hermes process running" + HERMES_PID=$(pgrep -f "hermes gateway run") + echo " PID: $HERMES_PID" + else + echo "✗ Hermes process not running" + exit 1 + fi +%{ endif ~} + + # Check if port is listening + if netstat -tlnp 2>/dev/null | grep -q ":18789 " || lsof -i :18789 > /dev/null 2>&1; then + echo "✓ Gateway listening on port 18789" + else + echo "✗ Gateway not listening on port 18789" + exit 1 + fi + + # Check if config files exist + if [ -f /home/${admin_user}/.hermes/config.yaml ]; then + echo "✓ config.yaml exists" + else + echo "✗ config.yaml missing" + exit 1 + fi + + if [ -f /home/${admin_user}/.hermes/.env ]; then + echo "✓ .env file exists" + else + echo "✗ .env file missing" + exit 1 + fi + + # Check systemd service + if systemctl is-active --quiet hermes.service; then + echo "✓ Hermes systemd service active" + else + echo "✗ Hermes systemd service not active" + systemctl status hermes.service || true + exit 1 + fi + + # Check recent logs + echo "" + echo "Recent logs:" +%{ if docker_enabled ~} + docker logs --tail=10 ${agent_name} 2>&1 | head -20 || echo " (No logs available)" +%{ else ~} + journalctl -u hermes.service -n 10 --no-pager || echo " (No logs available)" +%{ endif ~} + + # Check Discord configuration + if grep -q "DISCORD_BOT_TOKEN" /home/${admin_user}/.hermes/.env; then + if [ -s /home/${admin_user}/.hermes/.env ]; then + BOT_TOKEN=$(grep "DISCORD_BOT_TOKEN" /home/${admin_user}/.hermes/.env | cut -d= -f2 | wc -c) + echo "" + echo "Discord configuration:" + echo " Bot token configured: $([ $BOT_TOKEN -gt 10 ] && echo "✓ Yes" || echo "✗ No")" + grep "DISCORD_SERVER_ID" /home/${admin_user}/.hermes/.env > /dev/null && echo " Server ID configured: ✓" || echo " Server ID configured: ✗" + fi + fi + + echo "" + echo "=== Health Check Complete ===" + echo "" + echo "For more details:" + echo " systemctl status hermes.service" +%{ if docker_enabled ~} + echo " docker logs -f ${agent_name}" +%{ else ~} + echo " journalctl -u hermes.service -f" + echo " hermes --help" +%{ endif ~} + echo "" + permissions: '0755' + +%{ if docker_enabled == false ~} + # Direct installation script - avoids YAML escaping issues in runcmd + - path: /usr/local/bin/install-hermes-direct.sh + content: | + #!/bin/bash + set -e + + ADMIN_USER="${admin_user}" + + echo "=== Installing Hermes Agent (Direct Mode) ===" + + # Ensure home directory exists + mkdir -p /home/$ADMIN_USER + chown -R $ADMIN_USER:$ADMIN_USER /home/$ADMIN_USER + chmod 755 /home/$ADMIN_USER + + # Install dependencies + apt-get update + apt-get install -y git curl python3 python3-pip python3-venv build-essential libffi-dev libssl-dev + + # Install uv (running as root during cloud-init) + # Install uv system-wide so all users can access it + UV_INSTALL_DIR=/usr/local/bin + curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR=$UV_INSTALL_DIR sh + export PATH="$UV_INSTALL_DIR:$PATH" + + # Clone Hermes Agent repository + echo "Cloning Hermes Agent repository..." + su - $ADMIN_USER -c "cd /home/$ADMIN_USER && git clone --recurse-submodules https://github.com/NousResearch/hermes-agent.git" + + # Create virtual environment with Python 3.11 + echo "Creating Python 3.11 virtual environment..." + su - $ADMIN_USER -c "cd /home/$ADMIN_USER/hermes-agent && /usr/local/bin/uv venv venv --python 3.11" + + # Install Hermes with messaging extras + echo "Installing Hermes Agent (this may take a few minutes)..." + su - $ADMIN_USER -c "cd /home/$ADMIN_USER/hermes-agent && export VIRTUAL_ENV=/home/$ADMIN_USER/hermes-agent/venv && /usr/local/bin/uv pip install -e '.[messaging]'" + + # Create hermes wrapper script + echo "Creating wrapper script..." + cat > /usr/local/bin/hermes << WRAPPER_EOF + #!/bin/bash + # Hermes wrapper script - uv is installed during cloud-init + export PATH="/home/$ADMIN_USER/.local/bin:\$PATH" + export VIRTUAL_ENV="/home/$ADMIN_USER/hermes-agent/venv" + exec "/home/$ADMIN_USER/hermes-agent/venv/bin/hermes" "\$@" + WRAPPER_EOF + chmod +x /usr/local/bin/hermes + + # Verify installation + echo "Verifying installation..." + /usr/local/bin/hermes version || { + echo "ERROR: Hermes Agent installation failed" + exit 1 + } + + # Create config directory structure + su - $ADMIN_USER -c "mkdir -p /home/$ADMIN_USER/.hermes/{cron,sessions,logs,memories,skills,pairing,hooks,image_cache,audio_cache}" + chown -R $ADMIN_USER:$ADMIN_USER /home/$ADMIN_USER/.hermes + chmod 755 /home/$ADMIN_USER/.hermes + + echo "=== Installation Complete ===" + permissions: '0755' +%{ endif ~} + +# Run commands +runcmd: + # Create directories + - mkdir -p /home/${admin_user}/.hermes + - chown -R ${admin_user}:${admin_user} /home/${admin_user}/.hermes +%{ if docker_enabled ~} + + # Docker-based installation + - curl -fsSL https://get.docker.com | sh + + # Install Docker Compose plugin (BEFORE pulling images) + - apt-get update + - apt-get install -y docker-compose-plugin + + # Ensure home directory exists with correct ownership + - mkdir -p /home/${admin_user} + - chown -R ${admin_user}:${admin_user} /home/${admin_user} + - chmod 755 /home/${admin_user} + + # Add user to docker group for later use + - usermod -aG docker ${admin_user} + + # Wait for Docker daemon to be ready + - sleep 5 + - docker ps > /dev/null || (sleep 10 && docker ps) + + # Pull Hermes image (runs as root) + - docker pull nousresearch/hermes-agent:latest + + # Ensure .hermes directory has correct permissions for files written by docker + - mkdir -p /home/${admin_user}/.hermes + - chown -R ${admin_user}:${admin_user} /home/${admin_user}/.hermes + - chmod 755 /home/${admin_user}/.hermes + - chown ${admin_user}:${admin_user} /home/${admin_user}/docker-compose.yml + - chmod 644 /home/${admin_user}/docker-compose.yml +%{ else ~} + + # Direct installation - call the install script + - /usr/local/bin/install-hermes-direct.sh +%{ endif ~} + + # Enable and start Hermes service + - systemctl daemon-reload + - systemctl enable hermes.service + + # Start the service with a slight delay to ensure all prerequisites are ready + - sleep 2 + - systemctl start hermes.service + - sleep 3 + + # Verify service started + - systemctl is-active hermes.service || systemctl status hermes.service + + # Print completion message + - | + echo "" + echo "=======================================" + echo " Hermes Agent Bootstrap Complete!" + echo "=======================================" + echo "" + echo "Server: ${server_name}" + echo "Framework: Hermes Agent (Nous Research)" + echo "Model: ${primary_model}" +%{ if docker_enabled ~} + echo "Deployment: Docker Container" +%{ else ~} + echo "Deployment: Direct Installation" +%{ endif ~} + echo "" + echo "Verify deployment:" + echo " systemctl status hermes.service" +%{ if docker_enabled ~} + echo " docker ps" + echo " docker logs ${agent_name}" +%{ else ~} + echo " hermes --version" + echo " journalctl -u hermes.service -f" +%{ endif ~} + echo "" + echo "For Discord connectivity:" + echo " Check bot has 'online' status and is in your server" + echo "" \ No newline at end of file diff --git a/templates/userdata-openclaw.tpl b/templates/userdata-openclaw.tpl new file mode 100644 index 0000000..9bedf59 --- /dev/null +++ b/templates/userdata-openclaw.tpl @@ -0,0 +1,372 @@ +#cloud-config +# OpenClaw Gateway Bootstrap + +# Update packages +package_update: true +package_upgrade: true + +# Install required packages +packages: + - curl + - git + - fail2ban + - ufw + - jq + - gnupg + - ca-certificates + - software-properties-common + +# Create admin user (if different from root) +users: + - name: ${admin_user} + sudo: ALL=(ALL) NOPASSWD:ALL + shell: /bin/bash + ssh_authorized_keys: ${jsonencode(admin_ssh_keys)} + groups: [sudo, systemd-journal] + +# Write system configuration files +write_files: + # SSH hardening configuration + - path: /etc/ssh/sshd_config.d/99-hardening.conf + content: | + # SSH Security Hardening + Port ${ssh_port} + PermitRootLogin no + PasswordAuthentication no + PubkeyAuthentication yes + AuthorizedKeysFile .ssh/authorized_keys + ChallengeResponseAuthentication no + UsePAM yes + X11Forwarding no + PrintMotd no + AcceptEnv LANG LC_* + Subsystem sftp /usr/lib/openssh/sftp-server + permissions: '0644' + + # Fail2ban SSH configuration +%{if enable_fail2ban} + - path: /etc/fail2ban/jail.d/sshd.local + content: | + [sshd] + enabled = true + port = ${ssh_port} + filter = sshd + logpath = /var/log/auth.log + maxretry = 3 + bantime = 3600 + findtime = 600 + permissions: '0644' +%{endif} + + # OpenClaw environment file + - path: /etc/openclaw.env + content: | + # Secrets injected during provisioning - DO NOT commit to version control + + # Inference API keys + %{if venice_api_key != ""} + VENICE_API_KEY=${venice_api_key} + %{endif} + + %{if brave_search_api_key != ""} + BRAVE_SEARCH_API_KEY=${brave_search_api_key} + %{endif} + + # Discord configuration + %{if discord_bot_token != ""} + DISCORD_BOT_TOKEN=${discord_bot_token} + %{endif} + %{if discord_server_id != ""} + DISCORD_SERVER_ID=${discord_server_id} + %{endif} + %{if length(discord_user_id) > 0} + DISCORD_USER_ID=${jsonencode(discord_user_id)} + %{endif} + + # Tailscale auth key (if enabled) + %{if enable_tailscale && tailscale_auth_key != ""} + TAILSCALE_AUTH_KEY=${tailscale_auth_key} + %{endif} + permissions: '0600' + + # OpenClaw configuration file (uses env var references for secrets) + - path: /home/${admin_user}/.openclaw/openclaw.json + content: | + { + "auth": { + "profiles": { + "venice:default": { + "provider": "venice", + "mode": "api_key" + } + } + }, + "models": { + "mode": "merge", + "providers": ${models_config} + }, + "agents": { + "defaults": { + "model": { + "primary": "${primary_model}", + "fallbacks": ${fallback_models} + }, + "workspace": "/home/${admin_user}/.openclaw/workspace" + }, + "list": [ + { + "id": "${agent_name}", + "default": true, + "workspace": "/home/${admin_user}/.openclaw/workspace" + } + ] + }, + "tools": { + "web": { + "search": { + "enabled": true, + "provider": "brave", + "apiKey": "$${BRAVE_SEARCH_API_KEY}" + }, + "fetch": { "enabled": true } + }, + "exec": { + "security": "allowlist", + "ask": "on-miss" + } + }, + "messages": { + "queue": { "mode": "collect" }, + "ackReactionScope": "all" + }, + "channels": { + "discord": { + "enabled": true, + "token": "$${DISCORD_BOT_TOKEN}", + "groupPolicy": "allowlist", + "guilds": { + "${discord_server_id}": { + "requireMention": false, + "users": ${jsonencode(discord_user_id)}, + "channels": { "*": { "allow": true } } + } + } + } + }, + "gateway": { + "port": 18789, + "mode": "local", + "bind": "loopback" + } + } + permissions: '0644' + + # Systemd service for OpenClaw Gateway + - path: /etc/systemd/system/openclaw-gateway.service + content: | + [Unit] + Description=OpenClaw Gateway Service + After=network.target + Wants=network-online.target + + [Service] + Type=simple + ExecStart=/usr/local/bin/openclaw gateway run + Restart=on-failure + RestartSec=10 + StandardOutput=journal + StandardError=journal + SyslogIdentifier=openclaw-gateway + EnvironmentFile=/etc/openclaw.env + WorkingDirectory=/home/${admin_user}/.openclaw + + [Install] + WantedBy=multi-user.target + permissions: '0644' + + # Health check script + - path: /usr/local/bin/openclaw-health-check.sh + content: | + #!/bin/bash + # OpenClaw Gateway Health Check + set -e + + echo "=== OpenClaw Health Check ===" + echo "" + + # Check if OpenClaw is installed + if command -v openclaw &> /dev/null; then + echo "✓ OpenClaw installed: $(openclaw --version)" + else + echo "✗ OpenClaw not found" + exit 1 + fi + + # Check if gateway is running + if systemctl is-active --quiet openclaw-gateway; then + echo "✓ Gateway service running" + else + echo "✗ Gateway service not running" + fi + + # Check if gateway is listening + if lsof -i :18789 > /dev/null 2>&1; then + echo "✓ Gateway listening on port 18789" + else + echo "✗ Gateway not listening on port 18789" + fi + + # Check firewall + if ufw status | grep -q "Status: active"; then + echo "✓ Firewall active" + else + echo "⚠ Firewall not active" + fi + + # Check fail2ban +%{if enable_fail2ban} + if systemctl is-active --quiet fail2ban; then + echo "✓ fail2ban running" + else + echo "⚠ fail2ban not running" + fi +%{endif} + + echo "" + echo "=== End Health Check ===" + permissions: '0755' + +%{if enable_swap} + # Swap creation script + - path: /usr/local/bin/create-swap.sh + content: | + #!/bin/bash + set -e + if [ ! -f /swapfile ]; then + fallocate -l ${swap_size}G /swapfile + chmod 600 /swapfile + mkswap /swapfile + swapon /swapfile + echo '/swapfile none swap sw 0 0' >> /etc/fstab + echo "Created ${swap_size}GB swap" + else + echo "Swap already exists" + fi + permissions: '0755' +%{endif} +%{if enable_tailscale} + # Tailscale setup script + - path: /usr/local/bin/setup-tailscale.sh + content: | + #!/bin/bash + set -e + curl -fsSL https://tailscale.com/install.sh | sh + tailscale up --authkey=${tailscale_auth_key} --ssh + + # Wait for Tailscale to connect + sleep 5 + + # Enable Tailscale Serve for gateway access + # This exposes the gateway on the tailnet via HTTPS + tailscale serve --bg 18789 || echo "Note: Tailscale serve may require 'Serve' to be enabled in admin console" + + echo "Tailscale configured and serving on port 18789" + permissions: '0755' +%{endif} + +# Run commands +runcmd: + # Create admin user home directory + - mkdir -p /home/${admin_user} + - chown ${admin_user}:${admin_user} /home/${admin_user} + +%{if enable_swap} + # Create swapfile + - /usr/local/bin/create-swap.sh +%{endif} + + # Install Node.js ${node_version} + - curl -fsSL https://deb.nodesource.com/setup_${node_version}.x | bash - + - apt-get install -y nodejs + - node --version + - npm --version + + # Install OpenClaw +%{if openclaw_version == "latest"} + - curl -fsSL https://openclaw.ai/install.sh | bash +%{else} +%{if openclaw_version == "lts"} + - curl -fsSL https://openclaw.ai/install.sh | bash -s -- --version lts +%{else} + - curl -fsSL https://openclaw.ai/install.sh | bash -s -- --version ${openclaw_version} +%{endif} +%{endif} + - openclaw --version || echo "OpenClaw installed, needs configuration" + + # Create OpenClaw config directory + - mkdir -p /home/${admin_user}/.openclaw/workspace + - chown -R ${admin_user}:${admin_user} /home/${admin_user}/.openclaw + + # Configure firewall (SSH only) + - ufw default deny incoming + - ufw default allow outgoing + - ufw allow ${ssh_port}/tcp + - ufw --force enable + +%{if enable_fail2ban} + # Enable fail2ban + - systemctl enable fail2ban + - systemctl start fail2ban +%{endif} + +%{if enable_unattended_upgrades} + # Enable automatic security updates + - apt-get install -y unattended-upgrades + - dpkg-reconfigure --priority=low unattended-upgrades +%{endif} + + # Set up SSH hardening + - systemctl restart sshd + +%{if enable_tailscale} + # Install and configure Tailscale + - /usr/local/bin/setup-tailscale.sh +%{endif} + + # Set permissions on environment file + - chown root:root /etc/openclaw.env + - chmod 600 /etc/openclaw.env + + # Enable and start OpenClaw gateway service + # Config is pre-seeded, so gateway can start immediately + - systemctl daemon-reload + - systemctl enable openclaw-gateway.service + - systemctl start openclaw-gateway.service + + # Print completion message + - | + echo "" + echo "=======================================" + echo " OpenClaw Gateway Bootstrap Complete!" + echo "=======================================" + echo "" + echo "Server is ready." + echo "" +%{if enable_tailscale} + echo "Access via Tailscale:" + echo " https://${server_name}.TAILNET.ts.net/" + echo "" + echo "If Tailscale Serve didn't start, run:" + echo " sudo tailscale serve --bg 18789" + echo "" +%{else} + echo "SSH into this server:" + echo " ${ssh_port != 22 ? "ssh -p ${ssh_port}" : "ssh"} ${admin_user}@$(curl -s http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address)" + echo "" +%{endif} + echo "Check gateway status:" + echo " systemctl status openclaw-gateway" + echo "" + echo "View logs:" + echo " journalctl -u openclaw-gateway -f" + echo "" \ No newline at end of file diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..ea031de --- /dev/null +++ b/variables.tf @@ -0,0 +1,319 @@ +# OpenBoatmobile Configuration Variables +# Environment-based secrets: Set TF_VAR_ in your shell or .env file + +# ============================================================================= +# PROVIDER SELECTION +# ============================================================================= + +variable "cloud_provider" { + description = "Cloud provider to use: 'digitalocean' or 'hetzner'" + type = string + default = "hetzner" + + validation { + condition = contains(["digitalocean", "hetzner"], var.cloud_provider) + error_message = "Provider must be 'digitalocean' or 'hetzner'." + } +} + +# ============================================================================= +# AGENT FRAMEWORK SELECTION +# ============================================================================= + +variable "agent_framework" { + description = "Agent framework to deploy: 'openclaw' or 'hermes'" + type = string + default = "hermes" + + validation { + condition = contains(["openclaw", "hermes"], var.agent_framework) + error_message = "Framework must be 'openclaw' or 'hermes'." + } +} + +# ============================================================================= +# PROVIDER TOKENS (Set via environment: TF_VAR_do_token or TF_VAR_hcloud_token) +# ============================================================================= + +variable "do_token" { + description = "DigitalOcean API token (set via TF_VAR_do_token)" + type = string + sensitive = true + default = "" +} + +variable "hcloud_token" { + description = "Hetzner Cloud API token (set via TF_VAR_hcloud)" + type = string + sensitive = true + default = "" +} + +# ============================================================================= +# SERVER CONFIGURATION (Provider-agnostic) +# ============================================================================= + +variable "server_name" { + description = "Hostname for the server" + type = string + default = "agent-gateway" +} + +variable "server_type_hetzner" { + description = "Hetzner server type (e.g., cx23 for 2vCPU/4GB, cpx21 for 3vCPU/4GB)" + type = string + default = "cpx21" # 3 vCPU, 4 GB RAM, 80 GB disk - works in US regions +} + +variable "server_image" { + description = "Hetzner server image (e.g., ubuntu-24.04, ubuntu-22.04)" + type = string + default = "ubuntu-24.04" +} + +variable "create_network" { + description = "Create a private network for multi-server deployments" + type = bool + default = false +} + +variable "network_ip_range" { + description = "IP range for private network" + type = string + default = "10.10.0.0/16" +} + +variable "network_zone" { + description = "Hetzner network zone" + type = string + default = "eu-central" +} + +variable "droplet_size_digitalocean" { + description = "DigitalOcean droplet size (e.g., s-2vcpu-4gb)" + type = string + default = "s-2vcpu-4gb" +} + +variable "region_digitalocean" { + description = "DigitalOcean region (e.g., nyc3, sfo2, ams3)" + type = string + default = "nyc3" +} + +variable "location_hetzner" { + description = "Hetzner location (nbg1, fsn1, hel1, ash)" + type = string + default = "ash" # Ashburn, VA - US East Coast +} + +# ============================================================================= +# SSH CONFIGURATION +# ============================================================================= + +variable "ssh_key_names" { + description = "Names of SSH keys added to the cloud provider (Hetzner: key name in console)" + type = list(string) + default = [] +} + +variable "ssh_key_fingerprints" { + description = "DigitalOcean SSH key fingerprints" + type = list(string) + default = [] +} + +variable "ssh_port" { + description = "SSH port (non-standard can be more secure)" + type = number + default = 22 +} + +variable "ssh_allowed_ips" { + description = "IPs allowed to connect via SSH" + type = list(string) + default = ["0.0.0.0/0", "::/0"] +} + +variable "admin_user" { + description = "Admin username (not root). Defaults to framework name: 'hermes' for hermes deployments, 'openclaw' for openclaw deployments. Set to override." + type = string + default = "" +} + +variable "admin_ssh_keys" { + description = "Additional public SSH keys for admin user" + type = list(string) + default = [] +} + +# ============================================================================= +# AGENT CONFIGURATION +# ============================================================================= + +variable "agent_name" { + description = "Name for the agent" + type = string + default = "hermes" +} + +variable "docker_enabled" { + description = "Whether to deploy Hermes in Docker container (true) or install directly on host (false)" + type = bool + default = true +} + +variable "agent_timezone" { + description = "Timezone for the agent" + type = string + default = "UTC" +} + +# ============================================================================= +# MODEL CONFIGURATION +# ============================================================================= + +variable "primary_model" { + description = "Primary model for inference (without venice/ prefix when using Venice API directly)" + type = string + default = "olafangensan-glm-4.7-flash-heretic" +} + +variable "primary_model_name" { + description = "Human-readable name for the primary model" + type = string + default = "GLM 4.7 Flash Heretic" +} + +variable "fallback_models" { + description = "List of fallback models in priority order (without venice/ prefix)" + type = list(string) + default = ["zai-org-glm-5"] +} + +# ============================================================================= +# API KEYS (Set via environment: TF_VAR_) +# ============================================================================= + +variable "venice_api_key" { + description = "Venice AI API key for inference (used as OPENAI_API_KEY for custom endpoint)" + type = string + sensitive = true + default = "" +} + +variable "venice_base_url" { + description = "Venice AI base URL (default: https://api.venice.ai/api/v1)" + type = string + default = "https://api.venice.ai/api/v1" +} + +variable "brave_search_api_key" { + description = "Brave Search API key" + type = string + sensitive = true + default = "" +} + +# ============================================================================= +# DISCORD CONFIGURATION +# ============================================================================= + +variable "discord_bot_token" { + description = "Discord bot token" + type = string + sensitive = true + default = "" +} + +variable "discord_server_id" { + description = "Discord server/guild ID" + type = string + default = "" +} + +variable "discord_user_id" { + description = "Discord user IDs for allowlist" + type = list(string) + default = [] +} + +variable "discord_home_channel" { + description = "Discord channel ID for home channel (cron delivery, notifications)" + type = string + default = "" +} + +variable "discord_allowed_users" { + description = "Comma-separated Discord user IDs allowed (DISCORD_ALLOWED_USERS)" + type = string + default = "" +} + +variable "discord_auto_thread" { + description = "Auto-create threads on @mention (DISCORD_AUTO_THREAD)" + type = bool + default = true +} + +variable "gateway_allow_all_users" { + description = "Allow all users without allowlist (GATEWAY_ALLOW_ALL_USERS)" + type = bool + default = true +} + +# ============================================================================= +# GATEWAY CONFIGURATION +# ============================================================================= + +variable "gateway_token" { + description = "Gateway authentication token" + type = string + sensitive = true + default = "" +} + +variable "gateway_allowed_users" { + description = "Comma-separated list of allowed user IDs" + type = string + default = "" +} + +# ============================================================================= +# PROJECT METADATA +# ============================================================================= + +variable "project_name" { + description = "Project name for tagging" + type = string + default = "OpenBoatmobile" +} + +variable "environment" { + description = "Environment name (e.g., production, staging, development)" + type = string + default = "production" +} + +# ============================================================================= +# TAILSCALE (OPTIONAL) +# ============================================================================= + +variable "enable_tailscale" { + description = "Install Tailscale for secure remote access" + type = bool + default = false +} + +variable "tailscale_auth_key" { + description = "Tailscale auth key" + type = string + sensitive = true + default = "" +} + +variable "tailscale_tailnet_domain" { + description = "Tailscale tailnet domain (without .ts.net suffix)" + type = string + default = "tailnet" +} \ No newline at end of file