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
This commit is contained in:
commit
a593af9b27
34 changed files with 5646 additions and 0 deletions
95
.env.example
Normal file
95
.env.example
Normal file
|
|
@ -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
|
||||||
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
|
|
@ -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
|
||||||
239
HERMES_FIX_SUMMARY.md
Normal file
239
HERMES_FIX_SUMMARY.md
Normal file
|
|
@ -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@<SERVER_IP>
|
||||||
|
|
||||||
|
# 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.
|
||||||
233
HERMES_VERIFICATION_CHECKLIST.md
Normal file
233
HERMES_VERIFICATION_CHECKLIST.md
Normal file
|
|
@ -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@<SERVER_IP>
|
||||||
|
/usr/local/bin/hermes-health-check.sh
|
||||||
|
```
|
||||||
95
Makefile
Normal file
95
Makefile
Normal file
|
|
@ -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 <target>"
|
||||||
|
@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"
|
||||||
175
README.md
Normal file
175
README.md
Normal file
|
|
@ -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@<SERVER_IP>
|
||||||
|
# For OpenClaw: ssh openclaw@<SERVER_IP>
|
||||||
|
ssh <USERNAME>@<SERVER_IP>
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
---
|
||||||
120
cloudinit.tf
Normal file
120
cloudinit.tf
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
74
digitalocean.tf
Normal file
74
digitalocean.tf
Normal file
|
|
@ -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]
|
||||||
|
}
|
||||||
185
docs/DIGITALOCEAN_SETUP.md
Normal file
185
docs/DIGITALOCEAN_SETUP.md
Normal file
|
|
@ -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 <USERNAME>@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`
|
||||||
197
docs/DISCORD_SETUP.md
Normal file
197
docs/DISCORD_SETUP.md
Normal file
|
|
@ -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://<hostname>.<tailnet>.ts.net/`
|
||||||
|
2. If device pairing is required:
|
||||||
|
- You'll see a pairing code
|
||||||
|
- On the server: `openclaw pairing approve device <CODE>`
|
||||||
|
|
||||||
|
### If Using SSH Tunnel
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create tunnel
|
||||||
|
ssh -L 18789:localhost:18789 openclaw@<server-ip>
|
||||||
|
|
||||||
|
# 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 <CODE>`
|
||||||
|
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
|
||||||
255
docs/DOCKER_VS_DIRECT.md
Normal file
255
docs/DOCKER_VS_DIRECT.md
Normal file
|
|
@ -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.
|
||||||
166
docs/GETTING-STARTED.md
Normal file
166
docs/GETTING-STARTED.md
Normal file
|
|
@ -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 <USERNAME>@<YOUR_SERVER_IP>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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://<hostname>.<tailnet>.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
|
||||||
203
docs/HERMES_AUDIT_REPORT.md
Normal file
203
docs/HERMES_AUDIT_REPORT.md
Normal file
|
|
@ -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@<SERVER_IP>
|
||||||
|
|
||||||
|
# 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
|
||||||
330
docs/HERMES_DEBUGGING.md
Normal file
330
docs/HERMES_DEBUGGING.md
Normal file
|
|
@ -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`
|
||||||
194
docs/HETZNER_SETUP.md
Normal file
194
docs/HETZNER_SETUP.md
Normal file
|
|
@ -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 <USERNAME>@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
|
||||||
|
```
|
||||||
138
docs/SECRETS.md
Normal file
138
docs/SECRETS.md
Normal file
|
|
@ -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.
|
||||||
274
docs/SSH_GUIDE.md
Normal file
274
docs/SSH_GUIDE.md
Normal file
|
|
@ -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 <USERNAME>@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: `<USERNAME>@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 <USERNAME>@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 ./
|
||||||
|
```
|
||||||
167
docs/TAILSCALE_SETUP.md
Normal file
167
docs/TAILSCALE_SETUP.md
Normal file
|
|
@ -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://<hostname>.<tailnet>.ts.net/`
|
||||||
|
|
||||||
|
Where:
|
||||||
|
- `<hostname>` is your server name (default: `openclaw-gateway`)
|
||||||
|
- `<tailnet>` 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 <your-tailnet-ip>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 <hostname>`
|
||||||
|
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 <CODE>`
|
||||||
|
|
||||||
|
## 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://<hostname>.tailnet.ts.net/`
|
||||||
|
|
||||||
|
**Warning:** This exposes your gateway to the internet. Use with caution.
|
||||||
304
docs/TROUBLESHOOTING.md
Normal file
304
docs/TROUBLESHOOTING.md
Normal file
|
|
@ -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 <LOCK_ID>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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@<ip> # 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>@<ip> # username is 'openclaw' or 'hermes' depending on framework
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Verify key is correct:
|
||||||
|
```bash
|
||||||
|
ssh -i ~/.ssh/id_ed25519 openclaw@<ip>
|
||||||
|
```
|
||||||
|
|
||||||
|
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 <CODE>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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)
|
||||||
126
examples/terraform.tfvars.example
Normal file
126
examples/terraform.tfvars.example
Normal file
|
|
@ -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"
|
||||||
122
hermes-openclaw.json
Normal file
122
hermes-openclaw.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
124
hetzner.tf
Normal file
124
hetzner.tf
Normal file
|
|
@ -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]
|
||||||
|
}
|
||||||
55
main.tf
Normal file
55
main.tf
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
56
models/anthropic.json
Normal file
56
models/anthropic.json
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
90
models/combined.json
Normal file
90
models/combined.json
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
models/gemini.json
Normal file
40
models/gemini.json
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
56
models/groq.json
Normal file
56
models/groq.json
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
72
models/openai.json
Normal file
72
models/openai.json
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
72
models/openrouter.json
Normal file
72
models/openrouter.json
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
80
models/venice.json
Normal file
80
models/venice.json
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
87
outputs.tf
Normal file
87
outputs.tf
Normal file
|
|
@ -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"
|
||||||
|
)
|
||||||
|
}
|
||||||
492
templates/userdata-hermes.tpl
Normal file
492
templates/userdata-hermes.tpl
Normal file
|
|
@ -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 ""
|
||||||
372
templates/userdata-openclaw.tpl
Normal file
372
templates/userdata-openclaw.tpl
Normal file
|
|
@ -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 ""
|
||||||
319
variables.tf
Normal file
319
variables.tf
Normal file
|
|
@ -0,0 +1,319 @@
|
||||||
|
# OpenBoatmobile Configuration Variables
|
||||||
|
# Environment-based secrets: Set TF_VAR_<name> 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_<name>)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue