Initial commit — OpenClaw VM infrastructure

- ansible/: VM provisioning playbooks and roles
  - provision-vm.yml: create KVM VM from Ubuntu cloud image
  - install.yml: install OpenClaw on guest (upstream)
  - customize.yml: swappiness, virtiofs fstab, linger
  - roles/vm/: libvirt domain XML, cloud-init templates
  - inventory.yml + host_vars/zap.yml: zap instance config
- backup-openclaw-vm.sh: daily rsync + MinIO upload
- restore-openclaw-vm.sh: full redeploy from scratch
- README.md: full operational documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
William Valentin
2026-03-12 12:18:31 -07:00
commit aceeb7b542
71 changed files with 7840 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
# ── Secrets & credentials ──────────────────────────────────────────────────
.env
*.env
*.key
*.pass
*.token
*.pem
*.p12
# LiteLLM tokens (API keys, access tokens)
litellm-copilot-tokens/
# Kubeconfig (cluster credentials)
swarm-kubeconfig.yaml
ansible/host_vars/*/kubeconfig*
# ── OpenClaw runtime data ──────────────────────────────────────────────────
# Secrets and credentials — never commit
openclaw/secrets.json
openclaw/credentials/
openclaw/identity/
openclaw/devices/
openclaw/telegram/
openclaw/delivery-queue/
openclaw/exec-approvals.json
# Large ephemeral data — not useful in git
openclaw/workspace/
openclaw/workspace-*/
openclaw/memory/
openclaw/agents/
openclaw/logs/
openclaw/extensions-quarantine/
openclaw/sandboxes/
openclaw/sandbox/
openclaw/canvas/
openclaw/media/
openclaw/completions/
openclaw/cron/
openclaw/hooks/
openclaw/subagents/
openclaw/data/
openclaw/extensions/
openclaw/update-check.json
# Keep only the main config file for reference
# (secrets are excluded above; openclaw.json itself has no keys)
# ── OS / editor noise ─────────────────────────────────────────────────────
.DS_Store
*.swp
*.swo
*~
+191
View File
@@ -0,0 +1,191 @@
# swarm
This directory is the source of truth for the OpenClaw VM infrastructure. It is shared into the `zap` VM via virtiofs (mounted at `/mnt/swarm` inside the guest, active after reboot).
## Directory Structure
```
swarm/
├── ansible/ # VM provisioning and configuration
│ ├── inventory.yml # Host definitions
│ ├── host_vars/
│ │ └── zap.yml # All zap-specific variables
│ ├── playbooks/
│ │ ├── provision-vm.yml # Create the VM on the hypervisor
│ │ ├── install.yml # Install OpenClaw on the guest
│ │ └── customize.yml # Post-provision tweaks
│ └── roles/
│ ├── openclaw/ # Upstream role (from openclaw-ansible)
│ └── vm/ # VM provisioning role (local)
├── openclaw/ # Live mirror of guest ~/.openclaw/
├── backup-openclaw-vm.sh # Sync openclaw/ + upload to MinIO
├── restore-openclaw-vm.sh # Full VM redeploy from scratch
└── README.md # This file
```
## VM: zap
| Property | Value |
|----------|-------|
| Libvirt domain | `zap [claw]` |
| Guest hostname | `zap` |
| IP | `192.168.122.182` (static DHCP) |
| MAC | `52:54:00:01:00:71` |
| RAM | 3 GiB |
| vCPUs | 2 |
| Disk | `/var/lib/libvirt/images/claw.qcow2` (60 GiB qcow2) |
| OS | Ubuntu 24.04 |
| Firmware | EFI + Secure Boot + TPM 2.0 |
| Autostart | enabled |
| virtiofs | `~/lab/swarm``/mnt/swarm` (active after reboot) |
| Swappiness | 10 |
SSH access:
```bash
ssh root@192.168.122.182 # privileged operations
ssh openclaw@192.168.122.182 # application-level access
```
## Provisioning a New VM
Use this when deploying zap from scratch on a fresh hypervisor, or creating a new instance.
### Step 1 — Create the VM
```bash
cd ~/lab/swarm/ansible
ansible-playbook -i inventory.yml playbooks/provision-vm.yml --limit zap
```
This will:
- Download the Ubuntu 24.04 cloud image (cached at `/var/lib/libvirt/images/`)
- Create the disk image via copy-on-write (`claw.qcow2`, 60 GiB)
- Build a cloud-init seed ISO with your SSH key and hostname
- Define the VM XML (EFI, memfd shared memory, virtiofs, TPM, watchdog)
- Add a static DHCP reservation for the MAC/IP pair
- Enable autostart and start the VM
- Wait for SSH to become available
### Step 2 — Install OpenClaw
```bash
ansible-playbook -i inventory.yml playbooks/install.yml --limit zap
```
Installs Node.js, pnpm, Docker, UFW, fail2ban, Tailscale, and OpenClaw via the upstream `openclaw-ansible` role.
### Step 3 — Apply customizations
```bash
ansible-playbook -i inventory.yml playbooks/customize.yml --limit zap
```
Applies settings not covered by the upstream role:
- `vm.swappiness=10` (live + persisted)
- virtiofs fstab entry (`swarm``/mnt/swarm`)
- `loginctl enable-linger openclaw` (for user systemd services)
### Step 4 — Restore config
```bash
~/lab/swarm/restore-openclaw-vm.sh zap
```
Rsyncs `openclaw/` back to `~/.openclaw/` on the guest and restarts the gateway service.
### All-in-one redeploy
```bash
# Existing VM (just re-provision guest)
~/lab/swarm/restore-openclaw-vm.sh zap
# Fresh VM at a new IP
~/lab/swarm/restore-openclaw-vm.sh zap <new-ip>
```
When a target IP is passed, `restore-openclaw-vm.sh` runs all four steps above in sequence.
## Backup
The `openclaw/` directory is a live rsync mirror of the guest's `~/.openclaw/`, automatically updated daily at 03:00 by a systemd user timer.
```bash
# Run manually
~/lab/swarm/backup-openclaw-vm.sh zap
# Check timer status
systemctl --user status openclaw-backup.timer
systemctl --user list-timers openclaw-backup.timer
```
### What is backed up
| Included | Excluded |
|----------|----------|
| `openclaw.json` (main config) | `workspace/` (2.6 GiB conversation history) |
| `secrets.json` (API keys) | `logs/` |
| `credentials/`, `identity/` | `extensions-quarantine/` |
| `memory/`, `agents/` | `*.bak*`, `*.backup-*`, `*.pre-*`, `*.failed` |
| `hooks/`, `cron/`, `telegram/` | |
| `workspace-*/` (provider workspaces) | |
### MinIO
Timestamped archives are uploaded to MinIO on every backup run:
| Property | Value |
|----------|-------|
| Endpoint | `http://192.168.153.253:9000` |
| Bucket | `s3://zap/backups/` |
| Retention | 7 most recent archives |
| Credentials | `~/.aws/credentials` (default profile) |
To list available archives:
```bash
aws s3 ls s3://zap/backups/
```
## Adding a New Instance
1. Add an entry to `ansible/inventory.yml`
2. Create `ansible/host_vars/<name>.yml` with VM and OpenClaw variables (copy `host_vars/zap.yml` as a template)
3. Run the four provisioning steps above
4. Add the instance to `~/.claude/state/openclaw-instances.json`
5. Add a backup timer: copy `~/.config/systemd/user/openclaw-backup.{service,timer}`, update the instance name, reload
## Ansible Role Reference
### `vm` role (`roles/vm/`)
Provisions the KVM/libvirt VM on the hypervisor host. Variables (set in `host_vars`):
| Variable | Description | Example |
|----------|-------------|---------|
| `vm_domain` | Libvirt domain name | `"zap [claw]"` |
| `vm_hostname` | Guest hostname | `zap` |
| `vm_memory_mib` | RAM in MiB | `3072` |
| `vm_vcpus` | vCPU count | `2` |
| `vm_disk_path` | qcow2 path on host | `/var/lib/libvirt/images/claw.qcow2` |
| `vm_disk_size` | Disk size | `60G` |
| `vm_mac` | Network MAC address | `52:54:00:01:00:71` |
| `vm_ip` | Static DHCP IP | `192.168.122.182` |
| `vm_virtiofs_source` | Host path to share | `/home/will/lab/swarm` |
| `vm_virtiofs_tag` | Mount tag in guest | `swarm` |
### `openclaw` role (`roles/openclaw/`)
Upstream role from [openclaw-ansible](https://github.com/openclaw/openclaw-ansible). Installs and configures OpenClaw on the guest. Key variables:
| Variable | Value |
|----------|-------|
| `openclaw_install_mode` | `release` |
| `openclaw_ssh_keys` | will's public key |
### `customize.yml` playbook
Post-provision tweaks applied after the upstream role:
- `vm.swappiness = 10`
- `/etc/fstab` entry for virtiofs `swarm` share
- `loginctl enable-linger openclaw`
+23
View File
@@ -0,0 +1,23 @@
---
profile: production
skip_list:
- var-naming[no-role-prefix] # Allow variables without role prefix
- risky-shell-pipe # We handle pipefail where needed
- command-instead-of-module # curl for GPG keys is intentional
warn_list:
- args[module] # Warn on module args issues
kinds:
- playbook: "**/playbook.yml"
- tasks: "**/tasks/*.yml"
- vars: "**/defaults/*.yml"
- handlers: "**/handlers/*.yml"
exclude_paths:
- .github/
- venv/
- dist/
use_default_rules: true
+70
View File
@@ -0,0 +1,70 @@
---
name: Lint
on:
push:
branches: [main, development]
pull_request:
branches: [main]
jobs:
yaml-lint:
name: YAML Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install yamllint
run: pip install yamllint
- name: Run yamllint
run: yamllint .
ansible-lint:
name: Ansible Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install ansible ansible-lint
- name: Install Ansible collections
run: ansible-galaxy collection install -r requirements.yml
- name: Run ansible-lint
run: ansible-lint playbook.yml
syntax-check:
name: Ansible Syntax Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Ansible
run: pip install ansible
- name: Install Ansible collections
run: ansible-galaxy collection install -r requirements.yml
- name: Ansible syntax check
run: ansible-playbook playbook.yml --syntax-check
+14
View File
@@ -0,0 +1,14 @@
*.retry
*.log
.ansible/
.vault_pass
# Secrets and credentials
*.env
.env*
secrets.yml
vault.yml
*.pem
*.key
id_rsa*
# host_vars/ and group_vars/ are intentionally tracked in this repo
+28
View File
@@ -0,0 +1,28 @@
---
extends: default
rules:
line-length:
max: 120
level: warning
indentation:
spaces: 2
indent-sequences: true
truthy:
allowed-values: ['true', 'false']
check-keys: false
comments:
min-spaces-from-content: 1
comments-indentation: false
document-start: disable
braces:
max-spaces-inside: 1
brackets:
max-spaces-inside: 1
octal-values:
forbid-implicit-octal: true
forbid-explicit-octal: true
ignore: |
.github/
venv/
+189
View File
@@ -0,0 +1,189 @@
# Agent Guidelines
## Project Overview
Ansible playbook for automated, hardened OpenClaw installation on Debian/Ubuntu systems.
## Key Principles
1. **Security First**: Firewall must be configured before Docker installation
2. **One Command Install**: `curl | bash` should work out of the box
3. **Localhost Only**: All container ports bind to 127.0.0.1
4. **Defense in Depth**: UFW + DOCKER-USER + localhost binding + non-root container
## Critical Components
### Task Order
Docker must be installed **before** firewall configuration.
Task order in `roles/openclaw/tasks/main.yml`:
```yaml
- tailscale.yml # VPN setup
- user.yml # Create system user
- docker.yml # Install Docker (creates /etc/docker)
- firewall.yml # Configure UFW + daemon.json (needs /etc/docker to exist)
- nodejs.yml # Node.js + pnpm
- openclaw.yml # Container setup
```
Reason: `firewall.yml` writes `/etc/docker/daemon.json` and restarts Docker service.
### DOCKER-USER Chain
Located in `/etc/ufw/after.rules`. Uses dynamic interface detection (not hardcoded `eth0`).
**Never** use `iptables: false` in Docker daemon config - this would break container networking.
### Port Binding
Always use `127.0.0.1:HOST_PORT:CONTAINER_PORT` in docker-compose.yml, never `HOST_PORT:CONTAINER_PORT`.
## Code Style
### Ansible
- Use loops instead of repeated tasks
- No `become_user` (playbook already runs as root)
- Use `community.docker.docker_compose_v2` (not deprecated `docker_compose`)
- Always specify collections in `requirements.yml`
### Docker
- Multi-stage builds if needed
- USER directive for non-root
- Proper healthchecks (test the app, not just Node)
- Use `docker compose` (V2) not `docker-compose` (V1)
- No `version:` in compose files
### Templates
- Use variables for all paths/ports
- Add comments explaining security decisions
- Keep jinja2 logic simple
## Testing Checklist
Before committing changes:
```bash
# 1. Syntax check
ansible-playbook playbook.yml --syntax-check
# 2. Dry run
ansible-playbook playbook.yml --check
# 3. Full install (on test VM)
curl -fsSL https://raw.githubusercontent.com/.../install.sh | bash
# 4. Verify security
sudo ufw status verbose
sudo iptables -L DOCKER-USER -n
sudo ss -tlnp # Only SSH + localhost should listen
# 5. External port scan
nmap -p- TEST_SERVER_IP # Only port 22 should be open
# 6. Test isolation
sudo docker run -p 80:80 nginx
curl http://TEST_SERVER_IP:80 # Should fail
curl http://localhost:80 # Should work
```
## Common Mistakes to Avoid
1. ❌ Installing Docker before configuring firewall
2. ❌ Using `0.0.0.0` port binding
3. ❌ Hardcoding network interface names (use dynamic detection)
4. ❌ Setting `iptables: false` in Docker daemon
5. ❌ Running container as root
6. ❌ Using deprecated `docker-compose` (V1)
7. ❌ Forgetting to add collections to requirements.yml
## Documentation
### User-Facing
- **README.md**: Installation, quick start, basic management
- **docs/**: Technical details, architecture, troubleshooting
### Developer-Facing
- **AGENTS.md**: This file - guidelines for AI agents/contributors
- Code comments: Explain *why*, not *what*
Keep docs concise. No progress logs, no refactoring summaries.
## File Locations
### Host System
```
/opt/openclaw/ # Installation files
/home/openclaw/.openclaw/ # Config and data
/etc/systemd/system/openclaw.service
/etc/docker/daemon.json
/etc/ufw/after.rules
```
### Repository
```
roles/openclaw/
├── tasks/ # Ansible tasks (order matters!)
├── templates/ # Jinja2 configs
├── defaults/ # Variables
└── handlers/ # Service restart handlers
docs/ # Technical documentation (frontmatter format)
requirements.yml # Ansible Galaxy collections
```
## Security Notes
### Why UFW + DOCKER-USER?
Docker bypasses UFW by default. DOCKER-USER chain is evaluated first, allowing us to block before Docker sees the traffic.
### Why Fail2ban?
SSH is exposed to the internet. Fail2ban automatically bans IPs after 5 failed attempts for 1 hour.
### Why Unattended-Upgrades?
Security patches should be applied promptly. Automatic security-only updates reduce vulnerability windows.
### Why Scoped Sudo?
The openclaw user only needs to manage its own service and Tailscale. Full root access would be dangerous if the app is compromised.
### Why Localhost Binding?
Defense in depth. If DOCKER-USER fails, localhost binding prevents external access.
### Why Non-Root Container?
Least privilege. Limits damage if container is compromised.
### Why Systemd?
Clean lifecycle, auto-start, logging integration.
### Known Limitations
- **macOS**: Incomplete support (no launchd, basic firewall). Test thoroughly.
- **IPv6**: Disabled in Docker. Review if your network uses IPv6.
- **curl | bash**: Inherent risks. For production, clone and audit first.
## Making Changes
### Adding a New Task
1. Add to appropriate file in `roles/openclaw/tasks/`
2. Update main.yml if new task file
3. Test with `--check` first
4. Verify idempotency (can run multiple times safely)
### Changing Firewall Rules
1. Test on disposable VM first
2. Always keep SSH accessible
3. Update `docs/security.md` with changes
4. Verify with external port scan
### Updating Docker Config
1. Changes to `daemon.json.j2` trigger Docker restart (via handler)
2. Test container networking after restart
3. Verify DOCKER-USER chain still works
## Version Management
- Use semantic versioning for releases
- Tag releases in git
- Update CHANGELOG.md with user-facing changes
- No version numbers in code (use git tags)
## Support Channels
- OpenClaw issues: https://github.com/openclaw/openclaw
- This installer: https://github.com/openclaw/openclaw-ansible
+296
View File
@@ -0,0 +1,296 @@
# Changelog - Multi-OS Support & Bug Fixes
## [2.0.0] - 2025-01-09
### 🎉 Major Changes
#### Multi-OS Support
- **Added macOS support** alongside Debian/Ubuntu
- **Homebrew installation** for both Linux and macOS
- **OS-specific task files** for clean separation
- **Automatic OS detection** with proper fallback
#### Installation Modes
- **Release Mode** (default): Install via `pnpm install -g openclaw@latest`
- **Development Mode**: Clone repo, build from source, symlink binary
- Switch modes with `-e openclaw_install_mode=development`
- Development aliases: `openclaw-rebuild`, `openclaw-dev`, `openclaw-pull`
#### System Improvements
- **apt update & upgrade** runs automatically at start (Debian/Ubuntu)
- **Homebrew integrated** in PATH for all users
- **pnpm package manager** used for OpenClaw installation
### 🐛 Bug Fixes
#### Critical Fixes from User Feedback
1. **DBus Session Bus Issues**
- Fixed: `loginctl enable-linger` now configured automatically
- Fixed: `XDG_RUNTIME_DIR` set in .bashrc
- Fixed: `DBUS_SESSION_BUS_ADDRESS` configured properly
- **No more manual** `eval $(dbus-launch --sh-syntax)` needed!
2. **User Switching Command**
- Fixed: Changed from `sudo -i -u openclaw` to `sudo su - openclaw`
- Ensures proper login shell with .bashrc loading
- Alternative documented: `sudo -u openclaw -i`
3. **OpenClaw Installation**
- Changed: `pnpm add -g``pnpm install -g openclaw@latest`
- Added installation verification
- Added version display
4. **Configuration Management**
- Removed automatic config.yml creation
- Removed automatic systemd service installation
- Let `openclaw onboard --install-daemon` handle setup
- Only create directory structure
### 📦 New Files Created
#### OS-Specific Task Files
```
roles/openclaw/tasks/
├── system-tools-linux.yml # apt-based tool installation
├── system-tools-macos.yml # brew-based tool installation
├── docker-linux.yml # Docker CE installation
├── docker-macos.yml # Docker Desktop installation
├── firewall-linux.yml # UFW configuration
├── firewall-macos.yml # Application Firewall config
├── openclaw-release.yml # Release mode installation
└── openclaw-development.yml # Development mode installation
```
#### Documentation
- `UPGRADE_NOTES.md` - Detailed upgrade information
- `CHANGELOG.md` - This file
- `docs/development-mode.md` - Development mode guide
### 🔧 Modified Files
#### Core Playbook & Scripts
- **playbook.yml**
- Added OS detection (is_macos, is_debian, is_linux, is_redhat)
- Added apt update/upgrade at start
- Added Homebrew installation
- Enhanced welcome message with `openclaw onboard --install-daemon`
- Removed automatic config.yml creation
- **install.sh**
- Added macOS detection
- Removed Debian-only restriction
- Better error messages for unsupported OS
- **run-playbook.sh**
- Fixed user switch command documentation
- Added alternative command options
- Enhanced post-install instructions
- **README.md**
- Updated for multi-OS support
- Added OS-specific requirements
- Updated quick-start with `openclaw onboard --install-daemon`
- Added Homebrew to feature list
#### Role Files
- **roles/openclaw/defaults/main.yml**
- Added OS-specific variables (homebrew_prefix, package_manager)
- **roles/openclaw/tasks/main.yml**
- No changes (orchestrator)
- **roles/openclaw/tasks/system-tools.yml**
- Refactored to delegate to OS-specific files
- Added fail-safe for unsupported OS
- **roles/openclaw/tasks/docker.yml**
- Refactored to delegate to OS-specific files
- **roles/openclaw/tasks/firewall.yml**
- Refactored to delegate to OS-specific files
- **roles/openclaw/tasks/user.yml**
- Added loginctl enable-linger
- Added XDG_RUNTIME_DIR configuration
- Added DBUS_SESSION_BUS_ADDRESS setup
- Fixed systemd user service support
- **roles/openclaw/tasks/openclaw.yml**
- Changed to `pnpm install -g openclaw@latest`
- Added installation verification
- Removed config.yml template generation
- Removed systemd service installation
- Only creates directory structure
- **roles/openclaw/templates/openclaw-host.service.j2**
- Added XDG_RUNTIME_DIR environment
- Added DBUS_SESSION_BUS_ADDRESS
- Added Homebrew to PATH
- Enhanced security settings (ProtectSystem, ProtectHome)
### 🚀 Workflow Changes
#### Old Workflow
```bash
# Installation
curl -fsSL https://.../install.sh | bash
sudo -i -u openclaw # ❌ Wrong command
nano ~/.openclaw/config.yml # Manual config
openclaw login # Manual setup
# Missing DBus setup # ❌ Errors
```
#### New Workflow - Release Mode (Default)
```bash
# Installation
curl -fsSL https://.../install.sh | bash
sudo su - openclaw # ✅ Correct command
openclaw onboard --install-daemon # ✅ One command setup!
# DBus auto-configured # ✅ Works
# Service auto-installed # ✅ Works
```
#### New Workflow - Development Mode
```bash
# Installation with development mode
git clone https://github.com/openclaw/openclaw-ansible.git
cd openclaw-ansible
./run-playbook.sh -e openclaw_install_mode=development
# Switch to openclaw user
sudo su - openclaw
# Make changes
openclaw-dev # cd ~/code/openclaw
vim src/some-file.ts # Edit code
openclaw-rebuild # pnpm build
# Test immediately
openclaw doctor # Uses new build
```
### 🎯 User Experience Improvements
#### Welcome Message
- Shows environment status (XDG_RUNTIME_DIR, DBUS, Homebrew, OpenClaw version)
- Recommends `openclaw onboard --install-daemon` as primary command
- Provides manual setup steps as alternative
- Lists useful commands for troubleshooting
#### Environment Configuration
- Homebrew automatically added to PATH
- pnpm global bin directory configured
- DBus session bus properly initialized
- XDG_RUNTIME_DIR set for systemd user services
#### Directory Structure
Ansible creates only structure, no config files:
```
~/.openclaw/
├── sessions/ # Created (empty)
├── credentials/ # Created (secure: 0700)
├── data/ # Created (empty)
└── logs/ # Created (empty)
# openclaw.json # NOT created - user's openclaw creates it
# config.yml # NOT created - deprecated
```
### 🔒 Security Enhancements
#### Systemd Service Hardening
- `ProtectSystem=strict` - System directories read-only
- `ProtectHome=read-only` - Limited home access
- `ReadWritePaths=~/.openclaw` - Only config writable
- `NoNewPrivileges=true` - No privilege escalation
#### User Isolation
- Dedicated openclaw system user
- lingering enabled for systemd user services
- Proper DBus session isolation
- XDG_RUNTIME_DIR per-user
### 📊 Platform Support Matrix
| Feature | Debian/Ubuntu | macOS | Status |
|---------|--------------|-------|--------|
| Base Installation | ✅ | ✅ | Tested |
| Homebrew | ✅ | ✅ | Working |
| Docker | Docker CE | Docker Desktop | Working |
| Firewall | UFW | Application FW | Working |
| systemd | ✅ | ❌ | Linux only |
| DBus Setup | ✅ | N/A | Linux only |
| pnpm + OpenClaw | ✅ | ✅ | Working |
### ⚠️ Breaking Changes
1. **User Switch Command Changed**
- Old: `sudo -i -u openclaw`
- New: `sudo su - openclaw`
- Impact: Update documentation, scripts
2. **No Auto-Configuration**
- Old: config.yml auto-created
- New: User runs `openclaw onboard`
- Impact: Users must run onboard command
3. **No Auto-Service Install**
- Old: systemd service auto-installed
- New: `openclaw onboard --install-daemon`
- Impact: Service not running after ansible
### 🔄 Migration Guide
#### For Fresh Installations
Just run the new installer - everything works out of the box!
#### For Existing Installations
```bash
# 1. Add environment variables
echo 'export XDG_RUNTIME_DIR=/run/user/$(id -u)' >> ~/.bashrc
# 2. Enable lingering
sudo loginctl enable-linger openclaw
# 3. Add Homebrew (Linux)
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> ~/.bashrc
# 4. Reload
source ~/.bashrc
# 5. Reinstall openclaw
pnpm install -g openclaw@latest
```
### 📚 Documentation Updates
- README.md: Multi-OS support documented
- UPGRADE_NOTES.md: Detailed technical changes
- CHANGES.md: User-facing changelog (this file)
- install.sh: Updated help text
- run-playbook.sh: Better instructions
### 🐛 Known Issues
#### macOS Limitations
- systemd not available (Linux feature)
- Some Linux-specific tools not installed
- Firewall configuration limited
- **Recommendation**: Use for development, not production
#### Future Enhancements
- [ ] launchd support for macOS service management
- [ ] Full pf firewall configuration for macOS
- [ ] macOS-specific user management
- [ ] Cross-platform testing suite
### 🙏 Credits
Based on user feedback and real-world usage patterns from the openclaw community.
Special thanks to early testers who identified the DBus and user switching issues!
---
**For detailed technical information**, see `UPGRADE_NOTES.md`
**For installation instructions**, see `README.md`
+74
View File
@@ -0,0 +1,74 @@
feat: Add multi-OS support and fix critical user experience issues
BREAKING CHANGES:
- User switch command changed from `sudo -i -u clawdbot` to `sudo su - clawdbot`
- Config files no longer auto-generated, use `clawdbot onboard --install-daemon`
- systemd service no longer auto-installed, use `--install-daemon` flag
Features:
- Add macOS support alongside Debian/Ubuntu
- Add automatic Homebrew installation (Linux + macOS)
- Add OS detection framework (is_macos, is_debian, is_linux)
- Add apt update/upgrade at playbook start (Debian/Ubuntu only)
- Add OS-specific task files for clean separation
- Create clawdbot directory structure (sessions, credentials, data, logs)
Bug Fixes:
- Fix DBus session bus configuration (loginctl enable-linger, XDG_RUNTIME_DIR)
- Fix user switching command (sudo su - clawdbot)
- Fix pnpm installation command (pnpm install -g clawdbot@latest)
- Fix environment variable initialization in .bashrc
- Fix systemd service with proper DBus and XDG paths
Refactoring:
- Split system-tools.yml into OS-specific files
- Split docker.yml into OS-specific files
- Split firewall.yml into OS-specific files
- Remove automatic config.yml generation (let clawdbot handle it)
- Remove automatic systemd service installation (let clawdbot handle it)
Documentation:
- Update README.md with multi-OS support
- Add UPGRADE_NOTES.md with detailed technical changes
- Add CHANGES.md with user-facing changelog
- Update welcome message with clawdbot onboard command
- Add OS-specific installation requirements
Security:
- Enhance systemd service with ProtectSystem and ProtectHome
- Proper DBus session isolation per user
- XDG_RUNTIME_DIR properly configured
New Files:
- roles/clawdbot/tasks/system-tools-linux.yml
- roles/clawdbot/tasks/system-tools-macos.yml
- roles/clawdbot/tasks/docker-linux.yml
- roles/clawdbot/tasks/docker-macos.yml
- roles/clawdbot/tasks/firewall-linux.yml
- roles/clawdbot/tasks/firewall-macos.yml
- UPGRADE_NOTES.md
- CHANGES.md
Modified Files:
- playbook.yml (OS detection, apt upgrade, Homebrew, welcome message)
- install.sh (multi-OS detection)
- run-playbook.sh (correct user switch command)
- README.md (multi-OS documentation)
- roles/clawdbot/defaults/main.yml (OS-specific variables)
- roles/clawdbot/tasks/system-tools.yml (orchestrator)
- roles/clawdbot/tasks/docker.yml (orchestrator)
- roles/clawdbot/tasks/firewall.yml (orchestrator)
- roles/clawdbot/tasks/user.yml (DBus fixes)
- roles/clawdbot/tasks/clawdbot.yml (no auto-config)
- roles/clawdbot/templates/clawdbot-host.service.j2 (enhanced)
Tested on:
- Debian 11/12 ✅
- Ubuntu 20.04/22.04 ✅
- macOS (framework ready, needs testing)
Resolves issues reported in user history:
- DBus session errors
- Incorrect user switch command
- Manual environment setup required
- Missing Homebrew integration
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 OpenClaw Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+364
View File
@@ -0,0 +1,364 @@
# OpenClaw Ansible Installer
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Lint](https://github.com/openclaw/openclaw-ansible/actions/workflows/lint.yml/badge.svg)](https://github.com/openclaw/openclaw-ansible/actions/workflows/lint.yml)
[![Ansible](https://img.shields.io/badge/Ansible-2.14+-blue.svg)](https://www.ansible.com/)
[![Multi-OS](https://img.shields.io/badge/OS-Debian%20%7C%20Ubuntu-orange.svg)](https://www.debian.org/)
Automated, hardened installation of [OpenClaw](https://github.com/openclaw/openclaw) with Docker and Tailscale VPN support for Debian/Ubuntu Linux.
## ⚠️ macOS Support: Deprecated & Disabled
**Effective 2026-02-06, support for bare-metal macOS installations has been removed from this playbook.**
### Why?
The underlying project currently requires system-level permissions and configurations that introduce significant security risks when executed on a primary host OS. To protect user data and system integrity, we have disabled bare-metal execution.
### What does this mean?
* The playbook will now explicitly fail if run on a `Darwin` (macOS) system.
* We strongly discourage manual workarounds to bypass this check.
* **Future Support:** We are evaluating a virtualization-first strategy (using Vagrant or Docker) to provide a sandboxed environment for this project in the future.
## Features
- 🔒 **Firewall-first**: UFW firewall + Docker isolation
- 🛡️ **Fail2ban**: SSH brute-force protection out of the box
- 🔄 **Auto-updates**: Automatic security patches via unattended-upgrades
- 🔐 **Tailscale VPN**: Secure remote access without exposing services
- 🐳 **Docker**: Docker CE with security hardening
- 🚀 **One-command install**: Complete setup in minutes
- 🔧 **Auto-configuration**: DBus, systemd, environment setup
- 📦 **pnpm installation**: Uses `pnpm install -g openclaw@latest`
## Quick Start (Standalone / Self-Hosted)
### Release Mode (Recommended)
Install the latest stable version from npm:
```bash
curl -fsSL https://raw.githubusercontent.com/openclaw/openclaw-ansible/main/install.sh | bash
```
### Development Mode
Install from source for development or testing:
```bash
# Clone the installer
git clone https://github.com/openclaw/openclaw-ansible.git
cd openclaw-ansible
# Install in development mode
./run-playbook.sh -e openclaw_install_mode=development
```
## What Gets Installed
- Tailscale (mesh VPN)
- UFW firewall (SSH + Tailscale ports only)
- Docker CE + Compose V2 (for sandboxes)
- Node.js 22.x + pnpm
- OpenClaw on host (not containerized)
- Systemd service (auto-start)
## Post-Install
After installation completes, switch to the openclaw user:
```bash
sudo su - openclaw
```
Then run the quick-start onboarding wizard:
```bash
openclaw onboard --install-daemon
```
This will:
- Guide you through the setup wizard
- Configure your messaging provider (WhatsApp/Telegram/Signal)
- Install and start the daemon service
### Alternative Manual Setup
```bash
# Configure manually
openclaw configure
# Login to provider
openclaw providers login
# Test gateway
openclaw gateway
# Install as daemon
openclaw daemon install
openclaw daemon start
# Check status
openclaw status
openclaw logs
```
## Manual Installation
### Release Mode (Default)
```bash
# Install dependencies
sudo apt update && sudo apt install -y ansible git
# Clone repository
git clone https://github.com/openclaw/openclaw-ansible.git
cd openclaw-ansible
# Install Ansible collections
ansible-galaxy collection install -r requirements.yml
# Run installation
./run-playbook.sh
```
### Development Mode
Build from source for development:
```bash
# Same as above, but with development mode flag
./run-playbook.sh -e openclaw_install_mode=development
# Or directly:
ansible-playbook playbook.yml --ask-become-pass -e openclaw_install_mode=development
```
This will:
- Clone openclaw repo to `~/code/openclaw`
- Run `pnpm install` and `pnpm build`
- Symlink binary to `~/.local/bin/openclaw`
- Add development aliases to `.bashrc`
## Installation as Ansible Collection
`openclaw.installer` is an Ansible collection and can be installed with the `ansible-galaxy` command:
```bash
ansible-galaxy collection install git+https://github.com/openclaw/openclaw-ansible.git
```
Alternatively, add it to the [`requirements.yml` file of your Ansible project](https://docs.ansible.com/ansible/latest/collections_guide/collections_installing.html#install-multiple-collections-with-a-requirements-file) as follows:
```yaml
collections:
- name: https://github.com/openclaw/openclaw-ansible.git
type: git
version: main
```
As a version, you can use a branch, a version tag (e.g., `v2.0.0`), or a specific commit hash.
### Usage
First copy the sample inventory to `inventory.yml`.
```bash
cp inventory-sample.yml inventory.yml
```
Second edit the inventory file to match your cluster setup. For example:
```yaml
openclaw_servers:
children:
server:
hosts:
192.16.35.11:
192.16.35.12:
```
If needed, you can also edit `vars` section to match your environment.
Start provisioning of the server using one of the following commands. The command to be used depends on whether you installed `openclaw.installer` with `ansible-galaxy` or if you run the playbook from within the cloned git repository:
*Installed with ansible-galaxy*
```bash
ansible-playbook openclaw.installer.deploy -i inventory.yml
```
*In your existing playbook*
```yaml
- name: Deploy OpenClaw
hosts: my_servers
become: true
roles:
- openclaw.installer.openclaw
```
*Running the playbook from inside the repository*
```bash
ansible-playbook playbooks/deploy.yml -i inventory.yml
```
Alternatively, to run the playbook from your existing project setup, run the playbook from within your own playbook:
*Installed with ansible-galaxy*
```yaml
- name: Deploy OpenClaw
ansible.builtin.import_playbook: openclaw.installer.deploy
```
*Running the playbook from inside the repository*
```yaml
- name: Deploy OpenClaw
ansible.builtin.import_playbook: playbooks/deploy.yml
```
## Installation Modes
### Release Mode (Default)
- Installs via `pnpm install -g openclaw@latest`
- Gets latest stable version from npm registry
- Automatic updates via `pnpm install -g openclaw@latest`
- **Recommended for production**
### Development Mode
- Clones from `https://github.com/openclaw/openclaw.git`
- Builds from source with `pnpm build`
- Symlinks binary to `~/.local/bin/openclaw`
- Adds helpful aliases:
- `openclaw-rebuild` - Rebuild after code changes
- `openclaw-dev` - Navigate to repo directory
- `openclaw-pull` - Pull, install deps, and rebuild
- **Recommended for development and testing**
Enable with: `-e openclaw_install_mode=development`
## Security
- **Public ports**: SSH (22), Tailscale (41641/udp) only
- **Fail2ban**: SSH brute-force protection (5 attempts → 1 hour ban)
- **Automatic updates**: Security patches via unattended-upgrades
- **Docker isolation**: Containers can't expose ports externally (DOCKER-USER chain)
- **Non-root**: OpenClaw runs as unprivileged user
- **Scoped sudo**: Limited to service management (not full root)
- **Systemd hardening**: NoNewPrivileges, PrivateTmp, ProtectSystem
Verify: `nmap -p- YOUR_SERVER_IP` should show only port 22 open.
### Security Note
For high-security environments, audit before running:
```bash
git clone https://github.com/openclaw/openclaw-ansible.git
cd openclaw-ansible
# Review playbook.yml and roles/
ansible-playbook playbook.yml --check --diff # Dry run
ansible-playbook playbook.yml --ask-become-pass
```
## Documentation
- [Configuration Guide](docs/configuration.md) - All configuration options
- [Development Mode](docs/development-mode.md) - Build from source
- [Security Architecture](docs/security.md) - Security details
- [Technical Details](docs/architecture.md) - Architecture overview
- [Troubleshooting](docs/troubleshooting.md) - Common issues
- [Agent Guidelines](AGENTS.md) - AI agent instructions
## Requirements
- Debian 11+ or Ubuntu 20.04+
- Root/sudo access
- Internet connection
## Configuration Options
All configuration variables can be found in [`roles/openclaw/defaults/main.yml`](roles/openclaw/defaults/main.yml).
You can override them in three ways:
### 1. Via Command Line
```bash
ansible-playbook playbook.yml --ask-become-pass \
-e openclaw_install_mode=development \
-e "openclaw_ssh_keys=['ssh-ed25519 AAAAC3... user@host']"
```
### 2. Via Variables File
```bash
# Create vars.yml
cat > vars.yml << EOF
openclaw_install_mode: development
openclaw_ssh_keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxxxxxxxx user@host"
- "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB... user@host"
openclaw_repo_url: "https://github.com/YOUR_USERNAME/openclaw.git"
openclaw_repo_branch: "feature-branch"
tailscale_authkey: "tskey-auth-xxxxxxxxxxxxx"
EOF
# Use it
ansible-playbook playbook.yml --ask-become-pass -e @vars.yml
```
### 3. Edit Defaults Directly
Edit `roles/openclaw/defaults/main.yml` before running the playbook.
### Available Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `openclaw_user` | `openclaw` | System user name |
| `openclaw_home` | `/home/openclaw` | User home directory |
| `openclaw_install_mode` | `release` | `release` or `development` |
| `openclaw_ssh_keys` | `[]` | List of SSH public keys |
| `openclaw_repo_url` | `https://github.com/openclaw/openclaw.git` | Git repository (dev mode) |
| `openclaw_repo_branch` | `main` | Git branch (dev mode) |
| `tailscale_authkey` | `""` | Tailscale auth key for auto-connect |
| `nodejs_version` | `22.x` | Node.js version to install |
See [`roles/openclaw/defaults/main.yml`](roles/openclaw/defaults/main.yml) for the complete list.
### Common Configuration Examples
#### SSH Keys for Remote Access
```bash
ansible-playbook playbook.yml --ask-become-pass \
-e "openclaw_ssh_keys=['ssh-ed25519 AAAAC3... user@host']"
```
#### Development Mode with Custom Repository
```bash
ansible-playbook playbook.yml --ask-become-pass \
-e openclaw_install_mode=development \
-e openclaw_repo_url=https://github.com/YOUR_USERNAME/openclaw.git \
-e openclaw_repo_branch=feature-branch
```
#### Tailscale Auto-Connect
```bash
ansible-playbook playbook.yml --ask-become-pass \
-e tailscale_authkey=tskey-auth-xxxxxxxxxxxxx
```
## License
MIT - see [LICENSE](LICENSE)
## Support
- OpenClaw: https://github.com/openclaw/openclaw
- This installer: https://github.com/openclaw/openclaw-ansible/issues
+118
View File
@@ -0,0 +1,118 @@
# Release v2.0.0 - Multi-OS Support & Critical Fixes
## 🎉 Major Release
This release adds **multi-OS support** (macOS + Linux), **development mode**, and fixes **all critical issues** reported by users.
### ✨ New Features
#### Multi-OS Support
-**macOS support** alongside Debian/Ubuntu
-**Homebrew** automatically installed on both platforms
- ✅ OS-specific tasks for clean separation
- ✅ Automatic OS detection with proper fallback
#### Installation Modes
-**Release Mode** (default): `pnpm install -g openclaw@latest`
-**Development Mode**: Clone repo, build from source, symlink binary
- ✅ Switch with `-e openclaw_install_mode=development`
- ✅ Development aliases: `openclaw-rebuild`, `openclaw-dev`, `openclaw-pull`
### 🐛 Critical Bug Fixes
All issues from user feedback resolved:
1.**DBus Session Bus Errors**
- Auto-configured `loginctl enable-linger`
- Dynamic `XDG_RUNTIME_DIR=/run/user/$(id -u)`
- Proper `DBUS_SESSION_BUS_ADDRESS` setup
- No more manual `eval $(dbus-launch --sh-syntax)` needed!
2.**User Switch Command**
- Fixed from `sudo -i -u openclaw` to `sudo su - openclaw`
- Ensures proper login shell with environment
3.**Homebrew Integration**
- Installed for both Linux and macOS
- Added to PATH in both `.bashrc` and `.zshrc`
- `brew shellenv` properly configured
4.**PNPM Configuration**
- `PNPM_HOME` properly set in shell configs
- PATH includes pnpm directories
- Correct permissions on `~/.local/share/pnpm`
5.**User-ID Dynamic**
- No longer hardcoded to 1000
- Dynamically determined with `id -u`
### 🔧 Improvements
-**Better onboarding**: Recommends `openclaw onboard --install-daemon`
-**No auto-config**: Config files created by openclaw itself
-**Enhanced security**: systemd service hardening
-**Linting**: yamllint & ansible-lint production profile passed
### 📦 Installation
#### Quick Start (Release Mode)
```bash
curl -fsSL https://raw.githubusercontent.com/openclaw/openclaw-ansible/main/install.sh | bash
```
#### Development Mode
```bash
git clone https://github.com/openclaw/openclaw-ansible.git
cd openclaw-ansible
./run-playbook.sh -e openclaw_install_mode=development
```
### 📚 Documentation
- [README.md](README.md) - Getting started
- [CHANGELOG.md](CHANGELOG.md) - Full changelog
- [UPGRADE_NOTES.md](UPGRADE_NOTES.md) - Technical details
- [docs/development-mode.md](docs/development-mode.md) - Development guide
### ⚠️ Breaking Changes
1. **User switch command changed**: Use `sudo su - openclaw` instead of `sudo -i -u openclaw`
2. **No auto-configuration**: Config files no longer auto-generated, use `openclaw onboard`
3. **No auto-service**: systemd service not auto-installed, use `--install-daemon` flag
### 🔄 Migration
For existing installations:
```bash
# Add environment variables
echo 'export XDG_RUNTIME_DIR=/run/user/$(id -u)' >> ~/.bashrc
echo 'export PNPM_HOME="$HOME/.local/share/pnpm"' >> ~/.bashrc
# Enable lingering
sudo loginctl enable-linger openclaw
# Add Homebrew (Linux)
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> ~/.bashrc
# Reload
source ~/.bashrc
# Reinstall openclaw
pnpm install -g openclaw@latest
```
### 📊 Testing
- ✅ yamllint: **PASSED**
- ✅ ansible-lint: **PASSED** (production profile)
- ✅ Tested on Debian 11/12
- ✅ Tested on Ubuntu 20.04/22.04
- ⚠️ macOS framework ready (needs real hardware testing)
### 🙏 Thanks
Special thanks to early adopters who provided feedback on the DBus and user switching issues!
---
**Full Changelog**: https://github.com/openclaw/openclaw-ansible/blob/main/CHANGELOG.md
+238
View File
@@ -0,0 +1,238 @@
# Upgrade Notes - Option A Implementation
## ✅ Completed Changes
### 1. Installation Modes (Release vs Development)
- **File**: `roles/openclaw/defaults/main.yml`
- Added `openclaw_install_mode` variable (release | development)
- Release mode: Install via `pnpm install -g openclaw@latest` (default)
- Development mode: Clone repo, build, symlink binary
- Development settings: repo URL, branch, code directory
**Files Created**:
- `roles/openclaw/tasks/openclaw-release.yml` - npm installation
- `roles/openclaw/tasks/openclaw-development.yml` - git clone + build
- `docs/development-mode.md` - comprehensive guide
**Development Mode Features**:
- Clones to `~/code/openclaw`
- Runs `pnpm install` and `pnpm build`
- Symlinks `bin/openclaw.js` to `~/.local/bin/openclaw`
- Adds aliases: `openclaw-rebuild`, `openclaw-dev`, `openclaw-pull`
- Sets `OPENCLAW_DEV_DIR` environment variable
**Usage**:
```bash
# Release mode (default)
./run-playbook.sh
# Development mode
./run-playbook.sh -e openclaw_install_mode=development
# With custom repo
ansible-playbook playbook.yml --ask-become-pass \
-e openclaw_install_mode=development \
-e openclaw_repo_url=https://github.com/YOUR_USERNAME/openclaw.git \
-e openclaw_repo_branch=feature-branch
```
### 2. OS Detection & apt update/upgrade
- **File**: `playbook.yml`
- Added OS detection in pre_tasks (macOS, Debian/Ubuntu, RedHat)
- Added `apt update && apt upgrade` at the beginning (Debian/Ubuntu only)
- Detection variables: `is_macos`, `is_linux`, `is_debian`, `is_redhat`
### 2. Homebrew Installation
- **File**: `playbook.yml`
- Homebrew is now installed for both Linux and macOS
- Linux: `/home/linuxbrew/.linuxbrew/bin/brew`
- macOS: `/opt/homebrew/bin/brew`
- Automatically added to PATH
### 3. OS-Specific System Tools
- **Files**:
- `roles/openclaw/tasks/system-tools.yml` (orchestrator)
- `roles/openclaw/tasks/system-tools-linux.yml` (apt-based)
- `roles/openclaw/tasks/system-tools-macos.yml` (brew-based)
- Tools installed via appropriate package manager per OS
- Homebrew shellenv integrated into .zshrc
### 4. OS-Specific Docker Installation
- **Files**:
- `roles/openclaw/tasks/docker.yml` (orchestrator)
- `roles/openclaw/tasks/docker-linux.yml` (Docker CE)
- `roles/openclaw/tasks/docker-macos.yml` (Docker Desktop)
- Linux: Docker CE via apt
- macOS: Docker Desktop via Homebrew Cask
### 5. OS-Specific Firewall Configuration
- **Files**:
- `roles/openclaw/tasks/firewall.yml` (orchestrator)
- `roles/openclaw/tasks/firewall-linux.yml` (UFW)
- `roles/openclaw/tasks/firewall-macos.yml` (Application Firewall)
- Linux: UFW with Docker isolation
- macOS: Application Firewall configuration
### 6. DBus & systemd User Service Fixes
- **File**: `roles/openclaw/tasks/user.yml`
- Fixed: `loginctl enable-linger` for openclaw user
- Fixed: XDG_RUNTIME_DIR set to `/run/user/$(id -u)`
- Fixed: DBUS_SESSION_BUS_ADDRESS configuration in .bashrc
- No more manual `eval $(dbus-launch --sh-syntax)` needed!
### 7. Systemd Service Template Enhancement
- **File**: `roles/openclaw/templates/openclaw-host.service.j2`
- Added XDG_RUNTIME_DIR environment variable
- Added DBUS_SESSION_BUS_ADDRESS
- Added Homebrew to PATH
- Enhanced security with ProtectSystem and ProtectHome
### 8. OpenClaw Installation via pnpm
- **File**: `roles/openclaw/tasks/openclaw.yml`
- Changed from `pnpm add -g` to `pnpm install -g openclaw@latest`
- Added verification step
- Added version display
### 9. Correct User Switching Command
- **File**: `run-playbook.sh`
- Changed from `sudo -i -u openclaw` to `sudo su - openclaw`
- Alternative: `sudo -u openclaw -i`
- Ensures proper login shell with .bashrc loaded
### 10. Enhanced Welcome Message
- **File**: `playbook.yml` (post_tasks)
- Recommends: `openclaw onboard --install-daemon` as first command
- Shows environment status (XDG_RUNTIME_DIR, DBUS, Homebrew)
- Provides both quick-start and manual setup paths
- More helpful command examples
### 11. Multi-OS Install Script
- **File**: `install.sh`
- Removed Debian/Ubuntu-only check
- Added OS detection for macOS and Linux
- Proper messaging for detected OS
### 12. Updated Documentation
- **File**: `README.md`
- Multi-OS badge (Debian | Ubuntu | macOS)
- Updated features list
- Added OS-specific requirements
- Added post-install instructions with `openclaw onboard --install-daemon`
## 🎯 Key Improvements
### Fixed Issues from User History
1.**DBus errors**: Automatically configured, no manual setup needed
2.**User switching**: Correct command (`sudo su - openclaw`)
3.**Environment**: XDG_RUNTIME_DIR and DBUS properly set
4.**Homebrew**: Integrated and in PATH
5.**pnpm**: Uses `pnpm install -g openclaw@latest`
### OS Detection Framework
- Clean separation between Linux and macOS tasks
- Easy to extend for other distros
- Fails gracefully with clear error messages
### Better User Experience
- Clear next steps after installation
- Recommends `openclaw onboard --install-daemon`
- Helpful welcome message with environment status
- Proper shell initialization
## 🔄 Migration Path
### For Existing Installations
If you have an existing installation, you may need to:
```bash
# 1. Update environment variables
echo 'export XDG_RUNTIME_DIR=/run/user/$(id -u)' >> ~/.bashrc
# 2. Enable lingering
sudo loginctl enable-linger openclaw
# 3. Add Homebrew to PATH (if using Linux)
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> ~/.bashrc
# 4. Reload shell
source ~/.bashrc
# 5. Reinstall openclaw
pnpm install -g openclaw@latest
```
## 📝 TODO - Future macOS Enhancements
### Items NOT Yet Implemented (for future)
- [ ] macOS-specific user creation (different from Linux)
- [ ] launchd service instead of systemd (macOS)
- [ ] Full pf firewall configuration (macOS)
- [ ] macOS-specific Tailscale configuration
- [ ] Testing on actual macOS hardware
### Current macOS Status
- ✅ Basic framework in place
- ✅ Homebrew installation works
- ✅ Docker Desktop installation configured
- ⚠️ Some tasks may need macOS testing/refinement
## 🧪 Testing Recommendations
### Linux (Debian/Ubuntu)
```bash
# Test OS detection
ansible-playbook playbook.yml --ask-become-pass --tags=never -vv
# Test full installation
./run-playbook.sh
# Verify openclaw
sudo su - openclaw
openclaw --version
openclaw onboard --install-daemon
```
### macOS (Future)
```bash
# Similar process, but may need refinements
# Recommend thorough testing before production use
```
## 🔒 Security Notes
### Enhanced systemd Security
- `ProtectSystem=strict`: Read-only system directories
- `ProtectHome=read-only`: Limited home access
- `ReadWritePaths`: Only ~/.openclaw writable
- `NoNewPrivileges`: Prevents privilege escalation
### DBus Session Security
- User-specific DBus session
- Proper XDG_RUNTIME_DIR isolation
- No root access required for daemon
## 📚 Related Files
### Modified Files
- `playbook.yml` - Main orchestration with OS detection
- `install.sh` - Multi-OS detection
- `run-playbook.sh` - Correct user switch command
- `README.md` - Multi-OS documentation
- `roles/openclaw/defaults/main.yml` - OS-specific variables
- `roles/openclaw/tasks/*.yml` - OS-aware task orchestration
- `roles/openclaw/templates/openclaw-host.service.j2` - Enhanced service
### New Files Created
- `roles/openclaw/tasks/system-tools-linux.yml`
- `roles/openclaw/tasks/system-tools-macos.yml`
- `roles/openclaw/tasks/docker-linux.yml`
- `roles/openclaw/tasks/docker-macos.yml`
- `roles/openclaw/tasks/firewall-linux.yml`
- `roles/openclaw/tasks/firewall-macos.yml`
- `UPGRADE_NOTES.md` (this file)
---
**Implementation Date**: January 2025
**Implementation**: Option A (Incremental multi-OS support)
**Status**: ✅ Complete and ready for testing
+3
View File
@@ -0,0 +1,3 @@
[defaults]
# Keep repo-local playbooks runnable after the collection refactor.
roles_path = ./roles
+132
View File
@@ -0,0 +1,132 @@
---
title: Architecture
description: Technical implementation details
---
# Architecture
## Component Overview
```
┌─────────────────────────────────────────┐
│ UFW Firewall (SSH only) │
└──────────────┬──────────────────────────┘
┌──────────────┴──────────────────────────┐
│ DOCKER-USER Chain (iptables) │
│ Blocks all external container access │
└──────────────┬──────────────────────────┘
┌──────────────┴──────────────────────────┐
│ Docker Daemon │
│ - Non-root containers │
│ - Localhost-only binding │
└──────────────┬──────────────────────────┘
┌──────────────┴──────────────────────────┐
│ OpenClaw Container │
│ User: openclaw │
│ Port: 127.0.0.1:3000 │
└──────────────────────────────────────────┘
```
## File Structure
```
/opt/openclaw/
├── Dockerfile
├── docker-compose.yml
/home/openclaw/.openclaw/
├── config.yml
├── sessions/
└── credentials/
/etc/systemd/system/
└── openclaw.service
/etc/docker/
└── daemon.json
/etc/ufw/
└── after.rules (DOCKER-USER chain)
```
## Service Management
OpenClaw runs as a systemd service that manages the Docker container:
```bash
# Systemd controls Docker Compose
systemd → docker compose → openclaw container
```
## Installation Flow
1. **Tailscale Setup** (`tailscale.yml`)
- Add Tailscale repository
- Install Tailscale package
- Display connection instructions
2. **User Creation** (`user.yml`)
- Create `openclaw` system user
3. **Docker Installation** (`docker.yml`)
- Install Docker CE + Compose V2
- Add user to docker group
- Create `/etc/docker` directory
4. **Firewall Setup** (`firewall.yml`)
- Install UFW
- Configure DOCKER-USER chain
- Configure Docker daemon (`/etc/docker/daemon.json`)
- Allow SSH (22/tcp) and Tailscale (41641/udp)
5. **Node.js Installation** (`nodejs.yml`)
- Add NodeSource repository
- Install Node.js 22.x
- Install pnpm globally
6. **OpenClaw Setup** (`openclaw.yml`)
- Create directories
- Generate configs from templates
- Build Docker image
- Start container via Compose
- Install systemd service
## Key Design Decisions
### Why UFW + DOCKER-USER?
Docker manipulates iptables directly, bypassing UFW. The DOCKER-USER chain is evaluated before Docker's FORWARD chain, allowing us to block traffic before Docker sees it.
### Why Localhost Binding?
Defense in depth. Even if DOCKER-USER fails, localhost binding prevents external access.
### Why Systemd Service?
- Auto-start on boot
- Clean lifecycle management
- Integration with system logs
- Dependency management (after Docker)
### Why Non-Root Container?
Principle of least privilege. If container is compromised, attacker has limited privileges.
## Ansible Task Order
```
main.yml
├── tailscale.yml (VPN setup)
├── user.yml (create openclaw user)
├── docker.yml (install Docker, create /etc/docker)
├── firewall.yml (configure UFW + Docker daemon)
├── nodejs.yml (Node.js + pnpm)
└── openclaw.yml (container setup)
```
Order matters: Docker must be installed before firewall configuration because:
1. `/etc/docker` directory must exist for `daemon.json`
2. Docker service must exist to be restarted after config changes
+408
View File
@@ -0,0 +1,408 @@
# Configuration Guide
This guide explains all available configuration options for the OpenClaw Ansible installer.
## Configuration File
All default variables are defined in:
**[`roles/openclaw/defaults/main.yml`](../roles/openclaw/defaults/main.yml)**
## How to Configure
### Method 1: Command Line Variables
Pass variables directly via `-e` flag:
```bash
ansible-playbook playbook.yml --ask-become-pass \
-e openclaw_install_mode=development \
-e "openclaw_ssh_keys=['ssh-ed25519 AAAAC3... user@host']"
```
### Method 2: Variables File
Create a `vars.yml` file:
```yaml
# vars.yml
openclaw_install_mode: development
openclaw_ssh_keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxxxxxxxx user@host"
- "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB... admin@laptop"
openclaw_repo_url: "https://github.com/YOUR_USERNAME/openclaw.git"
openclaw_repo_branch: "main"
tailscale_authkey: "tskey-auth-xxxxxxxxxxxxx"
nodejs_version: "22.x"
```
Then use it:
```bash
ansible-playbook playbook.yml --ask-become-pass -e @vars.yml
```
### Method 3: Edit Defaults
Directly edit `roles/openclaw/defaults/main.yml` before running the playbook.
**Note**: This is not recommended for version control, use variables files instead.
## Available Variables
### User Configuration
#### `openclaw_user`
- **Type**: String
- **Default**: `openclaw`
- **Description**: System user name for running OpenClaw
- **Example**:
```bash
-e openclaw_user=myuser
```
#### `openclaw_home`
- **Type**: String
- **Default**: `/home/openclaw`
- **Description**: Home directory for the openclaw user
- **Example**:
```bash
-e openclaw_home=/home/myuser
```
#### `openclaw_ssh_keys`
- **Type**: List of strings
- **Default**: `[]` (empty)
- **Description**: SSH public keys for accessing the openclaw user account
- **Example**:
```yaml
openclaw_ssh_keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxxxxxxxx user@host"
- "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB... admin@laptop"
```
```bash
-e "openclaw_ssh_keys=['ssh-ed25519 AAAAC3... user@host']"
```
### Installation Mode
#### `openclaw_install_mode`
- **Type**: String (`release` or `development`)
- **Default**: `release`
- **Description**: Installation mode
- `release`: Install via npm (`pnpm install -g openclaw@latest`)
- `development`: Clone repo, build from source, symlink binary
- **Example**:
```bash
-e openclaw_install_mode=development
```
### Development Mode Settings
These variables only apply when `openclaw_install_mode: development`
#### `openclaw_repo_url`
- **Type**: String (Git URL)
- **Default**: `https://github.com/openclaw/openclaw.git`
- **Description**: Git repository URL to clone
- **Example**:
```bash
-e openclaw_repo_url=https://github.com/YOUR_USERNAME/openclaw.git
```
#### `openclaw_repo_branch`
- **Type**: String
- **Default**: `main`
- **Description**: Git branch to checkout
- **Example**:
```bash
-e openclaw_repo_branch=feature-branch
```
#### `openclaw_code_dir`
- **Type**: String (Path)
- **Default**: `{{ openclaw_home }}/code`
- **Description**: Directory where code repositories are stored
- **Example**:
```bash
-e openclaw_code_dir=/home/openclaw/projects
```
#### `openclaw_repo_dir`
- **Type**: String (Path)
- **Default**: `{{ openclaw_code_dir }}/openclaw`
- **Description**: Full path to openclaw repository
- **Example**:
```bash
-e openclaw_repo_dir=/home/openclaw/projects/openclaw
```
### OpenClaw Settings
#### `openclaw_port`
- **Type**: Integer
- **Default**: `3000`
- **Description**: Port for OpenClaw gateway (currently informational)
- **Example**:
```bash
-e openclaw_port=8080
```
#### `openclaw_config_dir`
- **Type**: String (Path)
- **Default**: `{{ openclaw_home }}/.openclaw`
- **Description**: OpenClaw configuration directory
- **Example**:
```bash
-e openclaw_config_dir=/etc/openclaw
```
### Node.js Configuration
#### `nodejs_version`
- **Type**: String
- **Default**: `22.x`
- **Description**: Node.js major version to install
- **Example**:
```bash
-e nodejs_version=20.x
```
### Tailscale Configuration
#### `tailscale_authkey`
- **Type**: String
- **Default**: `""` (empty - manual setup required)
- **Description**: Tailscale authentication key for automatic connection
- **Example**:
```bash
-e tailscale_authkey=tskey-auth-k1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6
```
- **Get Key**: https://login.tailscale.com/admin/settings/keys
### OS-Specific Settings
These are automatically set based on the detected OS:
#### `homebrew_prefix`
- **Type**: String (Path)
- **Default**: `/opt/homebrew` (macOS) or `/home/linuxbrew/.linuxbrew` (Linux)
- **Description**: Homebrew installation prefix
- **Read-only**: Set automatically based on OS
#### `package_manager`
- **Type**: String
- **Default**: `brew` (macOS) or `apt` (Linux)
- **Description**: System package manager
- **Read-only**: Set automatically based on OS
## Configuration Examples
### Basic Setup with SSH Keys
```yaml
# vars.yml
openclaw_ssh_keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxxxxxxxx user@desktop"
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHyyyyyyyy user@laptop"
```
```bash
ansible-playbook playbook.yml --ask-become-pass -e @vars.yml
```
### Development Setup
```yaml
# vars-dev.yml
openclaw_install_mode: development
openclaw_repo_url: "https://github.com/myorg/openclaw.git"
openclaw_repo_branch: "develop"
openclaw_ssh_keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxxxxxxxx dev@workstation"
```
```bash
ansible-playbook playbook.yml --ask-become-pass -e @vars-dev.yml
```
### Production Setup with Tailscale
```yaml
# vars-prod.yml
openclaw_install_mode: release
tailscale_authkey: "tskey-auth-k1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6"
openclaw_ssh_keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxxxxxxxx admin@mgmt-server"
nodejs_version: "22.x"
```
```bash
ansible-playbook playbook.yml --ask-become-pass -e @vars-prod.yml
```
### Custom User and Directories
```yaml
# vars-custom.yml
openclaw_user: mybot
openclaw_home: /opt/mybot
openclaw_config_dir: /etc/mybot
openclaw_code_dir: /opt/mybot/repositories
```
```bash
ansible-playbook playbook.yml --ask-become-pass -e @vars-custom.yml
```
### Testing Different Branches
```yaml
# vars-testing.yml
openclaw_install_mode: development
openclaw_repo_branch: "experimental-feature"
openclaw_ssh_keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxxxxxxxx tester@qa"
```
```bash
ansible-playbook playbook.yml --ask-become-pass -e @vars-testing.yml
```
## Environment-Specific Configurations
### Development Environment
```yaml
# environments/dev.yml
openclaw_install_mode: development
openclaw_repo_url: "https://github.com/openclaw/openclaw.git"
openclaw_repo_branch: "main"
openclaw_ssh_keys:
- "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
```
### Staging Environment
```yaml
# environments/staging.yml
openclaw_install_mode: release
tailscale_authkey: "{{ lookup('env', 'TAILSCALE_AUTHKEY_STAGING') }}"
openclaw_ssh_keys:
- "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
```
### Production Environment
```yaml
# environments/prod.yml
openclaw_install_mode: release
tailscale_authkey: "{{ lookup('env', 'TAILSCALE_AUTHKEY_PROD') }}"
openclaw_ssh_keys:
- "ssh-ed25519 AAAAC3... ops@prod-mgmt"
- "ssh-ed25519 AAAAC3... admin@backup-server"
nodejs_version: "22.x"
```
## Security Best Practices
### SSH Keys
1. **Use dedicated keys**: Create separate SSH keys for OpenClaw access
```bash
ssh-keygen -t ed25519 -f ~/.ssh/openclaw_ed25519 -C "openclaw-access"
```
2. **Limit key permissions**: Use SSH key options to restrict access
```
from="192.168.1.0/24" ssh-ed25519 AAAAC3... admin@trusted-network
```
3. **Rotate keys regularly**: Update SSH keys periodically
```bash
ansible-playbook playbook.yml --ask-become-pass \
-e "openclaw_ssh_keys=['$(cat ~/.ssh/new_key.pub)']"
```
### Tailscale Auth Keys
1. **Use ephemeral keys** for temporary access
2. **Set expiration times** for auth keys
3. **Use reusable keys** only for automation
4. **Store in secrets manager**: Don't commit to git
```bash
# Use environment variable
export TAILSCALE_AUTHKEY=$(vault read -field=key secret/tailscale)
ansible-playbook playbook.yml --ask-become-pass \
-e tailscale_authkey="$TAILSCALE_AUTHKEY"
```
### Sensitive Variables
Never commit sensitive data to git:
```yaml
# ❌ BAD - Don't do this
tailscale_authkey: "tskey-auth-actual-key-here"
# ✅ GOOD - Use environment variables or vault
tailscale_authkey: "{{ lookup('env', 'TAILSCALE_AUTHKEY') }}"
# ✅ GOOD - Use Ansible Vault
tailscale_authkey: "{{ vault_tailscale_authkey }}"
```
Create encrypted vault:
```bash
ansible-vault create secrets.yml
# Add: vault_tailscale_authkey: tskey-auth-xxxxx
ansible-playbook playbook.yml --ask-become-pass \
-e @secrets.yml --ask-vault-pass
```
## Validation
After configuration, verify settings:
```bash
# Check what variables will be used
ansible-playbook playbook.yml --ask-become-pass \
-e @vars.yml --check --diff
# View all variables
ansible-playbook playbook.yml --ask-become-pass \
-e @vars.yml -e "ansible_check_mode=true" \
--tags never -vv
```
## Troubleshooting
### SSH Keys Not Working
Check file ownership and permissions:
```bash
sudo ls -la /home/openclaw/.ssh/
sudo cat /home/openclaw/.ssh/authorized_keys
```
### Tailscale Not Connecting
Verify auth key is valid:
```bash
sudo tailscale up --authkey=YOUR_KEY --verbose
```
### Installation Mode Issues
Check which mode is active:
```bash
ansible-playbook playbook.yml --ask-become-pass \
-e @vars.yml --check | grep "install_mode"
```
## See Also
- [Main README](../README.md)
- [Development Mode Guide](development-mode.md)
- [Upgrade Notes](../UPGRADE_NOTES.md)
- [Defaults File](../roles/openclaw/defaults/main.yml)
+431
View File
@@ -0,0 +1,431 @@
# Development Mode Installation
This guide explains how to install OpenClaw in **development mode**, where the application is built from source instead of installed from npm.
## Overview
### Release Mode vs Development Mode
| Feature | Release Mode | Development Mode |
|---------|-------------|------------------|
| Source | npm registry | GitHub repository |
| Installation | `pnpm install -g openclaw@latest` | `git clone` + `pnpm build` |
| Location | `~/.local/share/pnpm/global/...` | `~/code/openclaw/` |
| Binary | Global pnpm package | Symlink to `bin/openclaw.js` |
| Updates | `pnpm install -g openclaw@latest` | `git pull` + `pnpm build` |
| Use Case | Production, stable deployments | Development, testing, debugging |
| Recommended For | End users | Developers, contributors |
## Installation
### Quick Install
```bash
# Clone the ansible installer
git clone https://github.com/openclaw/openclaw-ansible.git
cd openclaw-ansible
# Run in development mode
./run-playbook.sh -e openclaw_install_mode=development
```
### Manual Install
```bash
# Install ansible
sudo apt update && sudo apt install -y ansible git
# Clone repository
git clone https://github.com/openclaw/openclaw-ansible.git
cd openclaw-ansible
# Install collections
ansible-galaxy collection install -r requirements.yml
# Run playbook with development mode
ansible-playbook playbook.yml --ask-become-pass -e openclaw_install_mode=development
```
## What Gets Installed
### Directory Structure
```
/home/openclaw/
├── .openclaw/ # Configuration directory
│ ├── sessions/
│ ├── credentials/
│ ├── data/
│ └── logs/
├── .local/
│ ├── bin/
│ │ └── openclaw # Symlink -> ~/code/openclaw/bin/openclaw.js
│ └── share/pnpm/
└── code/
└── openclaw/ # Git repository
├── bin/
│ └── openclaw.js
├── dist/ # Built files
├── src/ # Source code
├── package.json
└── pnpm-lock.yaml
```
### Installation Steps
The Ansible playbook performs these steps:
1. **Create `~/code` directory**
```bash
mkdir -p ~/code
```
2. **Clone repository**
```bash
cd ~/code
git clone https://github.com/openclaw/openclaw.git
```
3. **Install dependencies**
```bash
cd openclaw
pnpm install
```
4. **Build from source**
```bash
pnpm build
```
5. **Create symlink**
```bash
ln -sf ~/code/openclaw/bin/openclaw.js ~/.local/bin/openclaw
chmod +x ~/code/openclaw/bin/openclaw.js
```
6. **Add development aliases** to `.bashrc`:
```bash
alias openclaw-rebuild='cd ~/code/openclaw && pnpm build'
alias openclaw-dev='cd ~/code/openclaw'
alias openclaw-pull='cd ~/code/openclaw && git pull && pnpm install && pnpm build'
```
## Development Workflow
### Making Changes
```bash
# 1. Navigate to repository
openclaw-dev
# or: cd ~/code/openclaw
# 2. Make your changes
vim src/some-file.ts
# 3. Rebuild
openclaw-rebuild
# or: pnpm build
# 4. Test immediately
openclaw --version
openclaw doctor
```
### Pulling Updates
```bash
# Pull latest changes and rebuild
openclaw-pull
# Or manually:
cd ~/code/openclaw
git pull
pnpm install
pnpm build
```
### Testing Changes
```bash
# After rebuilding, the openclaw command uses the new code immediately
openclaw status
openclaw gateway
# View daemon logs
openclaw logs
```
### Switching Branches
```bash
cd ~/code/openclaw
# Switch to feature branch
git checkout feature-branch
pnpm install
pnpm build
# Switch back to main
git checkout main
pnpm install
pnpm build
```
## Development Aliases
The following aliases are added to `.bashrc`:
| Alias | Command | Purpose |
|-------|---------|---------|
| `openclaw-dev` | `cd ~/code/openclaw` | Navigate to repo |
| `openclaw-rebuild` | `cd ~/code/openclaw && pnpm build` | Rebuild after changes |
| `openclaw-pull` | `cd ~/code/openclaw && git pull && pnpm install && pnpm build` | Update and rebuild |
Plus an environment variable:
```bash
export OPENCLAW_DEV_DIR="$HOME/code/openclaw"
```
## Configuration Variables
You can customize the development installation:
```yaml
# In playbook or command line
openclaw_install_mode: "development"
openclaw_repo_url: "https://github.com/openclaw/openclaw.git"
openclaw_repo_branch: "main"
openclaw_code_dir: "/home/openclaw/code"
openclaw_repo_dir: "/home/openclaw/code/openclaw"
```
### Using a Fork
```bash
ansible-playbook playbook.yml --ask-become-pass \
-e openclaw_install_mode=development \
-e openclaw_repo_url=https://github.com/YOUR_USERNAME/openclaw.git \
-e openclaw_repo_branch=your-feature-branch
```
### Custom Location
```bash
ansible-playbook playbook.yml --ask-become-pass \
-e openclaw_install_mode=development \
-e openclaw_code_dir=/home/openclaw/projects
```
## Switching Between Modes
### From Release to Development
```bash
# Uninstall global package
pnpm uninstall -g openclaw
# Run ansible in development mode
ansible-playbook playbook.yml --ask-become-pass -e openclaw_install_mode=development
```
### From Development to Release
```bash
# Remove symlink
rm ~/.local/bin/openclaw
# Remove repository (optional)
rm -rf ~/code/openclaw
# Install from npm
pnpm install -g openclaw@latest
```
## Troubleshooting
### Build Fails
```bash
cd ~/code/openclaw
# Check Node.js version (needs 22.x)
node --version
# Clean install
rm -rf node_modules
pnpm install
pnpm build
```
### Symlink Not Working
```bash
# Check symlink
ls -la ~/.local/bin/openclaw
# Recreate symlink
rm ~/.local/bin/openclaw
ln -sf ~/code/openclaw/bin/openclaw.js ~/.local/bin/openclaw
chmod +x ~/code/openclaw/bin/openclaw.js
```
### Command Not Found
```bash
# Ensure ~/.local/bin is in PATH
echo $PATH | grep -q ".local/bin" || echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
```
### Git Issues
```bash
cd ~/code/openclaw
# Reset to clean state
git reset --hard origin/main
git clean -fdx
# Rebuild
pnpm install
pnpm build
```
## Performance Considerations
### Build Time
First build takes longer (~1-2 minutes depending on system):
```bash
pnpm install # Downloads dependencies
pnpm build # Compiles TypeScript
```
Subsequent rebuilds are faster (~10-30 seconds):
```bash
pnpm build # Only recompiles changed files
```
### Disk Usage
Development mode uses more disk space:
- **Release mode**: ~150 MB (global pnpm cache)
- **Development mode**: ~400 MB (repo + node_modules + dist)
### Memory Usage
No difference in runtime memory usage between modes.
## CI/CD Integration
### Testing Before Merge
```bash
# Test specific commit
cd ~/code/openclaw
git fetch origin pull/123/head:pr-123
git checkout pr-123
pnpm install
pnpm build
# Test it
openclaw doctor
```
### Automated Testing
```bash
#!/bin/bash
# test-openclaw.sh
cd ~/code/openclaw
git pull
pnpm install
pnpm build
# Run tests
pnpm test
# Integration test
openclaw doctor
```
## Best Practices
### Development Workflow
1. ✅ **Always rebuild after code changes**
```bash
openclaw-rebuild
```
2. ✅ **Test changes before committing**
```bash
pnpm build && openclaw doctor
```
3. ✅ **Keep dependencies updated**
```bash
pnpm update
pnpm build
```
4. ✅ **Use feature branches**
```bash
git checkout -b feature/my-feature
```
### Don't Do
- ❌ Editing code without rebuilding
- ❌ Running `pnpm link` manually (breaks setup)
- ❌ Installing global packages while in dev mode
- ❌ Modifying symlink manually
## Advanced Usage
### Multiple Repositories
You can have multiple clones:
```bash
# Main development
~/code/openclaw/ # main branch
# Experimental features
~/code/openclaw-test/ # testing branch
# Switch binary symlink
ln -sf ~/code/openclaw-test/bin/openclaw.js ~/.local/bin/openclaw
```
### Custom Build Options
```bash
cd ~/code/openclaw
# Development build (faster, includes source maps)
NODE_ENV=development pnpm build
# Production build (optimized)
NODE_ENV=production pnpm build
```
### Debugging
```bash
# Run with debug output
DEBUG=* openclaw gateway
# Or specific namespaces
DEBUG=openclaw:* openclaw gateway
```
## See Also
- [Main README](../README.md)
- [Security Architecture](security.md)
- [Troubleshooting Guide](troubleshooting.md)
- [OpenClaw Repository](https://github.com/openclaw/openclaw)
+268
View File
@@ -0,0 +1,268 @@
---
title: Installation Guide
description: Detailed installation and configuration instructions
---
# Installation Guide
## Quick Install
```bash
curl -fsSL https://raw.githubusercontent.com/openclaw/openclaw-ansible/main/install.sh | bash
```
## Manual Installation
### Prerequisites
```bash
sudo apt update
sudo apt install -y ansible git
```
### Clone and Run
```bash
git clone https://github.com/openclaw/openclaw-ansible.git
cd openclaw-ansible
# Install Ansible collections
ansible-galaxy collection install -r requirements.yml
# Run playbook
ansible-playbook playbook.yml --ask-become-pass
```
## Post-Installation
### 1. Connect to Tailscale
```bash
# Interactive login
sudo tailscale up
# Or with auth key for automation
sudo tailscale up --authkey tskey-auth-xxxxx
# Check status
sudo tailscale status
```
Get auth keys from: https://login.tailscale.com/admin/settings/keys
### 2. Configure OpenClaw
```bash
# Edit config
sudo nano /home/openclaw/.openclaw/config.yml
# Key settings to configure:
# - provider: whatsapp/telegram/signal
# - phone: your number
# - ai.provider: anthropic/openai
# - ai.model: claude-3-5-sonnet-20241022
```
### 3. Login to Provider
```bash
# Login (will prompt for QR code or phone verification)
sudo docker exec -it openclaw openclaw login
# Check connection
sudo docker logs -f openclaw
```
## Service Management
### Systemd Commands
```bash
# Start/stop/restart
sudo systemctl start openclaw
sudo systemctl stop openclaw
sudo systemctl restart openclaw
# View status
sudo systemctl status openclaw
# Enable/disable auto-start
sudo systemctl enable openclaw
sudo systemctl disable openclaw
```
### Docker Commands
```bash
# View logs
sudo docker logs openclaw
sudo docker logs -f openclaw # follow
# Shell access
sudo docker exec -it openclaw bash
# Restart container
sudo docker restart openclaw
# Check status
sudo docker compose -f /opt/openclaw/docker-compose.yml ps
```
### Firewall Management
```bash
# View UFW status
sudo ufw status verbose
# Add custom rule
sudo ufw allow 8080/tcp comment 'Custom service'
sudo ufw reload
# View Docker isolation
sudo iptables -L DOCKER-USER -n -v
```
## Accessing OpenClaw
OpenClaw's web interface runs on port 3000 (localhost only).
### Via Tailscale (Recommended)
```bash
# After connecting Tailscale, browse to:
http://TAILSCALE_IP:3000
```
Wait, port 3000 is bound to localhost, so this won't work directly. Need to update the compose file or use SSH tunnel.
### Via SSH Tunnel
```bash
ssh -L 3000:localhost:3000 user@server
# Then browse to: http://localhost:3000
```
## Verification
### Security Check
```bash
# Check open ports (should show only SSH + Tailscale)
sudo ss -tlnp
# External port scan (only port 22 should be open)
nmap -p- YOUR_SERVER_IP
# Test container isolation
sudo docker run -d -p 80:80 --name test-nginx nginx
curl http://YOUR_SERVER_IP:80 # Should fail
curl http://localhost:80 # Should work
sudo docker rm -f test-nginx
```
### UFW Status
```bash
sudo ufw status verbose
# Expected output:
# Status: active
# To Action From
# -- ------ ----
# 22/tcp ALLOW IN Anywhere
# 41641/udp ALLOW IN Anywhere
```
### Tailscale Status
```bash
sudo tailscale status
# Expected output:
# 100.x.x.x hostname user@ linux -
```
## Uninstall
```bash
# Stop services
sudo systemctl stop openclaw
sudo systemctl disable openclaw
sudo tailscale down
# Remove containers and data
sudo docker compose -f /opt/openclaw/docker-compose.yml down
sudo rm -rf /opt/openclaw
sudo rm -rf /home/openclaw/.openclaw
sudo rm /etc/systemd/system/openclaw.service
sudo systemctl daemon-reload
# Remove packages (optional)
sudo apt remove --purge tailscale docker-ce docker-ce-cli containerd.io docker-compose-plugin nodejs
# Remove user (optional)
sudo userdel -r openclaw
# Reset firewall (optional)
sudo ufw disable
sudo ufw --force reset
```
## Advanced Configuration
### Custom Port
Edit `/opt/openclaw/docker-compose.yml`:
```yaml
ports:
- "127.0.0.1:3001:3000" # Change 3001 to desired port
```
Then restart:
```bash
sudo systemctl restart openclaw
```
### Environment Variables
Add to `/opt/openclaw/docker-compose.yml`:
```yaml
environment:
- NODE_ENV=production
- ANTHROPIC_API_KEY=sk-ant-xxx
- DEBUG=openclaw:*
```
### Volume Mounts
Add additional volumes in docker-compose.yml:
```yaml
volumes:
- /home/openclaw/.openclaw:/home/openclaw/.openclaw
- /path/to/custom:/custom
```
## Automation
### Unattended Install
```bash
# Set Tailscale auth key in playbook vars
ansible-playbook playbook.yml \
--ask-become-pass \
-e "tailscale_authkey=tskey-auth-xxxxx"
```
### CI/CD Integration
```yaml
# Example GitHub Actions
- name: Deploy OpenClaw
run: |
ansible-playbook playbook.yml \
-e "tailscale_authkey=${{ secrets.TAILSCALE_KEY }}" \
--become
```
+196
View File
@@ -0,0 +1,196 @@
---
title: Security Architecture
description: Firewall configuration, Docker isolation, and security hardening details
---
# Security Architecture
## Overview
This playbook implements a multi-layer defense strategy to secure OpenClaw installations.
## Security Layers
### Layer 1: UFW Firewall
```bash
# Default policies
Incoming: DENY
Outgoing: ALLOW
Routed: DENY
# Allowed
SSH (22/tcp): ALLOW
Tailscale (41641/udp): ALLOW
```
### Layer 2: Fail2ban (SSH Protection)
Automatic protection against SSH brute-force attacks:
```bash
# Configuration
Max retries: 5 attempts
Ban time: 1 hour (3600 seconds)
Find time: 10 minutes (600 seconds)
# Check status
sudo fail2ban-client status sshd
# Unban an IP
sudo fail2ban-client set sshd unbanip IP_ADDRESS
```
### Layer 3: DOCKER-USER Chain
Custom iptables chain that prevents Docker from bypassing UFW:
```
*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A DOCKER-USER -i lo -j ACCEPT
-A DOCKER-USER -i <default_interface> -j DROP
COMMIT
```
**Result**: Even `docker run -p 80:80 nginx` won't expose port 80 externally.
### Layer 4: Localhost-Only Binding
All container ports bind to 127.0.0.1:
```yaml
ports:
- "127.0.0.1:3000:3000"
```
### Layer 5: Non-Root Container
Container processes run as unprivileged `openclaw` user.
### Layer 6: Systemd Hardening
The openclaw service runs with security restrictions:
- `NoNewPrivileges=true` - Prevents privilege escalation
- `PrivateTmp=true` - Isolated /tmp directory
- `ProtectSystem=strict` - Read-only system directories
- `ProtectHome=read-only` - Limited home directory access
- `ReadWritePaths` - Only ~/.openclaw is writable
### Layer 7: Scoped Sudo Access
The openclaw user has limited sudo permissions (not full root):
```bash
# Allowed commands only:
- systemctl start/stop/restart/status openclaw
- systemctl daemon-reload
- tailscale commands
- journalctl for openclaw logs
```
### Layer 8: Automatic Security Updates
Unattended-upgrades is configured for automatic security patches:
```bash
# Check status
sudo unattended-upgrade --dry-run
# View logs
sudo cat /var/log/unattended-upgrades/unattended-upgrades.log
```
**Note**: Automatic reboots are disabled. Monitor for pending reboots:
```bash
cat /var/run/reboot-required 2>/dev/null || echo "No reboot required"
```
## Verification
```bash
# Check firewall
sudo ufw status verbose
# Check fail2ban
sudo fail2ban-client status
# Check Tailscale status
sudo tailscale status
# Check Docker isolation
sudo iptables -L DOCKER-USER -n -v
# Port scan from external machine (only SSH + Tailscale should be open)
nmap -p- YOUR_SERVER_IP
# Test container isolation
sudo docker run -d -p 80:80 --name test-nginx nginx
curl http://YOUR_SERVER_IP:80 # Should fail/timeout
curl http://localhost:80 # Should work
sudo docker rm -f test-nginx
# Check unattended-upgrades
sudo systemctl status unattended-upgrades
```
## Tailscale Access
OpenClaw's web interface (port 3000) is bound to localhost. Access it via:
1. **SSH tunnel**:
```bash
ssh -L 3000:localhost:3000 user@server
# Then browse to http://localhost:3000
```
2. **Tailscale** (recommended):
```bash
# On server: already done by playbook
sudo tailscale up
# From your machine:
# Browse to http://TAILSCALE_IP:3000
```
## Network Flow
```
Internet → UFW (SSH only) → fail2ban → DOCKER-USER Chain → DROP
Container → NAT → Internet (outbound allowed)
```
## Known Limitations
### macOS Support
- macOS firewall configuration is basic (Application Firewall only)
- No fail2ban equivalent on macOS
- Consider using Little Snitch or similar for enhanced macOS security
### IPv6
- Docker IPv6 is disabled by default (`ip6tables: false` in daemon.json)
- If your network uses IPv6, review and test firewall rules accordingly
### Installation Script
- The `curl | bash` installation pattern has inherent risks
- For high-security environments, clone the repository and audit before running
- Consider using `--check` mode first: `ansible-playbook playbook.yml --check`
## Security Checklist
After installation, verify:
- [ ] `sudo ufw status` shows only SSH and Tailscale allowed
- [ ] `sudo fail2ban-client status sshd` shows jail active
- [ ] `sudo iptables -L DOCKER-USER -n` shows DROP rule
- [ ] `nmap -p- YOUR_IP` from external shows only port 22
- [ ] `docker run -p 80:80 nginx` + `curl YOUR_IP:80` times out
- [ ] Tailscale access works for web UI
## Reporting Security Issues
If you discover a security vulnerability, please report it privately:
- OpenClaw: https://github.com/openclaw/openclaw/security
- This installer: https://github.com/openclaw/openclaw-ansible/security
+160
View File
@@ -0,0 +1,160 @@
---
title: Troubleshooting
description: Common issues and solutions
---
# Troubleshooting
## Container Can't Reach Internet
**Symptom**: OpenClaw can't connect to WhatsApp/Telegram
**Check**:
```bash
# Test from container
sudo docker exec openclaw ping -c 3 8.8.8.8
# Check UFW allows outbound
sudo ufw status verbose | grep OUT
```
**Solution**:
```bash
# Verify DOCKER-USER allows established connections
sudo iptables -L DOCKER-USER -n -v
# Restart Docker + Firewall
sudo systemctl restart docker
sudo ufw reload
sudo systemctl restart openclaw
```
## Port Already in Use
**Symptom**: Port 3000 conflict
**Solution**:
```bash
# Find what's using port 3000
sudo ss -tlnp | grep 3000
# Change OpenClaw port
sudo nano /opt/openclaw/docker-compose.yml
# Change: "127.0.0.1:3001:3000"
sudo systemctl restart openclaw
```
## Firewall Lockout
**Symptom**: Can't SSH after installation
**Solution** (via console/rescue mode):
```bash
# Disable UFW temporarily
sudo ufw disable
# Check SSH rule exists
sudo ufw status numbered
# Re-add SSH rule
sudo ufw allow 22/tcp
# Re-enable
sudo ufw enable
```
## Container Won't Start
**Check logs**:
```bash
# Systemd logs
sudo journalctl -u openclaw -n 50
# Docker logs
sudo docker logs openclaw
# Compose status
sudo docker compose -f /opt/openclaw/docker-compose.yml ps
```
**Common fixes**:
```bash
# Rebuild image
cd /opt/openclaw
sudo docker compose build --no-cache
sudo systemctl restart openclaw
# Check permissions
sudo chown -R openclaw:openclaw /home/openclaw/.openclaw
```
## Verify Docker Isolation
**Test that external ports are blocked**:
```bash
# Start test container
sudo docker run -d -p 80:80 --name test-nginx nginx
# From EXTERNAL machine (should fail):
curl http://YOUR_SERVER_IP:80
# From SERVER (should work):
curl http://localhost:80
# Cleanup
sudo docker rm -f test-nginx
```
## UFW Status Shows Inactive
**Fix**:
```bash
# Enable UFW
sudo ufw enable
# Reload rules
sudo ufw reload
# Verify
sudo ufw status verbose
```
## Ansible Playbook Fails
### Failed to set permissions on temporary files (Become Issue)
**Symptom**: `fatal: [host]: FAILED! => {"msg": "Failed to set permissions on the temporary files Ansible needs to create when becoming an unprivileged user..."}`
**Cause**: This happens when connecting as an unprivileged user (e.g., `ansible`) and using `become_user` to switch to another unprivileged user (e.g., `openclaw`). Ansible struggles to share temporary module files between them if the filesystem doesn't support POSIX ACLs.
**Solution**:
Enable **Ansible Pipelining** in your `ansible.cfg`. This executes modules via stdin without creating temporary files.
```ini
[defaults]
pipelining = True
```
Alternatively, if you cannot use pipelining, you can allow world-readable temporary files (less secure):
```ini
[defaults]
allow_world_readable_tmpfiles = True
```
### Collection missing
...
```bash
ansible-galaxy collection install -r requirements.yml
```
**Permission denied**:
```bash
# Run with --ask-become-pass
ansible-playbook playbook.yml --ask-become-pass
```
**Docker daemon not running**:
```bash
sudo systemctl start docker
# Re-run playbook
```
+52
View File
@@ -0,0 +1,52 @@
---
namespace: openclaw
name: installer
version: 2.0.0
readme: README.md
authors:
- OpenClaw Contributors <https://github.com/openclaw>
description: Automated, hardened installation of OpenClaw with Docker and Tailscale VPN support for Debian/Ubuntu Linux.
license:
- MIT
license_file: LICENSE
tags:
- openclaw
- docker
- tailscale
- vpn
- firewall
- security
- automation
- debian
- ubuntu
- messaging
dependencies:
community.docker: ">=3.4.0"
community.general: ">=8.0.0"
ansible.posix: ">=1.5.0"
repository: https://github.com/openclaw/openclaw-ansible
documentation: https://github.com/openclaw/openclaw-ansible/blob/main/README.md
homepage: https://github.com/openclaw/openclaw
issues: https://github.com/openclaw/openclaw-ansible/issues
build_ignore:
- .git
- .gitignore
- .github
- tests
- '*.tar.gz'
- install.sh
- run-playbook.sh
- CHANGELOG.md
- AGENTS.md
- GIT_COMMIT_MESSAGE.txt
- RELEASE_NOTES_v2.0.0.md
- UPGRADE_NOTES.md
- COLLECTION_MIGRATION_PLAN.md
+21
View File
@@ -0,0 +1,21 @@
---
# Host-specific vars for zap [claw]
# ── VM provisioning ────────────────────────────────────────────────────────
vm_domain: "zap [claw]"
vm_hostname: zap
vm_memory_mib: 3072
vm_vcpus: 2
vm_disk_path: /var/lib/libvirt/images/claw.qcow2
vm_disk_size: "60G"
vm_mac: "52:54:00:01:00:71"
vm_ip: "192.168.122.182"
vm_network: default
vm_virtiofs_source: /home/will/lab/swarm
vm_virtiofs_tag: swarm
# ── OpenClaw guest provisioning ────────────────────────────────────────────
openclaw_install_mode: release
openclaw_ssh_keys:
- "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC6l6Z3CBr0gU6tVMddCW1vjYk5CK8TExp/AViiUEJGADci/Dk26XnfmG0XjexIjD7L4a/V5hIh+0HEIwM146vcfRnB1lXty5BV6Rhum7J3qp7xXBPghqCC9tujc5KiMQZyCsLICFyhHOdqRoquUqbFeYL7cT+Vk+J+HSGXmXZvJGGSpW7b94wkGADkSTEn2u8FRpynU3vZ6KIIiBG+oreWl7LcBhlztZELlwiRx66HgW8t/DhJlL6mhfKJ6C0Sg7s98SwvsT+jJxsaip69SlXvAJhrun2oDvS+X+a/2u9LD6w8GazmkX6m626SqGEGdw21l+oJQf+2LphQ3h8gIScNg5LmhaxXFqo718nmKEi9aE1MNGU4HWsNLJGxXvPTZqTreyS81yKMiqSZKZ2WzwaCQO2VeRmHyuDgrlGUGcU9DFi9pEkkjiChp1PE7XNbIwTurUCC19WUHcijY1K/ZH9Ku8GXgWf0109QZpJKc/04dRlYNBgUBL7dCTxbC/UjIdDMmgdRmPZ4oDUqUyBMsIEu8Wsx2snaUh4E2i5m0Vrd4Yy0+Eiu5YZBZt2IsljFE+c0KGSZMOyoCJksmqlTfvC0Ejt/bVsNhbZDgVB2K3sxRYa9Sa6I9nlCm7bSZC94vILVKkDsivmi+sj9dTV8mlJhA/yaGsBOokbjYYAa2cgQyw== will@squareffect.com"
+97
View File
@@ -0,0 +1,97 @@
#!/bin/bash
set -e
# OpenClaw Ansible Installer
# This script installs Ansible if needed and runs the OpenClaw playbook via Ansible Galaxy
# Enable 256 colors
export TERM=xterm-256color
# Force color support
if [ -z "$COLORTERM" ]; then
export COLORTERM=truecolor
fi
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${GREEN}╔════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ OpenClaw Ansible Installer ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════╝${NC}"
echo ""
# Detect operating system
if command -v apt-get &> /dev/null; then
echo -e "${GREEN}✓ Detected: Debian/Ubuntu Linux${NC}"
else
echo -e "${RED}✗ Error: Unsupported operating system${NC}"
echo -e "${RED} This installer supports: Debian/Ubuntu Linux only${NC}"
exit 1
fi
# Check if running as root or with sudo access
if [ "$EUID" -eq 0 ]; then
echo -e "${GREEN}Running as root.${NC}"
SUDO=""
ANSIBLE_EXTRA_VARS="-e ansible_become=false"
else
if ! command -v sudo &> /dev/null; then
echo -e "${RED}Error: sudo is not installed. Please install sudo or run as root.${NC}"
exit 1
fi
SUDO="sudo"
ANSIBLE_EXTRA_VARS="--ask-become-pass"
fi
echo -e "${GREEN}[1/3] Checking prerequisites...${NC}"
# Check if Ansible is installed
if ! command -v ansible-playbook &> /dev/null; then
echo -e "${YELLOW}Ansible not found. Installing Ansible and git...${NC}"
$SUDO apt-get update -qq
$SUDO apt-get install -y ansible git
echo -e "${GREEN}✓ Ansible and git installed${NC}"
else
echo -e "${GREEN}✓ Ansible already installed${NC}"
if ! command -v git &> /dev/null; then
echo -e "${YELLOW}git not found. Installing...${NC}"
$SUDO apt-get update -qq
$SUDO apt-get install -y git
echo -e "${GREEN}✓ git installed${NC}"
else
echo -e "${GREEN}✓ git already installed${NC}"
fi
fi
echo -e "${GREEN}[2/3] Installing OpenClaw collection...${NC}"
# Create temporary requirements file
REQUIREMENTS_FILE=$(mktemp)
cat > "$REQUIREMENTS_FILE" << EOF
---
collections:
- name: https://github.com/openclaw/openclaw-ansible.git
type: git
version: main
EOF
# Install collection
ansible-galaxy collection install -r "$REQUIREMENTS_FILE" --force
echo -e "${GREEN}✓ Collection installed${NC}"
echo -e "${GREEN}[3/3] Running Ansible playbook...${NC}"
if [ "$EUID" -ne 0 ]; then
echo -e "${YELLOW}You will be prompted for your sudo password.${NC}"
fi
echo ""
# Run the playbook
ansible-playbook openclaw.installer.install $ANSIBLE_EXTRA_VARS "$@"
# Cleanup
rm -f "$REQUIREMENTS_FILE"
+25
View File
@@ -0,0 +1,25 @@
# OpenClaw Ansible Inventory Sample
all:
children:
openclaw_servers:
hosts:
# Example 1: Simple IP address
192.168.1.100:
# Example 2: Hostname with specific variables
my-claw-server:
ansible_host: 192.168.1.101
ansible_user: admin
# Override default variables for this host
openclaw_install_mode: release
# Global variables for all OpenClaw servers
vars:
# SSH Public Keys for the 'openclaw' user (Optional)
# If set, these keys will be added to ~/.ssh/authorized_keys
# openclaw_ssh_keys:
# - "ssh-ed25519 AAAAC3Nz..."
# Tailscale Auth Key (Optional) - Leave empty to skip auto-connect
# tailscale_authkey: "tskey-auth-..."
+8
View File
@@ -0,0 +1,8 @@
all:
children:
openclaw_servers:
hosts:
zap:
ansible_host: 192.168.122.182
ansible_user: root
ansible_ssh_common_args: "-o StrictHostKeyChecking=no"
+2
View File
@@ -0,0 +1,2 @@
---
requires_ansible: '>=2.14.0'
+2
View File
@@ -0,0 +1,2 @@
---
- ansible.builtin.import_playbook: playbooks/install.yml
+47
View File
@@ -0,0 +1,47 @@
---
# Post-provisioning customizations for OpenClaw VMs
# Run after playbooks/install.yml to apply host-specific tweaks
#
# Usage:
# ansible-playbook -i inventory.yml playbooks/customize.yml
# ansible-playbook -i inventory.yml playbooks/customize.yml --limit zap
- name: OpenClaw VM customizations
hosts: openclaw_servers
become: true
tasks:
- name: Set vm.swappiness=10 (live)
ansible.posix.sysctl:
name: vm.swappiness
value: "10"
state: present
reload: true
- name: Persist vm.swappiness in /etc/sysctl.conf
ansible.builtin.lineinfile:
path: /etc/sysctl.conf
regexp: '^vm\.swappiness'
line: 'vm.swappiness=10'
state: present
- name: Create virtiofs mount point
ansible.builtin.file:
path: /mnt/swarm
state: directory
mode: "0755"
- name: Mount virtiofs swarm share via fstab
ansible.posix.mount:
path: /mnt/swarm
src: swarm
fstype: virtiofs
opts: defaults
state: present
# Note: actual mount requires reboot after VM config update
- name: Ensure openclaw user lingering is enabled (for user systemd services)
ansible.builtin.command:
cmd: loginctl enable-linger openclaw
changed_when: false
+39
View File
@@ -0,0 +1,39 @@
---
- name: Deploy OpenClaw to remote servers
hosts: openclaw_servers
become: true
vars:
ansible_python_interpreter: /usr/bin/python3
pre_tasks:
- name: Detect operating system
ansible.builtin.set_fact:
is_linux: "{{ ansible_system == 'Linux' }}"
is_debian_family: "{{ ansible_os_family == 'Debian' }}"
is_supported_distro: "{{ ansible_distribution in ['Debian', 'Ubuntu'] }}"
- name: Fail on unsupported non-Linux systems
ansible.builtin.fail:
msg: >-
Unsupported operating system: {{ ansible_system }}.
This installer supports Linux only.
when: not is_linux
- name: Fail on unsupported Linux distribution
ansible.builtin.fail:
msg: >-
Unsupported Linux distribution: {{ ansible_distribution }} {{ ansible_distribution_version }}.
This installer currently supports Debian and Ubuntu.
when:
- is_linux
- not is_supported_distro
- name: Install ACL for privilege escalation
ansible.builtin.package:
name: acl
state: present
when: is_supported_distro
roles:
- openclaw
+188
View File
@@ -0,0 +1,188 @@
---
- name: Install OpenClaw with Docker and UFW firewall
hosts: localhost
connection: local
become: true
vars:
ansible_python_interpreter: /usr/bin/python3
environment:
TERM: xterm-256color
COLORTERM: truecolor
pre_tasks:
- name: Enable color terminal for current session
ansible.builtin.set_fact:
ansible_env: "{{ ansible_env | combine({'TERM': 'xterm-256color', 'COLORTERM': 'truecolor'}) }}"
- name: Detect operating system
ansible.builtin.set_fact:
is_linux: "{{ ansible_system == 'Linux' }}"
is_debian_family: "{{ ansible_os_family == 'Debian' }}"
is_supported_distro: "{{ ansible_distribution in ['Debian', 'Ubuntu'] }}"
- name: Fail on unsupported non-Linux systems
ansible.builtin.fail:
msg: >-
Unsupported operating system: {{ ansible_system }}.
This installer supports Linux only.
when: not is_linux
- name: Fail on unsupported macOS
ansible.builtin.fail:
msg: >-
macOS bare-metal support has been deprecated and disabled.
Please use a Linux VM or container instead.
See README.md for details.
when: ansible_os_family == 'Darwin'
- name: Fail on unsupported Linux distribution
ansible.builtin.fail:
msg: >-
Unsupported Linux distribution: {{ ansible_distribution }} {{ ansible_distribution_version }}.
This installer currently supports Debian and Ubuntu.
when:
- is_linux
- not is_supported_distro
- name: Display detected OS
ansible.builtin.debug:
msg: |
Detected OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
OS Family: {{ ansible_os_family }}
Linux: {{ is_linux }}
Debian family: {{ is_debian_family }}
Supported distro: {{ is_supported_distro }}
- name: Update apt cache and upgrade all packages (Debian/Ubuntu)
ansible.builtin.apt:
update_cache: true
upgrade: dist
cache_valid_time: 3600
when: is_debian_family and not ci_test
register: apt_upgrade_result
- name: Display apt upgrade results
ansible.builtin.debug:
msg: "✅ System packages updated and upgraded"
when: is_debian_family and apt_upgrade_result.changed
- name: Install ACL for privilege escalation
ansible.builtin.package:
name: acl
state: present
when: is_supported_distro
- name: Check if running as root
ansible.builtin.command: id -u
register: user_id
changed_when: false
become: false
- name: Set fact for root user
ansible.builtin.set_fact:
is_root: "{{ user_id.stdout == '0' }}"
roles:
- openclaw
post_tasks:
- name: Copy ASCII art script
ansible.builtin.template:
src: "{{ playbook_dir }}/../roles/openclaw/templates/show-lobster.sh.j2"
dest: /tmp/show-lobster.sh
mode: '0755'
- name: Display ASCII art
ansible.builtin.command: /tmp/show-lobster.sh
changed_when: false
- name: Create one-time welcome message for openclaw user
ansible.builtin.copy:
dest: "{{ openclaw_home }}/.openclaw-welcome"
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
mode: '0644'
content: |
echo ""
echo "╔════════════════════════════════════════════════════════╗"
echo "║ 📋 OpenClaw Setup - Next Steps ║"
echo "╚════════════════════════════════════════════════════════╝"
echo ""
echo "You are: $(whoami)@$(hostname)"
echo "Home: $HOME"
echo "OS: $(uname -s) $(uname -r)"
echo ""
echo "Environment is configured:"
echo " ✓ XDG_RUNTIME_DIR: ${XDG_RUNTIME_DIR:-not set}"
echo " ✓ DBUS_SESSION_BUS_ADDRESS: ${DBUS_SESSION_BUS_ADDRESS:-not set}"
echo " ✓ OpenClaw: $(openclaw --version 2>/dev/null || echo 'not found')"
echo ""
echo "────────────────────────────────────────────────────────"
echo "🚀 Quick Start - Run This Command:"
echo "────────────────────────────────────────────────────────"
echo ""
echo " openclaw onboard --install-daemon"
echo ""
echo "This will:"
echo " • Guide you through the setup wizard"
echo " • Configure your messaging provider"
echo " • Install and start the daemon service"
echo ""
echo "────────────────────────────────────────────────────────"
echo "📚 Alternative Manual Setup:"
echo "────────────────────────────────────────────────────────"
echo ""
echo "1️⃣ Interactive onboarding (recommended):"
echo " openclaw onboard --install-daemon"
echo ""
echo "2️⃣ Manual configuration:"
echo " openclaw configure"
echo " nano ~/.openclaw/openclaw.json"
echo ""
echo "3️⃣ Login to messaging provider:"
echo " openclaw providers login"
echo ""
echo "4️⃣ Test the gateway:"
echo " openclaw gateway"
echo ""
echo "5️⃣ Install as daemon (if not using onboard):"
echo " openclaw daemon install"
echo " openclaw daemon start"
echo ""
echo "────────────────────────────────────────────────────────"
echo "🔧 Useful Commands:"
echo "────────────────────────────────────────────────────────"
echo ""
echo " • View logs: openclaw logs"
echo " • Check status: openclaw status"
echo " • Stop daemon: openclaw daemon stop"
echo " • Restart daemon: openclaw daemon restart"
echo " • Troubleshoot: openclaw doctor"
echo " • List agents: openclaw agents list"
echo ""
{% if tailscale_enabled | default(false) %}echo "────────────────────────────────────────────────────────"
echo "🌐 Connect Tailscale VPN (optional):"
echo "────────────────────────────────────────────────────────"
echo ""
echo " exit"
echo " sudo tailscale up"
echo ""
{% endif %}echo "────────────────────────────────────────────────────────"
echo ""
echo "Type 'exit' to return to your previous user"
echo ""
# Remove welcome message (suppress errors if already deleted)
rm -f "$HOME/.openclaw-welcome" 2>/dev/null || true
- name: Add welcome message to .bashrc
ansible.builtin.lineinfile:
path: "{{ openclaw_home }}/.bashrc"
line: '[ -f ~/.openclaw-welcome ] && source ~/.openclaw-welcome'
state: present
insertafter: EOF
- name: Notify that playbook is complete
ansible.builtin.debug:
msg: "✅ OpenClaw installation complete!"
+30
View File
@@ -0,0 +1,30 @@
---
# Provision a new OpenClaw VM from scratch on the hypervisor host.
#
# This playbook runs on localhost (the hypervisor) and:
# 1. Downloads the Ubuntu cloud image (cached)
# 2. Creates the VM disk image
# 3. Builds a cloud-init seed ISO for first-boot configuration
# 4. Defines the VM XML (EFI, memfd, virtiofs, TPM, watchdog)
# 5. Configures a static DHCP reservation
# 6. Enables autostart and starts the VM
# 7. Waits for SSH
#
# After this playbook completes, run:
# ansible-playbook -i inventory.yml playbooks/install.yml --limit <instance>
# ansible-playbook -i inventory.yml playbooks/customize.yml --limit <instance>
# ~/lab/swarm/restore-openclaw-vm.sh <instance> # to restore config from backup
#
# Usage:
# ansible-playbook -i inventory.yml playbooks/provision-vm.yml --limit zap
- name: Provision OpenClaw VM
hosts: openclaw_servers
connection: local
become: true
vars:
ansible_python_interpreter: /usr/bin/python3
roles:
- vm
+8
View File
@@ -0,0 +1,8 @@
---
collections:
- name: community.docker
version: ">=3.4.0"
- name: community.general
version: ">=8.0.0"
- name: ansible.posix
version: ">=1.5.0"
+42
View File
@@ -0,0 +1,42 @@
---
# OpenClaw default variables
# CI testing mode - skips tasks that require systemd, Docker-in-Docker, or kernel access
ci_test: false
# Tailscale settings
# WARNING: Tasks using tailscale_authkey MUST set no_log: true to prevent credential exposure
tailscale_enabled: false # Set to true to install and configure Tailscale
tailscale_authkey: "" # Optional: set to auto-connect during installation
# Node.js version
nodejs_version: "22.x"
# OpenClaw settings
openclaw_port: 3000
# OpenClaw config directory
openclaw_config_dir: "{{ openclaw_home }}/.openclaw"
# User settings (will be created as system user)
openclaw_user: openclaw
openclaw_home: /home/openclaw
# Installation mode: 'release' or 'development'
# release: Install via pnpm install -g openclaw@latest
# development: Clone repo, build from source, link globally
openclaw_install_mode: "release"
# Development mode settings (only used when openclaw_install_mode: development)
openclaw_repo_url: "https://github.com/openclaw/openclaw.git"
openclaw_repo_branch: "main"
openclaw_code_dir: "{{ openclaw_home }}/code"
openclaw_repo_dir: "{{ openclaw_code_dir }}/openclaw"
# SSH keys for openclaw user
# Add your public SSH keys here to allow SSH access as openclaw user
# Example:
# openclaw_ssh_keys:
# - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx user@host"
# - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDxxxxxxxxxxxxxxxxxxxxxxx user@host"
openclaw_ssh_keys: []
@@ -0,0 +1,106 @@
#!/bin/bash
set -e
# Enable 256 colors
export TERM=xterm-256color
export COLORTERM=truecolor
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# OpenClaw ASCII Art Lobster
cat << 'LOBSTER'
[0;36m
+====================================================+
| |
| [0;33mWelcome to OpenClaw! [0;31m🦞[0;36m |
| |
|[0;31m ,.---._ [0;36m|
|[0;31m ,,,, / `, [0;36m|
|[0;31m \\\\\\ / '\_ ; [0;36m|
|[0;31m |||| /\/``-.__\;' [0;36m|
|[0;31m ::::/\/_ [0;36m|
|[0;31m {{`-.__.-'(`(^^(^^^(^ 9 `.=========' [0;36m|
|[0;31m{{{{{{ { ( ( ( ( (-----:= [0;36m|
|[0;31m {{.-'~~'-.(,(,,(,,,(__6_.'=========. [0;36m|
|[0;31m ::::\/\ [0;36m|
|[0;31m |||| \/\ ,-'/, [0;36m|
|[0;31m //// \ `` _/ ; [0;36m|
|[0;31m '''' \ ` .' [0;36m|
|[0;31m `---' [0;36m|
| |
| [0;32m✅ Installation Successful![0;36m |
| |
+====================================================+[0m
LOBSTER
echo ""
echo -e "${GREEN}🔒 Security Status:${NC}"
echo " - UFW Firewall: ENABLED"
echo " - Open Ports: SSH (22) + Tailscale (41641/udp)"
echo " - Docker isolation: ACTIVE"
echo ""
echo -e "📚 Documentation: ${GREEN}https://docs.openclaw.ai${NC}"
echo ""
# Switch to openclaw user for setup
echo -e "${YELLOW}Switching to openclaw user for setup...${NC}"
echo ""
echo "DEBUG: About to create init script..."
# Create init script that will be sourced on login
cat > /home/openclaw/.openclaw-init << 'INIT_EOF'
# Display welcome message
echo "============================================"
echo "📋 OpenClaw Setup - Next Steps"
echo "============================================"
echo ""
echo "You are now: $(whoami)@$(hostname)"
echo "Home: $HOME"
echo ""
echo "🔧 Setup Commands:"
echo ""
echo "1. Configure OpenClaw:"
echo " nano ~/.openclaw/config.yml"
echo ""
echo "2. Login to provider (WhatsApp/Telegram/Signal):"
echo " openclaw login"
echo ""
echo "3. Test gateway:"
echo " openclaw gateway"
echo ""
echo "4. Exit and manage as service:"
echo " exit"
echo " sudo systemctl status openclaw"
echo " sudo journalctl -u openclaw -f"
echo ""
echo "5. Connect Tailscale (as root):"
echo " exit"
echo " sudo tailscale up"
echo ""
echo "============================================"
echo ""
echo "Type 'exit' to return to previous user"
echo ""
# Remove this init file after first login
rm -f ~/.openclaw-init
INIT_EOF
chown openclaw:openclaw /home/openclaw/.openclaw-init
# Add one-time sourcing to .bashrc if not already there
grep -q '.openclaw-init' /home/openclaw/.bashrc 2>/dev/null || {
echo '' >> /home/openclaw/.bashrc
echo '# One-time setup message' >> /home/openclaw/.bashrc
echo '[ -f ~/.openclaw-init ] && source ~/.openclaw-init' >> /home/openclaw/.bashrc
}
# Switch to openclaw user with explicit interactive shell
# Using setsid to create new session + force pseudo-terminal allocation
exec sudo -i -u openclaw /bin/bash --login
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
cat << 'LOBSTER'
[0;36m
+====================================================+
| |
| [0;33mWelcome to OpenClaw! [0;31m🦞[0;36m |
| |
|[0;31m ,.---._ [0;36m|
|[0;31m ,,,, / `, [0;36m|
|[0;31m \\\\\\ / '\_ ; [0;36m|
|[0;31m |||| /\/``-.__\;' [0;36m|
|[0;31m ::::/\/_ [0;36m|
|[0;31m {{`-.__.-'(`(^^(^^^(^ 9 `.=========' [0;36m|
|[0;31m{{{{{{ { ( ( ( ( (-----:= [0;36m|
|[0;31m {{.-'~~'-.(,(,,(,,,(__6_.'=========. [0;36m|
|[0;31m ::::\/\ [0;36m|
|[0;31m |||| \/\ ,-'/, [0;36m|
|[0;31m //// \ `` _/ ; [0;36m|
|[0;31m '''' \ ` .' [0;36m|
|[0;31m `---' [0;36m|
| |
| [0;32m✅ Installation Successful![0;36m |
| |
+====================================================+[0m
LOBSTER
echo ""
echo "🔒 Security Status:"
echo " - UFW Firewall: ENABLED"
echo " - Open Ports: SSH (22) + Tailscale (41641/udp)"
echo " - Docker isolation: ACTIVE"
echo ""
echo "📚 Documentation: https://docs.openclaw.ai"
echo ""
+10
View File
@@ -0,0 +1,10 @@
---
- name: Restart docker
ansible.builtin.systemd:
name: docker
state: restarted
- name: Restart fail2ban
ansible.builtin.systemd:
name: fail2ban
state: restarted
@@ -0,0 +1,69 @@
---
# Linux-specific Docker installation (apt-based)
- name: Install required system packages for Docker
ansible.builtin.apt:
name:
- ca-certificates
- curl
- gnupg
- lsb-release
state: present
update_cache: true
- name: Create directory for Docker GPG key
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
mode: '0755'
- name: Add Docker GPG key
ansible.builtin.shell:
cmd: |
set -o pipefail
curl -fsSL https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg | \
gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
creates: /etc/apt/keyrings/docker.gpg
executable: /bin/bash
- name: Add Docker repository
ansible.builtin.shell:
cmd: |
set -o pipefail
echo "deb [arch=$(dpkg --print-architecture) \
signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/{{ ansible_distribution | lower }} \
$(lsb_release -cs) stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
creates: /etc/apt/sources.list.d/docker.list
executable: /bin/bash
- name: Update apt cache after adding Docker repo
ansible.builtin.apt:
update_cache: true
- name: Install Docker CE
ansible.builtin.apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
state: present
- name: Ensure Docker service is started and enabled
ansible.builtin.systemd:
name: docker
state: started
enabled: true
- name: Add user to docker group
ansible.builtin.user:
name: "{{ openclaw_user }}"
groups: docker
append: true
- name: Reset SSH connection to apply docker group
ansible.builtin.meta: reset_connection
@@ -0,0 +1,165 @@
---
# Linux-specific firewall configuration (UFW) with security hardening
# Install and configure fail2ban for SSH brute-force protection
- name: Install fail2ban
ansible.builtin.apt:
name: fail2ban
state: present
update_cache: true
- name: Configure fail2ban for SSH protection
ansible.builtin.copy:
dest: /etc/fail2ban/jail.local
owner: root
group: root
mode: '0644'
content: |
# OpenClaw security hardening - SSH protection
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
backend = systemd
[sshd]
enabled = true
port = ssh
filter = sshd
# logpath not needed - systemd backend reads from journal
notify: Restart fail2ban
- name: Enable and start fail2ban
ansible.builtin.systemd:
name: fail2ban
state: started
enabled: true
# Install and configure unattended-upgrades for automatic security updates
- name: Install unattended-upgrades
ansible.builtin.apt:
name:
- unattended-upgrades
- apt-listchanges
state: present
- name: Configure automatic security updates
ansible.builtin.copy:
dest: /etc/apt/apt.conf.d/20auto-upgrades
owner: root
group: root
mode: '0644'
content: |
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
- name: Configure unattended-upgrades to only install security updates
ansible.builtin.copy:
dest: /etc/apt/apt.conf.d/50unattended-upgrades
owner: root
group: root
mode: '0644'
content: |
// OpenClaw security hardening - automatic security updates
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
"${distro_id}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
};
Unattended-Upgrade::Package-Blacklist {
};
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::MinimalSteps "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";
# UFW Firewall configuration
- name: Install UFW
ansible.builtin.apt:
name: ufw
state: present
update_cache: true
- name: Set UFW default policies
community.general.ufw:
direction: "{{ item.direction }}"
policy: "{{ item.policy }}"
loop:
- { direction: 'incoming', policy: 'deny' }
- { direction: 'outgoing', policy: 'allow' }
- { direction: 'routed', policy: 'deny' }
- name: Allow SSH on port 22
community.general.ufw:
rule: allow
port: '22'
proto: tcp
comment: 'SSH'
- name: Allow Tailscale UDP port 41641
community.general.ufw:
rule: allow
port: '41641'
proto: udp
comment: 'Tailscale'
when: tailscale_enabled | bool
- name: Get default network interface
ansible.builtin.shell:
cmd: |
set -o pipefail
ip route | grep default | awk '{print $5}' | head -n1
executable: /bin/bash
register: default_interface
changed_when: false
- name: Validate default network interface was detected
ansible.builtin.assert:
that:
- default_interface.stdout is defined
- default_interface.stdout | length > 0
fail_msg: "Failed to detect default network interface. Cannot configure firewall rules safely."
success_msg: "Default network interface detected: {{ default_interface.stdout }}"
- name: Create UFW after.rules for Docker isolation
ansible.builtin.blockinfile:
path: /etc/ufw/after.rules
marker: "# {mark} ANSIBLE MANAGED BLOCK - Docker isolation"
insertbefore: "^COMMIT$"
block: |
# Docker port isolation - block all forwarded traffic by default
:DOCKER-USER - [0:0]
# Allow established connections
-A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# Allow localhost
-A DOCKER-USER -i lo -j ACCEPT
# Block all other forwarded traffic to Docker containers from external interface
-A DOCKER-USER -i {{ default_interface.stdout }} -j DROP
create: false
- name: Create Docker daemon config directory
ansible.builtin.file:
path: /etc/docker
state: directory
mode: '0755'
- name: Configure Docker daemon to work with UFW
ansible.builtin.template:
src: daemon.json.j2
dest: /etc/docker/daemon.json
owner: root
group: root
mode: '0644'
notify: Restart docker
- name: Enable UFW
community.general.ufw:
state: enabled
- name: Reload UFW
community.general.ufw:
state: reloaded
+24
View File
@@ -0,0 +1,24 @@
---
- name: Include system tools installation tasks
ansible.builtin.include_tasks: system-tools.yml
- name: Include Tailscale installation tasks
ansible.builtin.include_tasks: tailscale-linux.yml
when: tailscale_enabled | bool
- name: Include user creation tasks
ansible.builtin.include_tasks: user.yml
- name: Include Docker installation tasks
ansible.builtin.include_tasks: docker-linux.yml
when: not ci_test
- name: Include firewall configuration tasks
ansible.builtin.include_tasks: firewall-linux.yml
when: not ci_test
- name: Include Node.js installation tasks
ansible.builtin.include_tasks: nodejs.yml
- name: Include OpenClaw setup tasks
ansible.builtin.include_tasks: openclaw.yml
+69
View File
@@ -0,0 +1,69 @@
---
- name: Install required packages for Node.js
ansible.builtin.apt:
name:
- ca-certificates
- curl
- gnupg
state: present
- name: Create directory for NodeSource GPG key
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
mode: '0755'
- name: Add NodeSource GPG key
ansible.builtin.shell:
cmd: |
set -o pipefail
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | \
gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
chmod a+r /etc/apt/keyrings/nodesource.gpg
creates: /etc/apt/keyrings/nodesource.gpg
executable: /bin/bash
- name: Add NodeSource repository
ansible.builtin.shell:
cmd: |
set -o pipefail
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] \
https://deb.nodesource.com/node_{{ nodejs_version }} nodistro main" | \
tee /etc/apt/sources.list.d/nodesource.list > /dev/null
creates: /etc/apt/sources.list.d/nodesource.list
executable: /bin/bash
- name: Update apt cache after adding NodeSource repo
ansible.builtin.apt:
update_cache: true
- name: Install Node.js
ansible.builtin.apt:
name: nodejs
state: present
- name: Check if pnpm is already installed
ansible.builtin.command: pnpm --version
register: pnpm_check
failed_when: false
changed_when: false
- name: Install pnpm globally
ansible.builtin.command: npm install -g pnpm
when: pnpm_check.rc != 0
- name: Verify Node.js installation
ansible.builtin.command: node --version
register: node_version
changed_when: false
- name: Verify pnpm installation
ansible.builtin.command: pnpm --version
register: pnpm_version
changed_when: false
- name: Display Node.js and pnpm versions
ansible.builtin.debug:
msg:
- "Node.js version: {{ node_version.stdout }}"
- "pnpm version: {{ pnpm_version.stdout }}"
@@ -0,0 +1,215 @@
---
# Development mode installation - Clone repo, build from source, link globally
- name: Create code directory
ansible.builtin.file:
path: "{{ openclaw_code_dir }}"
state: directory
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
mode: '0755'
- name: Check if openclaw repository already exists
ansible.builtin.stat:
path: "{{ openclaw_repo_dir }}/.git"
register: openclaw_repo_exists
- name: Clone openclaw repository
ansible.builtin.git:
repo: "{{ openclaw_repo_url }}"
dest: "{{ openclaw_repo_dir }}"
version: "{{ openclaw_repo_branch }}"
update: true
become: true
become_user: "{{ openclaw_user }}"
when: not openclaw_repo_exists.stat.exists
- name: Pull latest changes if repo exists
ansible.builtin.git:
repo: "{{ openclaw_repo_url }}"
dest: "{{ openclaw_repo_dir }}"
version: "{{ openclaw_repo_branch }}"
update: true
become: true
become_user: "{{ openclaw_user }}"
when: openclaw_repo_exists.stat.exists
register: git_pull_result
- name: Display git pull status
ansible.builtin.debug:
msg: "Git repository updated: {{ git_pull_result.changed | default(false) }}"
when: openclaw_repo_exists.stat.exists
- name: Install dependencies with pnpm
ansible.builtin.shell:
cmd: pnpm install
chdir: "{{ openclaw_repo_dir }}"
executable: /bin/bash
become: true
become_user: "{{ openclaw_user }}"
environment:
PNPM_HOME: "{{ openclaw_home }}/.local/share/pnpm"
PATH: "{{ openclaw_home }}/.local/bin:{{ openclaw_home }}/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin"
HOME: "{{ openclaw_home }}"
register: pnpm_install_result
changed_when: "'Already up to date' not in pnpm_install_result.stdout"
- name: Build openclaw from source
ansible.builtin.shell:
cmd: pnpm build
chdir: "{{ openclaw_repo_dir }}"
executable: /bin/bash
become: true
become_user: "{{ openclaw_user }}"
environment:
PNPM_HOME: "{{ openclaw_home }}/.local/share/pnpm"
PATH: "{{ openclaw_home }}/.local/bin:{{ openclaw_home }}/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin"
HOME: "{{ openclaw_home }}"
register: pnpm_build_result
changed_when: true # Build always changes dist/ directory
- name: Display build output
ansible.builtin.debug:
msg: "Build completed successfully"
when: pnpm_build_result.rc == 0
- name: Check if dist directory exists
ansible.builtin.stat:
path: "{{ openclaw_repo_dir }}/dist"
register: dist_dir
- name: Fail if build didn't create dist directory
ansible.builtin.fail:
msg: "Build failed - dist directory not found"
when: not dist_dir.stat.exists
- name: Check for package metadata
ansible.builtin.stat:
path: "{{ openclaw_repo_dir }}/package.json"
register: openclaw_package_json_stat
- name: Read package metadata
ansible.builtin.slurp:
src: "{{ openclaw_repo_dir }}/package.json"
register: openclaw_package_json_raw
when: openclaw_package_json_stat.stat.exists
- name: Parse package metadata
ansible.builtin.set_fact:
openclaw_package_json: "{{ openclaw_package_json_raw.content | b64decode | from_json }}"
when: openclaw_package_json_stat.stat.exists
- name: Resolve metadata-defined CLI entrypoint
ansible.builtin.set_fact:
openclaw_package_bin_entry: >-
{{
openclaw_package_json.bin
if openclaw_package_json.bin is string else
(
openclaw_package_json.bin.openclaw
if openclaw_package_json.bin is mapping and 'openclaw' in openclaw_package_json.bin else
(
(openclaw_package_json.bin | dict2items | map(attribute='value') | list | first)
if openclaw_package_json.bin is mapping else
''
)
)
}}
when: openclaw_package_json_stat.stat.exists
- name: Build CLI entrypoint candidate list
ansible.builtin.set_fact:
openclaw_cli_candidate_paths: >-
{{
[
openclaw_package_bin_entry | default(''),
'openclaw.mjs',
'bin/openclaw.js',
'dist/index.js'
] | reject('equalto', '') | unique | list
}}
- name: Check possible OpenClaw CLI entrypoints
ansible.builtin.stat:
path: "{{ openclaw_repo_dir }}/{{ item }}"
loop: "{{ openclaw_cli_candidate_paths }}"
register: openclaw_cli_candidates
- name: Resolve OpenClaw CLI entrypoint path
ansible.builtin.set_fact:
openclaw_cli_entry: >-
{{
(
openclaw_cli_candidates.results
| selectattr('stat.exists')
| map(attribute='stat.path')
| list
| first
) | default('')
}}
changed_when: false
- name: Fail if openclaw CLI entrypoint is missing
ansible.builtin.fail:
msg: >-
Unable to locate OpenClaw CLI entrypoint in {{ openclaw_repo_dir }}.
Expected one of: {{ openclaw_cli_candidate_paths | join(', ') }}
when: openclaw_cli_entry == ""
- name: Remove existing global openclaw symlink (if any)
ansible.builtin.file:
path: "{{ openclaw_home }}/.local/bin/openclaw"
state: absent
- name: Create symlink to openclaw binary
ansible.builtin.file:
src: "{{ openclaw_cli_entry }}"
dest: "{{ openclaw_home }}/.local/bin/openclaw"
state: link
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
force: true
- name: Make openclaw binary executable
ansible.builtin.file:
path: "{{ openclaw_cli_entry }}"
mode: '0755'
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
- name: Verify openclaw installation from development build
ansible.builtin.shell:
cmd: openclaw --version
executable: /bin/bash
become: true
become_user: "{{ openclaw_user }}"
environment:
PNPM_HOME: "{{ openclaw_home }}/.local/share/pnpm"
PATH: "{{ openclaw_home }}/.local/bin:{{ openclaw_home }}/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin"
HOME: "{{ openclaw_home }}"
register: openclaw_dev_version
changed_when: false
- name: Display installed OpenClaw version (development build)
ansible.builtin.debug:
msg: |
OpenClaw installed from source: {{ openclaw_dev_version.stdout }}
Repository: {{ openclaw_repo_dir }}
Binary: {{ openclaw_home }}/.local/bin/openclaw -> {{ openclaw_cli_entry }}
- name: Add development mode info to .bashrc
ansible.builtin.blockinfile:
path: "{{ openclaw_home }}/.bashrc"
marker: "# {mark} ANSIBLE MANAGED BLOCK - OpenClaw development"
block: |
# OpenClaw development mode
export OPENCLAW_DEV_DIR="{{ openclaw_repo_dir }}"
# Aliases for development
alias openclaw-rebuild='cd {{ openclaw_repo_dir }} && pnpm build'
alias openclaw-dev='cd {{ openclaw_repo_dir }}'
alias openclaw-pull='cd {{ openclaw_repo_dir }} && git pull && pnpm install && pnpm build'
create: true
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
mode: '0644'
@@ -0,0 +1,32 @@
---
# Release mode installation - Install via pnpm from npm registry
- name: Install OpenClaw globally as openclaw user (using pnpm)
ansible.builtin.shell:
cmd: pnpm install -g openclaw@latest
executable: /bin/bash
become: true
become_user: "{{ openclaw_user }}"
environment:
PNPM_HOME: "{{ openclaw_home }}/.local/share/pnpm"
PATH: "{{ openclaw_home }}/.local/bin:{{ openclaw_home }}/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin"
HOME: "{{ openclaw_home }}"
register: openclaw_install
changed_when: "'Already up to date' not in openclaw_install.stdout"
- name: Verify openclaw installation
ansible.builtin.shell:
cmd: openclaw --version
executable: /bin/bash
become: true
become_user: "{{ openclaw_user }}"
environment:
PNPM_HOME: "{{ openclaw_home }}/.local/share/pnpm"
PATH: "{{ openclaw_home }}/.local/bin:{{ openclaw_home }}/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin"
HOME: "{{ openclaw_home }}"
register: openclaw_version
changed_when: false
- name: Display installed OpenClaw version (release)
ansible.builtin.debug:
msg: "OpenClaw installed from npm: {{ openclaw_version.stdout }}"
+121
View File
@@ -0,0 +1,121 @@
---
- name: Validate openclaw_install_mode
ansible.builtin.assert:
that:
- openclaw_install_mode in ["release", "development"]
fail_msg: "Invalid openclaw_install_mode: '{{ openclaw_install_mode }}'. Must be 'release' or 'development'."
success_msg: "Valid install mode: {{ openclaw_install_mode }}"
- name: Ensure openclaw home directory exists with correct ownership
ansible.builtin.file:
path: "{{ openclaw_home }}"
state: directory
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
mode: '0755'
- name: Create OpenClaw directories (structure only, no config files)
ansible.builtin.file:
path: "{{ item.path }}"
state: directory
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
mode: "{{ item.mode }}"
loop:
- { path: "{{ openclaw_config_dir }}", mode: '0755' }
- { path: "{{ openclaw_config_dir }}/sessions", mode: '0755' }
- { path: "{{ openclaw_config_dir }}/credentials", mode: '0700' }
- { path: "{{ openclaw_config_dir }}/data", mode: '0755' }
- { path: "{{ openclaw_config_dir }}/logs", mode: '0755' }
- { path: "{{ openclaw_config_dir }}/agents", mode: '0755' }
- { path: "{{ openclaw_config_dir }}/agents/main", mode: '0755' }
- { path: "{{ openclaw_config_dir }}/agents/main/agent", mode: '0700' }
- { path: "{{ openclaw_config_dir }}/workspace", mode: '0755' }
- name: Create pnpm directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
mode: '0755'
loop:
- "{{ openclaw_home }}/.local/share/pnpm"
- "{{ openclaw_home }}/.local/share/pnpm/store"
- "{{ openclaw_home }}/.local/bin"
- name: Ensure pnpm directories have correct ownership
ansible.builtin.file:
path: "{{ openclaw_home }}/.local/share/pnpm"
state: directory
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
recurse: true
mode: '0755'
- name: Configure pnpm for openclaw user
ansible.builtin.shell:
cmd: |
CURRENT_GLOBAL_DIR=$(pnpm config get global-dir 2>/dev/null || echo "")
CURRENT_BIN_DIR=$(pnpm config get global-bin-dir 2>/dev/null || echo "")
CHANGED=0
if [ "$CURRENT_GLOBAL_DIR" != "{{ openclaw_home }}/.local/share/pnpm" ]; then
pnpm config set global-dir {{ openclaw_home }}/.local/share/pnpm
CHANGED=1
fi
if [ "$CURRENT_BIN_DIR" != "{{ openclaw_home }}/.local/bin" ]; then
pnpm config set global-bin-dir {{ openclaw_home }}/.local/bin
CHANGED=1
fi
exit $CHANGED
executable: /bin/bash
become: true
become_user: "{{ openclaw_user }}"
register: pnpm_config_result
changed_when: pnpm_config_result.rc == 1
failed_when: pnpm_config_result.rc > 1
- name: Display installation mode
ansible.builtin.debug:
msg: "Installation mode: {{ openclaw_install_mode }}"
# Include appropriate installation method based on mode
- name: Include release installation (pnpm install -g)
ansible.builtin.include_tasks: openclaw-release.yml
when: openclaw_install_mode == "release"
- name: Include development installation (git clone + build + link)
ansible.builtin.include_tasks: openclaw-development.yml
when: openclaw_install_mode == "development"
- name: Configure .bashrc for openclaw user (base config)
ansible.builtin.blockinfile:
path: "{{ openclaw_home }}/.bashrc"
marker: "# {mark} ANSIBLE MANAGED BLOCK - OpenClaw pnpm"
block: |
# pnpm configuration
export PNPM_HOME="{{ openclaw_home }}/.local/share/pnpm"
export PATH="{{ openclaw_home }}/.local/bin:$PNPM_HOME:$PATH"
create: true
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
mode: '0644'
insertafter: EOF
# NOTE: We do NOT create config.yml here - openclaw onboard/configure will do that
# We also do NOT install the systemd service - openclaw onboard --install-daemon will do that
# The .openclaw directory structure is created above, but config and daemon are user-initiated
- name: Display configuration note
ansible.builtin.debug:
msg: |
OpenClaw is installed but NOT configured yet.
Next steps (run as openclaw user):
1. Switch user: sudo su - {{ openclaw_user }}
2. Run onboarding: openclaw onboard --install-daemon
This will:
- Create configuration files (~/.openclaw/openclaw.json)
- Guide you through provider setup
- Install and start the daemon service automatically
@@ -0,0 +1,53 @@
---
# Linux-specific system tools installation (apt-based)
- name: Install essential system tools (Linux - apt)
ansible.builtin.apt:
name:
# Editors
- vim
- nano
# Version control
- git
- git-lfs
# Network tools
- curl
- wget
- netcat-openbsd
- net-tools
- dnsutils
- iputils-ping
- traceroute
- tcpdump
- nmap
- socat
- telnet
# Debugging tools
- strace
- lsof
- gdb
- htop
- iotop
- iftop
- sysstat
- procps
# System utilities
- tmux
- tree
- jq
- unzip
- rsync
- less
# Build essentials for development
- build-essential
- file
state: present
update_cache: true
- name: Deploy global vim configuration (Linux)
ansible.builtin.template:
src: vimrc.j2
dest: /etc/vim/vimrc.local
owner: root
group: root
mode: '0644'
@@ -0,0 +1,25 @@
---
# Main system tools orchestration - Linux only
- name: Include Linux system tools installation
ansible.builtin.include_tasks: system-tools-linux.yml
# Common tasks for all operating systems
- name: Configure git globally
community.general.git_config:
name: "{{ item.name }}"
scope: global
value: "{{ item.value }}"
loop:
- { name: 'init.defaultBranch', value: 'main' }
- { name: 'pull.rebase', value: 'false' }
- { name: 'core.editor', value: 'vim' }
- { name: 'color.ui', value: 'auto' }
- { name: 'alias.st', value: 'status' }
- { name: 'alias.co', value: 'checkout' }
- { name: 'alias.br', value: 'branch' }
- { name: 'alias.ci', value: 'commit' }
- { name: 'alias.unstage', value: 'reset HEAD --' }
- { name: 'alias.last', value: 'log -1 HEAD' }
- { name: 'alias.lg', value: 'log --oneline --graph --decorate --all' }
@@ -0,0 +1,61 @@
---
# Linux-specific Tailscale installation (Debian/Ubuntu)
- name: Add Tailscale GPG key
ansible.builtin.shell:
cmd: |
set -o pipefail
DIST="{{ ansible_distribution | lower }}"
RELEASE="{{ ansible_distribution_release }}"
curl -fsSL "https://pkgs.tailscale.com/stable/${DIST}/${RELEASE}.noarmor.gpg" | \
tee /usr/share/keyrings/tailscale-archive-keyring.gpg > /dev/null
creates: /usr/share/keyrings/tailscale-archive-keyring.gpg
executable: /bin/bash
- name: Add Tailscale repository
ansible.builtin.shell:
cmd: |
set -o pipefail
DIST="{{ ansible_distribution | lower }}"
RELEASE="{{ ansible_distribution_release }}"
curl -fsSL "https://pkgs.tailscale.com/stable/${DIST}/${RELEASE}.tailscale-keyring.list" | \
tee /etc/apt/sources.list.d/tailscale.list > /dev/null
creates: /etc/apt/sources.list.d/tailscale.list
executable: /bin/bash
- name: Update apt cache after adding Tailscale repo
ansible.builtin.apt:
update_cache: true
- name: Install Tailscale
ansible.builtin.apt:
name: tailscale
state: present
- name: Enable Tailscale service (Linux)
ansible.builtin.systemd:
name: tailscaled
enabled: true
state: started
- name: Check if Tailscale is already connected (Linux)
ansible.builtin.command: tailscale status --json
register: tailscale_status_linux
changed_when: false
failed_when: false
- name: Display Tailscale auth URL if not connected (Linux)
ansible.builtin.debug:
msg:
- "============================================"
- "Tailscale installed but not connected yet"
- "============================================"
- ""
- "To connect this machine to your Tailnet:"
- "Run: sudo tailscale up"
- ""
- "For unattended installation, use an auth key:"
- "sudo tailscale up --authkey tskey-auth-xxxxx"
- ""
- "Get auth key from: https://login.tailscale.com/admin/settings/keys"
when: tailscale_status_linux.rc != 0
+192
View File
@@ -0,0 +1,192 @@
---
- name: Create openclaw system user
ansible.builtin.user:
name: "{{ openclaw_user }}"
comment: "OpenClaw Service User"
system: true
shell: /bin/bash
create_home: true
home: "{{ openclaw_home }}"
state: present
- name: Ensure openclaw home directory has correct ownership
ansible.builtin.file:
path: "{{ openclaw_home }}"
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
state: directory
mode: '0755'
- name: Configure .bashrc for openclaw user
ansible.builtin.blockinfile:
path: "{{ openclaw_home }}/.bashrc"
marker: "# {mark} ANSIBLE MANAGED BLOCK - OpenClaw config"
block: |
# Enable 256 colors
export TERM=xterm-256color
export COLORTERM=truecolor
# Add pnpm to PATH
export PNPM_HOME="{{ openclaw_home }}/.local/share/pnpm"
export PATH="{{ openclaw_home }}/.local/bin:$PNPM_HOME:$PATH"
# Color support for common tools
export CLICOLOR=1
export LS_COLORS='di=34:ln=35:so=32:pi=33:ex=31:bd=34;46:cd=34;43:su=30;41:sg=30;46:tw=30;42:ow=30;43'
# Aliases
alias ls='ls --color=auto'
alias grep='grep --color=auto'
alias ll='ls -lah'
create: true
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
mode: '0644'
- name: Add openclaw user to sudoers with scoped NOPASSWD
ansible.builtin.copy:
dest: "/etc/sudoers.d/{{ openclaw_user }}"
mode: '0440'
owner: root
group: root
content: |
# OpenClaw sudo permissions (scoped for security)
#
# SECURITY NOTE: These permissions are intentionally limited.
# If openclaw is compromised, attackers can only:
# - Manage the openclaw service
# - Run basic tailscale diagnostics
# - View openclaw logs
#
# To grant full tailscale control (e.g., for self-healing VPN):
# {{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale *
#
# To grant full sudo (NOT RECOMMENDED):
# {{ openclaw_user }} ALL=(ALL) NOPASSWD: ALL
# Service control - openclaw service only
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl start openclaw
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop openclaw
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart openclaw
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl status openclaw
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl enable openclaw
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl disable openclaw
# daemon-reload affects all units (required after service file changes)
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl daemon-reload
# Tailscale - diagnostics + connect/disconnect
# NOTE: 'up' allows flags like --advertise-exit-node. For tighter control,
# remove 'up' and 'down' lines - operator must then manage VPN manually.
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale status
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale up *
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale down
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale ip *
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale version
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale ping *
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale whois *
# Journal access - openclaw logs only
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/journalctl -u openclaw *
validate: /usr/sbin/visudo -cf %s
- name: Set openclaw user as primary user for installation
ansible.builtin.set_fact:
openclaw_user: "{{ openclaw_user }}"
openclaw_home: "{{ openclaw_home }}"
- name: Create .bash_profile to source .bashrc for login shells
ansible.builtin.copy:
dest: "{{ openclaw_home }}/.bash_profile"
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
mode: '0644'
content: |
# .bash_profile - Executed for login shells
# Source .bashrc to ensure environment is loaded for login shells
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
# Fix DBus issues for systemd user services
- name: Get openclaw user ID
ansible.builtin.command: "id -u {{ openclaw_user }}"
register: openclaw_uid
changed_when: false
when: ansible_os_family == 'Debian' and not ci_test
- name: Display openclaw user ID
ansible.builtin.debug:
msg: "OpenClaw user ID: {{ openclaw_uid.stdout }}"
when: ansible_os_family == 'Debian' and not ci_test
- name: Enable lingering for openclaw user (allows systemd user services without login)
ansible.builtin.command: "loginctl enable-linger {{ openclaw_user }}"
changed_when: false
when: ansible_os_family == 'Debian' and not ci_test
- name: Create runtime directory for openclaw user
ansible.builtin.file:
path: "/run/user/{{ openclaw_uid.stdout }}"
state: directory
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
mode: '0700'
when: ansible_os_family == 'Debian' and not ci_test
- name: Store openclaw UID as fact for later use
ansible.builtin.set_fact:
openclaw_uid_value: "{{ openclaw_uid.stdout }}"
when: ansible_os_family == 'Debian' and not ci_test
# SSH key configuration
- name: Create .ssh directory for openclaw user
ansible.builtin.file:
path: "{{ openclaw_home }}/.ssh"
state: directory
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
mode: '0700'
- name: Add SSH authorized keys for openclaw user
ansible.posix.authorized_key:
user: "{{ openclaw_user }}"
state: present
key: "{{ item }}"
loop: "{{ openclaw_ssh_keys }}"
when: openclaw_ssh_keys | length > 0
- name: Display SSH key configuration status
ansible.builtin.debug:
msg: "{{ openclaw_ssh_keys | length }} SSH key(s) configured for openclaw user"
when: openclaw_ssh_keys | length > 0
- name: Display SSH key warning if none configured
ansible.builtin.debug:
msg: "No SSH keys configured. Set 'openclaw_ssh_keys' variable to allow SSH access."
when: openclaw_ssh_keys | length == 0
- name: Set XDG_RUNTIME_DIR in .bashrc for openclaw user
ansible.builtin.lineinfile:
path: "{{ openclaw_home }}/.bashrc"
line: 'export XDG_RUNTIME_DIR=/run/user/$(id -u)'
state: present
create: true
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
mode: '0644'
when: ansible_os_family == 'Debian' and not ci_test
- name: Set DBUS_SESSION_BUS_ADDRESS in .bashrc for openclaw user
ansible.builtin.blockinfile:
path: "{{ openclaw_home }}/.bashrc"
marker: "# {mark} ANSIBLE MANAGED BLOCK - DBus config"
block: |
# DBus session bus configuration
if [ -z "$DBUS_SESSION_BUS_ADDRESS" ]; then
export DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus"
fi
create: true
owner: "{{ openclaw_user }}"
group: "{{ openclaw_user }}"
mode: '0644'
when: ansible_os_family == 'Debian' and not ci_test
@@ -0,0 +1,18 @@
{
"iptables": true,
"ip-forward": true,
"userland-proxy": false,
"live-restore": true,
"ip6tables": false,
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"default-address-pools": [
{
"base": "172.17.0.0/12",
"size": 24
}
]
}
@@ -0,0 +1,76 @@
# OpenClaw Configuration Template
# Generated by Ansible on {{ ansible_date_time.iso8601 }}
#
# For full documentation, visit: https://docs.openclaw.ai/configuration
# Connection Provider
# Options: whatsapp, telegram, signal
provider: whatsapp
# WhatsApp Configuration (if using whatsapp provider)
whatsapp:
# Phone number in international format (e.g., +4366412345678)
phone: ""
# Telegram Configuration (if using telegram provider)
telegram:
# Telegram bot token from @BotFather
token: ""
# Signal Configuration (if using signal provider)
signal:
# Signal phone number
phone: ""
# AI Model Configuration
ai:
# Model provider: anthropic, openai
provider: anthropic
# API Keys (set as environment variables or here)
# anthropic_api_key: ""
# openai_api_key: ""
# Model selection
model: claude-3-5-sonnet-20241022
# Max tokens per response
max_tokens: 4096
# Gateway Settings
gateway:
# Port for web interface
port: {{ openclaw_port }}
# Enable web UI
web_ui: true
# Logging
logging:
# Log level: debug, info, warn, error
level: info
# Log file location
file: {{ openclaw_config_dir }}/openclaw.log
# Security
security:
# Allowed phone numbers (whitelist)
# Leave empty to allow all
allowed_numbers: []
# Rate limiting
rate_limit:
enabled: true
max_requests_per_minute: 10
# Advanced Settings
advanced:
# Session timeout in minutes
session_timeout: 60
# Auto-reconnect on disconnect
auto_reconnect: true
# Keep-alive interval in seconds
keep_alive_interval: 30
@@ -0,0 +1,42 @@
[Unit]
Description=OpenClaw AI Gateway
After=network.target docker.service
Requires=docker.service
[Service]
Type=simple
User={{ openclaw_user }}
Group={{ openclaw_user }}
WorkingDirectory={{ openclaw_home }}
# Environment variables
Environment="PNPM_HOME={{ openclaw_home }}/.local/share/pnpm"
Environment="PATH={{ openclaw_home }}/.local/bin:{{ openclaw_home }}/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin"
Environment="HOME={{ openclaw_home }}"
Environment="XDG_RUNTIME_DIR=/run/user/{{ openclaw_uid_value }}"
# DBus session bus
Environment="DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{{ openclaw_uid_value }}/bus"
# Start command
ExecStart=openclaw gateway
# Restart policy
Restart=always
RestartSec=10
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths={{ openclaw_home }}/.openclaw
ReadWritePaths={{ openclaw_home }}/.local
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=openclaw
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,42 @@
#jinja2: lstrip_blocks: True
{% raw %}#!/bin/bash
cat << 'LOBSTER'
[0;36m
+====================================================+
| |
| [0;33mWelcome to OpenClaw! [0;31m🦞[0;36m |
| |
|[0;31m ,.---._ [0;36m|
|[0;31m ,,,, / `, [0;36m|
|[0;31m \\\ / '\_ ; [0;36m|
|[0;31m |||| /\/``-.__\;' [0;36m|
|[0;31m ::::/\/_ [0;36m|
|[0;31m {{`-.__.-'(`(^^(^^^(^ 9 `.=========' [0;36m|
|[0;31m{{{{{{ { ( ( ( ( (-----:= [0;36m|
|[0;31m {{.-'~~'-.(,(,,(,,,(__6_.'=========. [0;36m|
|[0;31m ::::\/\ [0;36m|
|[0;31m |||| \/\ ,-'/, [0;36m|
|[0;31m //// \ `` _/ ; [0;36m|
|[0;31m '''' \ ` .' [0;36m|
|[0;31m `---' [0;36m|
| |
| [0;32m✅ Installation Successful![0;36m |
| |
+====================================================+[0m
LOBSTER
echo ""
echo "🔒 Security Status:"
echo " - UFW Firewall: ENABLED"
{% endraw %}
{% if tailscale_enabled | default(false) %}
echo " - Open Ports: SSH (22) + Tailscale (41641/udp)"
{% else %}
echo " - Open Ports: SSH (22)"
{% endif %}
{% raw %}
echo " - Docker isolation: ACTIVE"
echo ""
echo "📚 Documentation: https://github.com/openclaw/openclaw-ansible"
echo ""
{% endraw %}
+136
View File
@@ -0,0 +1,136 @@
" Vim Configuration - Generated by Ansible
" Modern, practical vim setup for development and debugging
" Basic Settings
set nocompatible " Disable vi compatibility
filetype plugin indent on " Enable file type detection
syntax on " Enable syntax highlighting
" UI Settings
set number " Show line numbers
set relativenumber " Show relative line numbers
set ruler " Show cursor position
set showcmd " Show command in bottom bar
set wildmenu " Visual autocomplete for command menu
set showmatch " Highlight matching brackets
set cursorline " Highlight current line
set laststatus=2 " Always show status line
set colorcolumn=80,120 " Show column markers
" Search Settings
set incsearch " Search as characters are entered
set hlsearch " Highlight search results
set ignorecase " Case insensitive search
set smartcase " Case sensitive when uppercase present
" Indentation
set autoindent " Auto-indent new lines
set smartindent " Smart indent
set expandtab " Use spaces instead of tabs
set tabstop=2 " Number of visual spaces per TAB
set shiftwidth=2 " Number of spaces for auto-indent
set softtabstop=2 " Number of spaces in tab when editing
" Performance
set lazyredraw " Don't redraw while executing macros
set ttyfast " Fast terminal connection
" Backups and Undo
set nobackup " No backup files
set nowritebackup " No backup while editing
set noswapfile " No swap files
set undofile " Persistent undo
set undodir=~/.vim/undo " Undo directory
set undolevels=1000 " Maximum number of undos
set undoreload=10000 " Maximum lines to save for undo
" File Handling
set encoding=utf-8 " Use UTF-8 encoding
set fileencoding=utf-8 " File encoding
set autoread " Auto-reload changed files
set hidden " Allow hidden buffers
" Navigation
set scrolloff=8 " Keep 8 lines above/below cursor
set sidescrolloff=8 " Keep 8 columns left/right of cursor
set mouse=a " Enable mouse support
" Folding
set foldmethod=indent " Fold based on indentation
set foldlevel=99 " Open all folds by default
" Status Line
set statusline=%F " Full file path
set statusline+=%m " Modified flag
set statusline+=%r " Read-only flag
set statusline+=%h " Help buffer flag
set statusline+=%w " Preview window flag
set statusline+=%= " Right align
set statusline+=%y " File type
set statusline+=\ [%{&ff}] " File format
set statusline+=\ [%{strlen(&fenc)?&fenc:'none'}] " File encoding
set statusline+=\ %l:%c " Line:Column
set statusline+=\ %p%% " Percentage through file
" Key Mappings
let mapleader = "," " Set leader key to comma
" Quick save
nnoremap <leader>w :w<CR>
" Quick quit
nnoremap <leader>q :q<CR>
" Clear search highlighting
nnoremap <leader><space> :nohlsearch<CR>
" Split navigation
nnoremap <C-h> <C-w>h
nnoremap <C-j> <C-w>j
nnoremap <C-k> <C-w>k
nnoremap <C-l> <C-w>l
" Tab navigation
nnoremap <leader>tn :tabnew<CR>
nnoremap <leader>tc :tabclose<CR>
nnoremap <leader>1 1gt
nnoremap <leader>2 2gt
nnoremap <leader>3 3gt
nnoremap <leader>4 4gt
nnoremap <leader>5 5gt
" Buffer navigation
nnoremap <leader>bn :bnext<CR>
nnoremap <leader>bp :bprevious<CR>
nnoremap <leader>bd :bdelete<CR>
" Paste toggle
set pastetoggle=<F2>
" File Type Specific
autocmd FileType python setlocal tabstop=4 shiftwidth=4 softtabstop=4
autocmd FileType javascript,typescript,json setlocal tabstop=2 shiftwidth=2 softtabstop=2
autocmd FileType yaml,yml setlocal tabstop=2 shiftwidth=2 softtabstop=2
autocmd FileType go setlocal tabstop=4 shiftwidth=4 softtabstop=4 noexpandtab
autocmd FileType markdown setlocal wrap linebreak nolist
" Auto-create undo directory
if !isdirectory($HOME."/.vim/undo")
call mkdir($HOME."/.vim/undo", "p", 0700)
endif
" Colors
set background=dark
if &term =~ "xterm" || &term =~ "screen"
set t_Co=256
endif
" Highlight trailing whitespace
highlight ExtraWhitespace ctermbg=red guibg=red
match ExtraWhitespace /\s\+$/
" Remember cursor position
autocmd BufReadPost *
\ if line("'\"") > 1 && line("'\"") <= line("$") |
\ exe "normal! g`\"" |
\ endif
+34
View File
@@ -0,0 +1,34 @@
---
# VM provisioning defaults — override in host_vars/<instance>.yml
# Libvirt connection URI
vm_libvirt_uri: qemu:///system
# Cloud image
vm_ubuntu_release: noble
vm_cloud_image_url: "https://cloud-images.ubuntu.com/{{ vm_ubuntu_release }}/current/{{ vm_ubuntu_release }}-server-cloudimg-amd64.img"
vm_cloud_image_cache: "/var/lib/libvirt/images/{{ vm_ubuntu_release }}-cloudimg-amd64.img"
# VM identity
vm_domain: "" # full libvirt domain name, e.g. "zap [claw]"
vm_hostname: "" # guest hostname
vm_disk_path: "" # path to qcow2 disk image
# Resources
vm_memory_mib: 3072
vm_vcpus: 2
vm_disk_size: "60G"
# Network
vm_mac: "" # MAC address, e.g. "52:54:00:01:00:71"
vm_ip: "" # static IP for DHCP reservation
vm_network: default
# virtiofs share (host → guest)
vm_virtiofs_source: "" # host path
vm_virtiofs_tag: "" # mount tag used inside guest
# OVMF firmware (Arch/CachyOS paths)
vm_ovmf_code: /usr/share/edk2/x64/OVMF_CODE.secboot.4m.fd
vm_ovmf_vars_template: /usr/share/edk2/x64/OVMF_VARS.4m.fd
vm_ovmf_vars_dir: /var/lib/libvirt/qemu/nvram
+149
View File
@@ -0,0 +1,149 @@
---
# Provision a KVM/libvirt VM from an Ubuntu cloud image.
# Runs on the hypervisor host (localhost).
- name: Validate required variables
ansible.builtin.assert:
that:
- vm_domain | length > 0
- vm_hostname | length > 0
- vm_disk_path | length > 0
- vm_mac | length > 0
- vm_ip | length > 0
fail_msg: "vm_domain, vm_hostname, vm_disk_path, vm_mac, and vm_ip must all be set in host_vars"
- name: Install host dependencies
ansible.builtin.package:
name:
- qemu-img
- genisoimage
- libvirt-utils
state: present
# ── Cloud image ────────────────────────────────────────────────────────────
- name: Check if cloud image cache exists
ansible.builtin.stat:
path: "{{ vm_cloud_image_cache }}"
register: cloud_image_stat
- name: Download Ubuntu cloud image
ansible.builtin.get_url:
url: "{{ vm_cloud_image_url }}"
dest: "{{ vm_cloud_image_cache }}"
mode: "0644"
timeout: 300
when: not cloud_image_stat.stat.exists
# ── Disk image ─────────────────────────────────────────────────────────────
- name: Check if VM disk already exists
ansible.builtin.stat:
path: "{{ vm_disk_path }}"
register: vm_disk_stat
- name: Create VM disk from cloud image
ansible.builtin.command:
cmd: >
qemu-img create -f qcow2 -F qcow2
-b {{ vm_cloud_image_cache }}
{{ vm_disk_path }} {{ vm_disk_size }}
creates: "{{ vm_disk_path }}"
when: not vm_disk_stat.stat.exists
# ── Cloud-init seed ISO ────────────────────────────────────────────────────
- name: Create cloud-init temp directory
ansible.builtin.tempfile:
state: directory
suffix: cloud-init
register: cloud_init_dir
- name: Write cloud-init user-data
ansible.builtin.template:
src: cloud-init-user-data.j2
dest: "{{ cloud_init_dir.path }}/user-data"
mode: "0644"
vars:
vm_ssh_keys: "{{ openclaw_ssh_keys | default([]) }}"
- name: Write cloud-init meta-data
ansible.builtin.template:
src: cloud-init-meta-data.j2
dest: "{{ cloud_init_dir.path }}/meta-data"
mode: "0644"
- name: Set seed ISO path fact
ansible.builtin.set_fact:
vm_seed_iso: "/var/lib/libvirt/images/{{ vm_hostname }}-seed.iso"
- name: Create cloud-init seed ISO
ansible.builtin.command:
cmd: >
genisoimage -output {{ vm_seed_iso }}
-volid cidata -joliet -rock
{{ cloud_init_dir.path }}/user-data
{{ cloud_init_dir.path }}/meta-data
changed_when: true
- name: Clean up cloud-init temp directory
ansible.builtin.file:
path: "{{ cloud_init_dir.path }}"
state: absent
# ── VM definition ──────────────────────────────────────────────────────────
- name: Check if VM domain already exists
community.libvirt.virt:
command: list_vms
uri: "{{ vm_libvirt_uri }}"
register: existing_vms
- name: Define VM from XML template
community.libvirt.virt:
command: define
xml: "{{ lookup('template', 'domain.xml.j2') }}"
uri: "{{ vm_libvirt_uri }}"
when: vm_domain not in existing_vms.list_vms
# ── Network ────────────────────────────────────────────────────────────────
- name: Add static DHCP reservation
ansible.builtin.command:
cmd: >
virsh -c {{ vm_libvirt_uri }} net-update {{ vm_network }}
add ip-dhcp-host
'<host mac="{{ vm_mac }}" name="{{ vm_hostname }}" ip="{{ vm_ip }}"/>'
--live --config
register: dhcp_result
failed_when:
- dhcp_result.rc != 0
- "'already exists' not in dhcp_result.stderr"
changed_when: dhcp_result.rc == 0
# ── Autostart & boot ───────────────────────────────────────────────────────
- name: Enable autostart
community.libvirt.virt:
name: "{{ vm_domain }}"
autostart: true
uri: "{{ vm_libvirt_uri }}"
- name: Start VM
community.libvirt.virt:
name: "{{ vm_domain }}"
state: running
uri: "{{ vm_libvirt_uri }}"
- name: Wait for SSH to become available
ansible.builtin.wait_for:
host: "{{ vm_ip }}"
port: 22
delay: 10
timeout: 180
state: started
delegate_to: localhost
- name: VM is ready
ansible.builtin.debug:
msg: "VM '{{ vm_domain }}' is up at {{ vm_ip }}. Run install.yml + customize.yml to provision the guest."
@@ -0,0 +1,2 @@
instance-id: {{ vm_hostname }}-{{ vm_mac | replace(':', '') }}
local-hostname: {{ vm_hostname }}
@@ -0,0 +1,28 @@
#cloud-config
hostname: {{ vm_hostname }}
manage_etc_hosts: true
# Enable root SSH with key from host
disable_root: false
ssh_pwauth: false
users:
- name: root
ssh_authorized_keys:
{% for key in vm_ssh_keys | default([]) %}
- {{ key }}
{% endfor %}
# Grow root partition to fill disk
growpart:
mode: auto
devices: [/]
resize_rootfs: true
# Ensure SSH is running
packages:
- qemu-guest-agent
runcmd:
- systemctl enable --now qemu-guest-agent
+123
View File
@@ -0,0 +1,123 @@
<domain type='kvm'>
<name>{{ vm_domain }}</name>
<metadata>
<libosinfo:libosinfo xmlns:libosinfo="http://libosinfo.org/xmlns/libvirt/domain/1.0">
<libosinfo:os id="http://ubuntu.com/ubuntu/24.04"/>
</libosinfo:libosinfo>
</metadata>
<memory unit='KiB'>{{ vm_memory_mib * 1024 }}</memory>
<currentMemory unit='KiB'>{{ vm_memory_mib * 1024 }}</currentMemory>
<memoryBacking>
<source type='memfd'/>
<access mode='shared'/>
</memoryBacking>
<vcpu placement='static'>{{ vm_vcpus }}</vcpu>
<os firmware='efi'>
<type arch='x86_64' machine='pc-q35-10.2'>hvm</type>
<firmware>
<feature enabled='no' name='enrolled-keys'/>
<feature enabled='yes' name='secure-boot'/>
</firmware>
<loader readonly='yes' secure='yes' type='pflash' format='raw'>{{ vm_ovmf_code }}</loader>
<nvram template='{{ vm_ovmf_vars_template }}' templateFormat='raw' format='raw'>{{ vm_ovmf_vars_dir }}/{{ vm_domain }}_VARS.fd</nvram>
<boot dev='hd'/>
</os>
<features>
<acpi/>
<apic/>
<vmport state='off'/>
<smm state='on'/>
</features>
<cpu mode='host-passthrough' check='none' migratable='on'/>
<clock offset='utc'>
<timer name='rtc' tickpolicy='catchup'/>
<timer name='pit' tickpolicy='delay'/>
<timer name='hpet' present='no'/>
</clock>
<on_poweroff>destroy</on_poweroff>
<on_reboot>restart</on_reboot>
<on_crash>destroy</on_crash>
<pm>
<suspend-to-mem enabled='no'/>
<suspend-to-disk enabled='no'/>
</pm>
<devices>
<emulator>/usr/bin/qemu-system-x86_64</emulator>
<!-- Primary disk -->
<disk type='file' device='disk'>
<driver name='qemu' type='qcow2' discard='unmap'/>
<source file='{{ vm_disk_path }}'/>
<target dev='vda' bus='virtio'/>
</disk>
<!-- Cloud-init seed (removed after first boot) -->
<disk type='file' device='cdrom'>
<driver name='qemu' type='raw'/>
<source file='{{ vm_seed_iso }}'/>
<target dev='sda' bus='sata'/>
<readonly/>
</disk>
<!-- virtio-serial for qemu-guest-agent -->
<controller type='virtio-serial' index='0'/>
<!-- Network -->
<interface type='network'>
<mac address='{{ vm_mac }}'/>
<source network='{{ vm_network }}'/>
<model type='virtio'/>
</interface>
<!-- Serial console -->
<serial type='pty'>
<target type='isa-serial' port='0'>
<model name='isa-serial'/>
</target>
</serial>
<console type='pty'>
<target type='serial' port='0'/>
</console>
<!-- qemu-guest-agent channel -->
<channel type='unix'>
<target type='virtio' name='org.qemu.guest_agent.0'/>
</channel>
{% if vm_virtiofs_source and vm_virtiofs_tag %}
<!-- virtiofs host share -->
<filesystem type='mount' accessmode='passthrough'>
<driver type='virtiofs'/>
<source dir='{{ vm_virtiofs_source }}'/>
<target dir='{{ vm_virtiofs_tag }}'/>
</filesystem>
{% endif %}
<!-- TPM 2.0 -->
<tpm model='tpm-crb'>
<backend type='emulator' version='2.0'/>
</tpm>
<!-- Watchdog -->
<watchdog model='itco' action='reset'/>
<!-- Memory balloon -->
<memballoon model='virtio'>
<stats period='5'/>
</memballoon>
<!-- RNG -->
<rng model='virtio'>
<backend model='random'>/dev/urandom</backend>
</rng>
<!-- SPICE (for virt-manager) -->
<graphics type='spice' autoport='yes' listen='127.0.0.1'>
<listen type='address' address='127.0.0.1'/>
<image compression='off'/>
</graphics>
<video>
<model type='virtio' heads='1' primary='yes'/>
</video>
</devices>
</domain>
+95
View File
@@ -0,0 +1,95 @@
#!/bin/bash
set -e
# Run OpenClaw playbook from local source or installed collection
OPENCLAW_USER="${OPENCLAW_USER:-openclaw}"
# Keep instructions aligned when user overrides openclaw_user via -e.
extract_openclaw_user_from_args() {
local prev_is_extra=0
local arg
for arg in "$@"; do
if [ "$prev_is_extra" -eq 1 ]; then
if [[ "$arg" =~ (^|[[:space:]])openclaw_user=([^[:space:]]+) ]]; then
OPENCLAW_USER="${BASH_REMATCH[2]}"
fi
prev_is_extra=0
continue
fi
case "$arg" in
-e|--extra-vars)
prev_is_extra=1
;;
-e=*|--extra-vars=*)
local extra="${arg#*=}"
if [[ "$extra" =~ (^|[[:space:]])openclaw_user=([^[:space:]]+) ]]; then
OPENCLAW_USER="${BASH_REMATCH[2]}"
fi
;;
esac
done
}
extract_openclaw_user_from_args "$@"
# Determine playbook source
if [ -f "playbooks/install.yml" ]; then
echo "Running from local source..."
PLAYBOOK="playbook.yml"
export ANSIBLE_ROLES_PATH="${PWD}/roles:${ANSIBLE_ROLES_PATH}"
elif ansible-galaxy collection list 2>/dev/null | grep -q "openclaw.installer"; then
echo "Running from installed collection..."
PLAYBOOK="openclaw.installer.install"
else
echo "Error: Collection not installed and not running from source"
echo "Install with: ansible-galaxy collection install -r requirements.yml"
exit 1
fi
# Run the Ansible playbook
if [ "$EUID" -eq 0 ]; then
ansible-playbook "$PLAYBOOK" -e ansible_become=false "$@"
PLAYBOOK_EXIT=$?
else
if sudo -n true 2>/dev/null; then
echo "Passwordless sudo detected. Running without become password prompt."
ansible-playbook "$PLAYBOOK" "$@"
PLAYBOOK_EXIT=$?
else
echo "Sudo password required. Prompting for become password."
ansible-playbook "$PLAYBOOK" --ask-become-pass "$@"
PLAYBOOK_EXIT=$?
fi
fi
# After playbook completes successfully, show instructions
if [ $PLAYBOOK_EXIT -eq 0 ]; then
echo ""
echo "═══════════════════════════════════════════════════════════"
echo "✅ INSTALLATION COMPLETE!"
echo "═══════════════════════════════════════════════════════════"
echo ""
echo "🔄 SWITCH TO OPENCLAW USER with:"
echo ""
echo " sudo su - ${OPENCLAW_USER}"
echo ""
echo " OR (alternative):"
echo ""
echo " sudo -u ${OPENCLAW_USER} -i"
echo ""
echo "This will switch you to the OpenClaw user with a proper"
echo "login shell (loads .bashrc, sets environment correctly)."
echo ""
echo "After switching, you'll see the next setup steps:"
echo " • Configure OpenClaw (~/.openclaw/config.yml)"
echo " • Login to messaging provider (WhatsApp/Telegram/Signal)"
echo " • Test the gateway"
echo ""
echo "═══════════════════════════════════════════════════════════"
echo ""
else
echo "❌ Playbook failed with exit code $PLAYBOOK_EXIT"
exit $PLAYBOOK_EXIT
fi
+29
View File
@@ -0,0 +1,29 @@
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
# Install Ansible and dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ansible \
python3 \
python3-apt \
sudo \
systemd \
git \
curl \
ca-certificates \
acl \
gpg \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Copy project into container
COPY . /opt/ansible
WORKDIR /opt/ansible
# Install Ansible Galaxy collections
RUN ansible-galaxy collection install -r requirements.yml
# Default: run the test entrypoint
ENTRYPOINT ["bash", "tests/entrypoint.sh"]
+68
View File
@@ -0,0 +1,68 @@
# Docker CI Test Harness
This directory contains a Docker-based CI test harness for the Ansible playbook. It validates convergence, correctness, and idempotency by running the playbook inside an Ubuntu 24.04 container.
## Quick Start
```bash
# Run all tests
bash tests/run-tests.sh
# Or specify a distro (currently only ubuntu2404 available)
bash tests/run-tests.sh ubuntu2404
```
## Test Structure
The test harness runs three sequential tests:
1. **Convergence**: Runs the playbook with `ci_test=true` to verify it completes without errors
2. **Verification**: Runs `verify.yml` to assert the system is in the expected state
3. **Idempotency**: Runs the playbook a second time and verifies `changed=0`
## Files
- `Dockerfile.ubuntu2404` - Ubuntu 24.04 container with Ansible pre-installed
- `entrypoint.sh` - Test execution script (convergence → verification → idempotency)
- `verify.yml` - Post-convergence assertions (user exists, packages installed, directories created, etc.)
- `run-tests.sh` - Local test runner script
## CI Test Mode
The `ci_test` variable skips tasks that require:
- Docker-in-Docker (Docker CE installation)
- Kernel access (UFW/iptables firewall)
- systemd services (loginctl, daemon installation)
- External package installation (openclaw app install)
Everything else runs normally: package installation, user creation, Node.js/pnpm setup, directory structure, config file rendering, etc.
## What Gets Tested
| Component | Tested? | Notes |
|-----------|---------|-------|
| System packages (35+) | ✅ Yes | Full apt install |
| User creation + config | ✅ Yes | User, .bashrc, sudoers, SSH dir |
| Node.js + pnpm | ✅ Yes | Full install + version check |
| Directory structure | ✅ Yes | All .openclaw/* dirs with perms |
| Git global config | ✅ Yes | Aliases, default branch |
| Vim config | ✅ Yes | Template rendering |
| Docker CE install | ❌ No | Needs Docker-in-Docker |
| UFW / iptables | ❌ No | Needs kernel access |
| fail2ban / systemd | ❌ No | Needs running systemd |
| Tailscale | ❌ No | Disabled by default already |
| OpenClaw app install | ❌ No | External package |
| Idempotency | ✅ Yes | Second run must have 0 changes |
## Exit Codes
- `0` - All tests passed
- `1` - Test failure (convergence failed, verification failed, or idempotency check failed)
## Development
To add tests for additional distributions:
1. Create `Dockerfile.<distro>` (e.g., `Dockerfile.debian12`)
2. Run: `bash tests/run-tests.sh <distro>`
The test harness automatically builds the image and runs the test suite.
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
PLAYBOOK_ARGS=(-e ci_test=true -e ansible_become=false --connection=local)
# --- Step 1: Convergence ---
echo "===> Step 1: Convergence test"
ansible-playbook playbook.yml "${PLAYBOOK_ARGS[@]}"
echo "===> Convergence: PASSED"
# --- Step 2: Verification ---
echo "===> Step 2: Verification"
ansible-playbook tests/verify.yml "${PLAYBOOK_ARGS[@]}"
echo "===> Verification: PASSED"
# --- Step 3: Idempotency ---
echo "===> Step 3: Idempotency test"
IDEMPOTENCY_OUT=$(ansible-playbook playbook.yml "${PLAYBOOK_ARGS[@]}" 2>&1)
echo "$IDEMPOTENCY_OUT"
CHANGED=$(echo "$IDEMPOTENCY_OUT" | tail -n 5 | grep -oP 'changed=\K[0-9]+' | head -1)
if [ "${CHANGED:-1}" -eq 0 ]; then
echo "===> Idempotency: PASSED (0 changed)"
else
echo "===> Idempotency: FAILED (changed=$CHANGED)"
exit 1
fi
echo ""
echo "===> All tests passed"
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
DISTRO="${1:-ubuntu2404}"
IMAGE="openclaw-ansible-test:${DISTRO}"
echo "Building test image (${DISTRO})..."
docker build -t "$IMAGE" -f "tests/Dockerfile.${DISTRO}" .
echo "Running tests..."
docker run --rm "$IMAGE"
+76
View File
@@ -0,0 +1,76 @@
---
- name: Verify playbook results
hosts: localhost
connection: local
gather_facts: true
vars:
openclaw_user: openclaw
openclaw_home: /home/openclaw
tasks:
- name: Verify openclaw user exists
ansible.builtin.command: "id {{ openclaw_user }}"
changed_when: false
- name: Verify critical packages installed
ansible.builtin.command: "dpkg -s {{ item }}"
loop: [git, curl, vim, jq, tmux, tree, htop]
changed_when: false
- name: Verify Node.js installed
ansible.builtin.command: node --version
changed_when: false
- name: Verify pnpm installed
ansible.builtin.command: pnpm --version
changed_when: false
- name: Verify openclaw directory structure
ansible.builtin.stat:
path: "{{ item.path }}"
loop:
- { path: "{{ openclaw_home }}/.openclaw", mode: "0755" }
- { path: "{{ openclaw_home }}/.openclaw/sessions" }
- { path: "{{ openclaw_home }}/.openclaw/credentials", mode: "0700" }
- { path: "{{ openclaw_home }}/.openclaw/data" }
- { path: "{{ openclaw_home }}/.openclaw/logs" }
- { path: "{{ openclaw_home }}/.openclaw/agents" }
- { path: "{{ openclaw_home }}/.openclaw/agents/main" }
- { path: "{{ openclaw_home }}/.openclaw/agents/main/agent", mode: "0700" }
- { path: "{{ openclaw_home }}/.openclaw/workspace" }
- { path: "{{ openclaw_home }}/.ssh", mode: "0700" }
register: dir_checks
- name: Assert directories exist
ansible.builtin.assert:
that: item.stat.exists and item.stat.isdir
fail_msg: "Directory missing: {{ item.item.path }}"
loop: "{{ dir_checks.results }}"
loop_control:
label: "{{ item.item.path }}"
- name: Assert restricted directories have correct permissions
ansible.builtin.assert:
that:
- dir_checks.results[2].stat.mode == '0700'
- dir_checks.results[7].stat.mode == '0700'
fail_msg: "credentials and agents/main/agent dirs should be 0700"
- name: Verify sudoers file exists and is valid
ansible.builtin.command: "visudo -cf /etc/sudoers.d/{{ openclaw_user }}"
changed_when: false
- name: Verify global vim config exists
ansible.builtin.stat:
path: /etc/vim/vimrc.local
register: vimrc
- ansible.builtin.assert:
that: vimrc.stat.exists
- name: Verify git global config
ansible.builtin.command: git config --global init.defaultBranch
changed_when: false
register: git_branch
- ansible.builtin.assert:
that: git_branch.stdout == 'main'
+84
View File
@@ -0,0 +1,84 @@
#!/bin/bash
# Sync OpenClaw VM config to ~/lab/swarm and back up to MinIO
#
# Local: rsync ~/.openclaw/ → ~/lab/swarm/openclaw/ (live mirror, no archive)
# MinIO: timestamped archive → s3://zap/backups/ (point-in-time recovery)
set -euo pipefail
INSTANCE="${1:-zap}"
REGISTRY="${HOME}/.claude/state/openclaw-instances.json"
SYNC_DIR="${HOME}/lab/swarm/openclaw"
MINIO_BUCKET="zap"
MINIO_PREFIX="backups"
KEEP=7
# Resolve instance from registry
GUEST_HOST=$(python3 -c "
import json, sys
data = json.load(open('${REGISTRY}'))
inst = next((i for i in data['instances'] if i['name'] == '${INSTANCE}'), None)
if not inst:
print('ERROR: unknown instance', file=sys.stderr); sys.exit(1)
if not inst.get('host'):
print('ERROR: instance has no host', file=sys.stderr); sys.exit(1)
print(inst['host'])
")
GUEST_USER=$(python3 -c "
import json
data = json.load(open('${REGISTRY}'))
inst = next(i for i in data['instances'] if i['name'] == '${INSTANCE}')
print(inst['user'])
")
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Syncing ${INSTANCE} (${GUEST_HOST})..."
# Check VM is reachable
if ! ssh -o ConnectTimeout=5 -o BatchMode=yes "root@${GUEST_HOST}" "echo ok" &>/dev/null; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Cannot reach ${GUEST_HOST}" >&2
exit 1
fi
mkdir -p "${SYNC_DIR}"
# Rsync ~/.openclaw/ → ~/lab/swarm/openclaw/
# Excludes large/ephemeral data not needed for redeployment
rsync -az --delete \
--exclude='workspace/' \
--exclude='extensions-quarantine/' \
--exclude='logs/' \
--exclude='*.bak' \
--exclude='*.bak.*' \
--exclude='*.bak-*' \
--exclude='*.backup-*' \
--exclude='*.pre-*' \
--exclude='*.failed' \
-e "ssh -o BatchMode=yes" \
"root@${GUEST_HOST}:/home/${GUEST_USER}/.openclaw/" \
"${SYNC_DIR}/"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Synced to ${SYNC_DIR}"
# Create archive from synced files and upload to MinIO
ARCHIVE=$(mktemp --suffix=".tar.gz")
trap 'rm -f "${ARCHIVE}"' EXIT
tar czf "${ARCHIVE}" -C "${HOME}/lab/swarm" openclaw
MINIO_KEY="${MINIO_PREFIX}/${INSTANCE}-${TIMESTAMP}.tar.gz"
aws s3 cp "${ARCHIVE}" "s3://${MINIO_BUCKET}/${MINIO_KEY}" --quiet
SIZE=$(du -sh "${ARCHIVE}" | cut -f1)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] MinIO: s3://${MINIO_BUCKET}/${MINIO_KEY} (${SIZE})"
# Prune MinIO backups, keep last N
mapfile -t OLD_KEYS < <(aws s3 ls "s3://${MINIO_BUCKET}/${MINIO_PREFIX}/${INSTANCE}-" \
| awk '{print $4}' | sort | head -n -${KEEP})
for key in "${OLD_KEYS[@]:-}"; do
[[ -z "${key}" ]] && continue
aws s3 rm "s3://${MINIO_BUCKET}/${MINIO_PREFIX}/${key}" --quiet
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Pruned MinIO: ${key}"
done
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Done."
+229
View File
@@ -0,0 +1,229 @@
services:
# flynn:
# build: .
# container_name: flynn
# restart: unless-stopped
# ports:
# - "18800:18800"
# volumes:
# # Persistent data (sessions DB, memory store)
# - flynn-data:/data
# # Mount your config file
# - ./config/default.yaml:/config/config.yaml:ro
# environment:
# # Required: at least one model provider API key
# - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
# # Optional: additional provider keys
# - OPENAI_API_KEY=${OPENAI_API_KEY:-}
# - OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-}
# - GOOGLE_API_KEY=${GOOGLE_API_KEY:-}
# # Optional: Telegram integration
# - FLYNN_TELEGRAM_TOKEN=${FLYNN_TELEGRAM_TOKEN:-}
# # Optional: Discord integration
# - DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN:-}
# # Optional: Gateway auth token
# - FLYNN_SERVER_TOKEN=${FLYNN_SERVER_TOKEN:-}
# healthcheck:
# test: ["CMD", "wget", "-qO-", "http://localhost:18800/"]
# interval: 30s
# timeout: 5s
# start_period: 15s
# retries: 3
# Optional local dependency: whisper.cpp server for audio transcription.
# Start with: docker compose --profile voice up -d whisper-server
whisper-server:
image: ghcr.io/ggml-org/whisper.cpp:main
container_name: whisper-server
restart: unless-stopped
profiles: ["voice"]
ports:
- "18801:8080"
volumes:
- whisper-models:/app/models
# Override image entrypoint so args are passed directly to whisper-server.
entrypoint: ["whisper-server"]
command:
- --model
- /app/models/ggml-base.en.bin
- --host
- 0.0.0.0
- --port
- "8080"
- --convert
- --language
- en
- --inference-path
- /v1/audio/transcriptions
healthcheck:
test:
[
"CMD-SHELL",
"curl -f http://localhost:8080/ >/dev/null 2>&1 || exit 1",
]
interval: 30s
timeout: 5s
start_period: 15s
retries: 3
# kokoro TTS
kokoro-tts:
image: ghcr.io/remsky/kokoro-fastapi-cpu:latest
container_name: kokoro-tts
profiles: ["voice"]
ports:
- "18805:8880"
environment:
- USE_GPU=false
# - PYTHONUNBUFFERED=1
#deploy:
# resources:
# reservations:
# devices:
# - driver: nvidia
# count: all
# capabilities: [gpu]
restart: unless-stopped
# Optional local dependency: Brave Search MCP server (HTTP mode).
# Start with: docker compose --profile search up -d brave-search
brave-search:
image: mcp/brave-search:latest
container_name: brave-search
restart: unless-stopped
profiles: ["search"]
ports:
- "18802:8000"
environment:
- BRAVE_API_KEY=${BRAVE_API_KEY:?BRAVE_API_KEY is required}
- BRAVE_MCP_TRANSPORT=http
- BRAVE_MCP_HOST=0.0.0.0
- BRAVE_MCP_PORT=8000
# Optional local dependency: SearXNG metasearch instance.
# Start with: docker compose --profile search up -d searxng
searxng:
image: searxng/searxng:latest
container_name: searxng
restart: unless-stopped
profiles: ["search"]
ports:
- "18803:8080"
environment:
- BASE_URL=http://localhost:18803/
- INSTANCE_NAME=Flynn Local SearXNG
volumes:
- ./searxng/settings.yml:/etc/searxng/settings.yml:ro
# Optional local dependency: liteLLM proxy for unified LLM API.
# Start with: docker compose --profile api up -d litellm
litellm:
image: litellm/litellm:latest
container_name: litellm
restart: unless-stopped
profiles: ["api"]
ports:
- "18804:4000"
volumes:
- ./litellm-config.yaml:/app/config.yaml:ro
- ./litellm-copilot-tokens:/root/.config/litellm/github_copilot
environment:
- LITELLM_PORT=4000
- LITELLM_DROP_PARAMS=true
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-}
- GEMINI_API_KEY=${GEMINI_API_KEY:-}
- ZAI_API_KEY=${ZAI_API_KEY:-}
- GITHUB_COPILOT_TOKEN_DIR=/root/.config/litellm/github_copilot
- DATABASE_URL=postgresql://litellm:litellm_password@litellm-db:5432/litellm
- LITELLM_MASTER_KEY=${LITELLM_MASTER_KEY:-sk-1234}
- LITELLM_SALT_KEY=${LITELLM_SALT_KEY:-}
- STORE_MODEL_IN_DB=True
command:
[
"--config",
"/app/config.yaml",
"--port",
"4000",
]
depends_on:
litellm-db:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "python3 -c \"import urllib.request; urllib.request.urlopen('http://localhost:4000/health/liveliness')\""]
interval: 30s
timeout: 5s
start_period: 15s
retries: 3
litellm-init:
image: curlimages/curl:latest
container_name: litellm-init
profiles: ["api"]
restart: "no"
volumes:
- ./litellm-init-credentials.sh:/init.sh:ro
- ./litellm-init-models.sh:/litellm-init-models.sh:ro
environment:
- LITELLM_URL=http://litellm:4000
- LITELLM_MASTER_KEY=${LITELLM_MASTER_KEY:-sk-1234}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
- GEMINI_API_KEY=${GEMINI_API_KEY:-}
- ZAI_API_KEY=${ZAI_API_KEY:-}
entrypoint: ["sh", "/init.sh"]
depends_on:
litellm:
condition: service_healthy
litellm-db:
image: postgres:15-alpine
container_name: litellm-db
restart: unless-stopped
profiles: ["api"]
volumes:
- litellm-db-data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=litellm
- POSTGRES_PASSWORD=litellm_password
- POSTGRES_DB=litellm
healthcheck:
test: ["CMD-SHELL", "pg_isready -U litellm"]
interval: 10s
timeout: 5s
start_period: 5s
retries: 5
# Dedicated local n8n instance for agent-oriented workflows.
# Start with: docker compose --profile automation up -d n8n-agent
n8n-agent:
image: docker.n8n.io/n8nio/n8n:latest
container_name: n8n-agent
restart: unless-stopped
profiles: ["automation"]
ports:
- "18808:5678"
environment:
- N8N_HOST=0.0.0.0
- N8N_PORT=5678
- N8N_PROTOCOL=http
- N8N_EDITOR_BASE_URL=http://localhost:18808
- WEBHOOK_URL=http://localhost:18808/
- TZ=UTC
- GENERIC_TIMEZONE=UTC
- N8N_SECURE_COOKIE=false
volumes:
- n8n-agent-data:/home/node/.n8n
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:5678/healthz >/dev/null 2>&1 || exit 1"]
interval: 30s
timeout: 5s
start_period: 30s
retries: 5
volumes:
# flynn-data:
whisper-models:
litellm-db-data:
n8n-agent-data:
+3
View File
@@ -0,0 +1,3 @@
litellm_settings:
drop_params: true
set_verbose: false
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Registers API credentials into LiteLLM's database so they appear in the web UI.
# Run once after the litellm service is healthy.
# Usage: ./litellm-init-credentials.sh
# Reads credentials from environment (or .env if sourced).
set -euo pipefail
LITELLM_URL="${LITELLM_URL:-http://localhost:18804}"
LITELLM_MASTER_KEY="${LITELLM_MASTER_KEY:?LITELLM_MASTER_KEY is required}"
post_credential() {
local name="$1"
local provider="$2"
local api_key="$3"
if [ -z "$api_key" ]; then
echo " [skip] $name — key is empty"
return
fi
local body="{\"credential_name\":\"$name\",\"credential_values\":{\"api_key\":\"$api_key\"},\"credential_info\":{\"custom_llm_provider\":\"$provider\"}}"
local status
status=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "$LITELLM_URL/credentials" \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d "$body")
if [ "$status" = "200" ] || [ "$status" = "201" ]; then
echo " [ok] $name ($provider)"
else
echo " [fail] $name ($provider) — HTTP $status"
fi
}
echo "Waiting for LiteLLM to be ready..."
until curl -sf "$LITELLM_URL/health/liveliness" > /dev/null 2>&1; do sleep 2; done
echo "LiteLLM is up. Registering credentials..."
post_credential "anthropic" "anthropic" "${ANTHROPIC_API_KEY:-}"
post_credential "openai" "openai" "${OPENAI_API_KEY:-}"
post_credential "gemini" "gemini" "${GEMINI_API_KEY:-}"
post_credential "zai" "openai" "${ZAI_API_KEY:-}"
echo "Registering models..."
sh /litellm-init-models.sh
echo "Done."
+162
View File
@@ -0,0 +1,162 @@
#!/bin/sh
# Registers models into LiteLLM's database so they are UI-manageable.
# Idempotent: skips models that are already in the DB.
LITELLM_URL="${LITELLM_URL:-http://localhost:18804}"
LITELLM_MASTER_KEY="${LITELLM_MASTER_KEY:?LITELLM_MASTER_KEY is required}"
# Fetch existing DB model names once
EXISTING=$(curl -s "$LITELLM_URL/v2/model/info" \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" | \
python3 -c "
import sys, json
data = json.load(sys.stdin)
for m in data.get('data', []):
if m.get('model_info', {}).get('db_model'):
print(m['model_name'])
" 2>/dev/null)
add_model() {
local model_name="$1"
local litellm_model="$2"
local api_key_env="$3"
local api_base="${4:-}"
if echo "$EXISTING" | grep -qx "$model_name"; then
echo " [skip] $model_name"
return
fi
if [ -n "$api_base" ]; then
base_json=",\"api_base\":\"$api_base\""
else
base_json=""
fi
local body="{\"model_name\":\"$model_name\",\"litellm_params\":{\"model\":\"$litellm_model\",\"api_key\":\"os.environ/$api_key_env\"$base_json}}"
local status
status=$(curl -s -o /tmp/model_resp.json -w "%{http_code}" \
-X POST "$LITELLM_URL/model/new" \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d "$body")
if [ "$status" = "200" ] || [ "$status" = "201" ]; then
echo " [ok] $model_name"
else
echo " [fail] $model_name — HTTP $status: $(cat /tmp/model_resp.json)"
fi
}
# GitHub Copilot models use token-file auth, no api_key needed.
add_copilot_model() {
local model_name="$1"
local copilot_model="$2"
if echo "$EXISTING" | grep -qx "$model_name"; then
echo " [skip] $model_name"
return
fi
local body="{\"model_name\":\"$model_name\",\"litellm_params\":{\"model\":\"github_copilot/$copilot_model\",\"extra_headers\":{\"editor-version\":\"vscode/1.85.1\",\"editor-plugin-version\":\"copilot/1.155.0\",\"Copilot-Integration-Id\":\"vscode-chat\",\"user-agent\":\"GithubCopilot/1.155.0\"}}}"
local status
status=$(curl -s -o /tmp/model_resp.json -w "%{http_code}" \
-X POST "$LITELLM_URL/model/new" \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d "$body")
if [ "$status" = "200" ] || [ "$status" = "201" ]; then
echo " [ok] $model_name"
else
echo " [fail] $model_name — HTTP $status: $(cat /tmp/model_resp.json)"
fi
}
echo "Registering models in LiteLLM DB..."
# OpenAI — GPT series
add_model "gpt-4o" "openai/gpt-4o" "OPENAI_API_KEY"
add_model "gpt-4o-mini" "openai/gpt-4o-mini" "OPENAI_API_KEY"
add_model "gpt-4.1" "openai/gpt-4.1" "OPENAI_API_KEY"
add_model "gpt-4.1-mini" "openai/gpt-4.1-mini" "OPENAI_API_KEY"
add_model "gpt-4.1-nano" "openai/gpt-4.1-nano" "OPENAI_API_KEY"
add_model "gpt-5" "openai/gpt-5" "OPENAI_API_KEY"
add_model "gpt-5-mini" "openai/gpt-5-mini" "OPENAI_API_KEY"
add_model "gpt-5-nano" "openai/gpt-5-nano" "OPENAI_API_KEY"
add_model "gpt-5-pro" "openai/gpt-5-pro" "OPENAI_API_KEY"
add_model "gpt-5.1" "openai/gpt-5.1" "OPENAI_API_KEY"
add_model "gpt-5.2" "openai/gpt-5.2" "OPENAI_API_KEY"
add_model "gpt-5.2-pro" "openai/gpt-5.2-pro" "OPENAI_API_KEY"
# OpenAI — o-series reasoning
add_model "o1" "openai/o1" "OPENAI_API_KEY"
add_model "o1-pro" "openai/o1-pro" "OPENAI_API_KEY"
add_model "o3" "openai/o3" "OPENAI_API_KEY"
add_model "o3-mini" "openai/o3-mini" "OPENAI_API_KEY"
add_model "o4-mini" "openai/o4-mini" "OPENAI_API_KEY"
# OpenAI — Codex series
add_model "gpt-5-codex" "openai/gpt-5-codex" "OPENAI_API_KEY"
add_model "gpt-5.1-codex" "openai/gpt-5.1-codex" "OPENAI_API_KEY"
add_model "gpt-5.1-codex-max" "openai/gpt-5.1-codex-max" "OPENAI_API_KEY"
add_model "gpt-5.1-codex-mini" "openai/gpt-5.1-codex-mini" "OPENAI_API_KEY"
add_model "gpt-5.2-codex" "openai/gpt-5.2-codex" "OPENAI_API_KEY"
add_model "gpt-5.3-codex" "openai/gpt-5.3-codex" "OPENAI_API_KEY"
# Anthropic
add_model "claude-opus-4-6" "anthropic/claude-opus-4-6" "ANTHROPIC_API_KEY"
add_model "claude-sonnet-4-6" "anthropic/claude-sonnet-4-6" "ANTHROPIC_API_KEY"
add_model "claude-opus-4-5" "anthropic/claude-opus-4-5-20251101" "ANTHROPIC_API_KEY"
add_model "claude-opus-4-1" "anthropic/claude-opus-4-1-20250805" "ANTHROPIC_API_KEY"
add_model "claude-sonnet-4-5" "anthropic/claude-sonnet-4-5-20250929" "ANTHROPIC_API_KEY"
add_model "claude-opus-4" "anthropic/claude-opus-4-20250514" "ANTHROPIC_API_KEY"
add_model "claude-sonnet-4" "anthropic/claude-sonnet-4-20250514" "ANTHROPIC_API_KEY"
add_model "claude-haiku-4-5" "anthropic/claude-haiku-4-5-20251001" "ANTHROPIC_API_KEY"
add_model "claude-3-haiku" "anthropic/claude-3-haiku-20240307" "ANTHROPIC_API_KEY"
# Google Gemini
add_model "gemini-2.0-flash" "gemini/gemini-2.0-flash" "GEMINI_API_KEY"
add_model "gemini-2.0-flash-lite" "gemini/gemini-2.0-flash-lite" "GEMINI_API_KEY"
add_model "gemini-2.5-flash-lite" "gemini/gemini-2.5-flash-lite" "GEMINI_API_KEY"
add_model "gemini-2.5-flash" "gemini/gemini-2.5-flash" "GEMINI_API_KEY"
add_model "gemini-2.5-pro" "gemini/gemini-2.5-pro" "GEMINI_API_KEY"
add_model "gemini-3-flash-preview" "gemini/gemini-3-flash-preview" "GEMINI_API_KEY"
add_model "gemini-3-pro-preview" "gemini/gemini-3-pro-preview" "GEMINI_API_KEY"
add_model "gemini-3.1-pro-preview" "gemini/gemini-3.1-pro-preview" "GEMINI_API_KEY"
add_model "gemini-flash-latest" "gemini/gemini-flash-latest" "GEMINI_API_KEY"
add_model "gemini-flash-lite-latest" "gemini/gemini-flash-lite-latest" "GEMINI_API_KEY"
add_model "gemini-pro-latest" "gemini/gemini-pro-latest" "GEMINI_API_KEY"
# ZAI / GLM
add_model "zai-glm-4.5" "openai/glm-4.5" "ZAI_API_KEY" "https://api.z.ai/api/coding/paas/v4"
add_model "zai-glm-4.5-air" "openai/glm-4.5-air" "ZAI_API_KEY" "https://api.z.ai/api/coding/paas/v4"
add_model "zai-glm-4.6" "openai/glm-4.6" "ZAI_API_KEY" "https://api.z.ai/api/coding/paas/v4"
add_model "zai-glm-4.7" "openai/glm-4.7" "ZAI_API_KEY" "https://api.z.ai/api/coding/paas/v4"
add_model "zai-glm-5" "openai/glm-5" "ZAI_API_KEY" "https://api.z.ai/api/coding/paas/v4"
# GitHub Copilot (token-file auth, no API key)
add_copilot_model "copilot-gpt-4o" "gpt-4o"
add_copilot_model "copilot-gpt-4.1" "gpt-4.1"
add_copilot_model "copilot-gpt-5-mini" "gpt-5-mini"
add_copilot_model "copilot-gpt-5.1" "gpt-5.1"
add_copilot_model "copilot-gpt-5.2" "gpt-5.2"
add_copilot_model "copilot-gpt-5.1-codex" "gpt-5.1-codex"
add_copilot_model "copilot-gpt-5.1-codex-max" "gpt-5.1-codex-max"
add_copilot_model "copilot-gpt-5.1-codex-mini" "gpt-5.1-codex-mini"
add_copilot_model "copilot-gpt-5.2-codex" "gpt-5.2-codex"
add_copilot_model "copilot-gpt-5.3-codex" "gpt-5.3-codex"
add_copilot_model "copilot-claude-opus-4.6" "claude-opus-4.6"
add_copilot_model "copilot-claude-opus-4.6-fast" "claude-opus-4.6-fast"
add_copilot_model "copilot-claude-sonnet-4.6" "claude-sonnet-4.6"
add_copilot_model "copilot-claude-sonnet-4.5" "claude-sonnet-4.5"
add_copilot_model "copilot-claude-sonnet-4" "claude-sonnet-4"
add_copilot_model "copilot-claude-opus-4.5" "claude-opus-4.5"
add_copilot_model "copilot-claude-haiku-4.5" "claude-haiku-4.5"
add_copilot_model "copilot-gemini-2.5-pro" "gemini-2.5-pro"
add_copilot_model "copilot-gemini-3-flash" "gemini-3-flash-preview"
add_copilot_model "copilot-gemini-3-pro" "gemini-3-pro-preview"
add_copilot_model "copilot-gemini-3.1-pro" "gemini-3.1-pro-preview"
add_copilot_model "copilot-grok-code-fast" "grok-code-fast-1"
echo "Done."
File diff suppressed because it is too large Load Diff
+131
View File
@@ -0,0 +1,131 @@
#!/bin/bash
# Restore OpenClaw VM from backup
# Usage: restore-openclaw-vm.sh [instance] [target-host]
# instance - registry name (default: zap)
# target-host - override host IP for fresh VM deployments
set -euo pipefail
INSTANCE="${1:-zap}"
TARGET_HOST="${2:-}"
REGISTRY="${HOME}/.claude/state/openclaw-instances.json"
ANSIBLE_DIR="${HOME}/lab/swarm/ansible"
# Resolve instance from registry
GUEST_USER=$(python3 -c "
import json
data = json.load(open('${REGISTRY}'))
inst = next((i for i in data['instances'] if i['name'] == '${INSTANCE}'), None)
if not inst: print('openclaw')
else: print(inst['user'])
")
if [[ -z "${TARGET_HOST}" ]]; then
TARGET_HOST=$(python3 -c "
import json, sys
data = json.load(open('${REGISTRY}'))
inst = next((i for i in data['instances'] if i['name'] == '${INSTANCE}'), None)
if not inst or not inst.get('host'):
print('ERROR: no host in registry — pass target-host as second argument', file=sys.stderr)
sys.exit(1)
print(inst['host'])
")
fi
SYNC_DIR="${HOME}/lab/swarm/openclaw"
MINIO_BUCKET="zap"
MINIO_PREFIX="backups"
# Find config source — local sync dir first, fall back to MinIO archive
USE_MINIO=false
if [[ ! -d "${SYNC_DIR}" || -z "$(ls -A "${SYNC_DIR}" 2>/dev/null)" ]]; then
echo "No local sync found in ${SYNC_DIR}, fetching latest archive from MinIO..."
LATEST_KEY=$(aws s3 ls "s3://${MINIO_BUCKET}/${MINIO_PREFIX}/${INSTANCE}-" \
| awk '{print $4}' | sort | tail -1)
if [[ -z "${LATEST_KEY}" ]]; then
echo "ERROR: No config found locally or in s3://${MINIO_BUCKET}/${MINIO_PREFIX}/" >&2
exit 1
fi
MINIO_ARCHIVE=$(mktemp --suffix=".tar.gz")
trap 'rm -f "${MINIO_ARCHIVE}"' EXIT
aws s3 cp "s3://${MINIO_BUCKET}/${MINIO_PREFIX}/${LATEST_KEY}" "${MINIO_ARCHIVE}"
echo "Downloaded: ${LATEST_KEY}"
USE_MINIO=true
fi
SOURCE_DESC="${SYNC_DIR}"
[[ "${USE_MINIO}" == true ]] && SOURCE_DESC="MinIO: ${LATEST_KEY}"
echo "========================================"
echo " OpenClaw VM Restore"
echo "========================================"
echo " Instance : ${INSTANCE}"
echo " Target : ${TARGET_HOST}"
echo " Source : ${SOURCE_DESC}"
echo "========================================"
echo ""
read -rp "Proceed? [y/N] " confirm
[[ "${confirm}" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; }
cd "${ANSIBLE_DIR}"
# Step 1: Provision VM if a target host override was given (implies fresh VM needed)
if [[ -n "${2:-}" ]]; then
echo ""
echo "==> [1/4] Provisioning VM (libvirt)..."
ansible-playbook -i inventory.yml playbooks/provision-vm.yml --limit "${INSTANCE}" \
-e "vm_ip=${TARGET_HOST}" -e "ansible_host=${TARGET_HOST}"
else
echo ""
echo "==> [1/4] Skipping VM provisioning (using existing VM at ${TARGET_HOST})"
fi
echo ""
echo "==> [2/4] Provisioning guest OS..."
ansible-playbook -i inventory.yml playbooks/install.yml --limit "${INSTANCE}" \
-e "ansible_host=${TARGET_HOST}"
echo ""
echo "==> [3/4] Applying customizations..."
ansible-playbook -i inventory.yml playbooks/customize.yml --limit "${INSTANCE}" \
-e "ansible_host=${TARGET_HOST}"
# Step 3: Restore config
echo ""
echo "==> [4/5] Restoring OpenClaw config..."
# Stop service before restoring
ssh -o StrictHostKeyChecking=no "root@${TARGET_HOST}" \
"su - ${GUEST_USER} -c 'systemctl --user stop openclaw-gateway.service 2>/dev/null || true'"
# Push config and restore
if [[ "${USE_MINIO}" == true ]]; then
cat "${MINIO_ARCHIVE}" | ssh -o StrictHostKeyChecking=no "root@${TARGET_HOST}" \
"cd /home/${GUEST_USER} && tar xzf - && chown -R ${GUEST_USER}:${GUEST_USER} openclaw"
else
rsync -az \
-e "ssh -o StrictHostKeyChecking=no -o BatchMode=yes" \
"${SYNC_DIR}/" \
"root@${TARGET_HOST}:/home/${GUEST_USER}/.openclaw/"
ssh -o StrictHostKeyChecking=no "root@${TARGET_HOST}" \
"chown -R ${GUEST_USER}:${GUEST_USER} /home/${GUEST_USER}/.openclaw"
fi
echo " Config restored from: ${SOURCE_DESC}"
# Step 4: Start service
echo ""
echo "==> [5/5] Starting service..."
ssh -o StrictHostKeyChecking=no "root@${TARGET_HOST}" \
"su - ${GUEST_USER} -c 'systemctl --user daemon-reload && systemctl --user start openclaw-gateway.service'"
# Verify
sleep 3
STATUS=$(ssh -o StrictHostKeyChecking=no "root@${TARGET_HOST}" \
"su - ${GUEST_USER} -c 'systemctl --user is-active openclaw-gateway.service 2>/dev/null'" || echo "unknown")
echo ""
echo "========================================"
echo " Restore complete"
echo " Service status: ${STATUS}"
echo "========================================"
+10
View File
@@ -0,0 +1,10 @@
use_default_settings: true
server:
secret_key: "02ad40ddb9cbb39ad70e983d17569b253a749493b478a4378575b9688bb176b0"
search:
formats:
- html
- json