Merge pull request #16 from hintjen/remove-macos-support

Remove macos support
This commit is contained in:
AndyML 2026-02-13 16:45:30 -05:00 committed by GitHub
commit badcb65df7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 435 additions and 424 deletions

11
.gitignore vendored
View File

@ -2,3 +2,14 @@
*.log
.ansible/
.vault_pass
# Secrets and credentials
*.env
.env*
secrets.yml
vault.yml
*.pem
*.key
id_rsa*
host_vars/
group_vars/

View File

@ -141,7 +141,7 @@ SSH is exposed to the internet. Fail2ban automatically bans IPs after 5 failed a
Security patches should be applied promptly. Automatic security-only updates reduce vulnerability windows.
### Why Scoped Sudo?
The clawdbot user only needs to manage its own service and Tailscale. Full root access would be dangerous if the app is compromised.
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.

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 Clawdbot Contributors
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

View File

@ -3,19 +3,29 @@
[![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%20%7C%20macOS-orange.svg)](https://www.debian.org/)
[![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, Homebrew, and Tailscale VPN support for Linux and macOS.
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 (Linux) + Application Firewall (macOS) + Docker isolation
- 🔒 **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
- 🍺 **Homebrew**: Package manager for both Linux and macOS
- 🐳 **Docker**: Docker CE (Linux) / Docker Desktop (macOS)
- 🌐 **Multi-OS Support**: Debian, Ubuntu, and macOS
- 🐳 **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`
@ -147,36 +157,18 @@ ansible-playbook playbook.yml --ask-become-pass
## Requirements
### Linux (Debian/Ubuntu)
- Debian 11+ or Ubuntu 20.04+
- Root/sudo access
- Internet connection
### macOS
- macOS 11 (Big Sur) or later
- Homebrew will be installed automatically
- Admin/sudo access
- Internet connection
## What Gets Installed
### Common (All OS)
- Homebrew package manager
- Tailscale (mesh VPN)
- UFW firewall (SSH + Tailscale ports only)
- Docker CE + Compose V2 (for sandboxes)
- Node.js 22.x + pnpm
- OpenClaw via `pnpm install -g openclaw@latest`
- Essential development tools
- Git, zsh, oh-my-zsh
### Linux-Specific
- Docker CE + Compose V2
- UFW firewall (configured)
- Tailscale VPN
- systemd service
### macOS-Specific
- Docker Desktop (via Homebrew Cask)
- Application Firewall
- Tailscale app
- OpenClaw on host (not containerized)
- Systemd service (auto-start)
## Manual Installation

View File

@ -19,7 +19,7 @@
- Runs `pnpm install` and `pnpm build`
- Symlinks `bin/openclaw.js` to `~/.local/bin/openclaw`
- Adds aliases: `openclaw-rebuild`, `openclaw-dev`, `openclaw-pull`
- Sets `CLAWDBOT_DEV_DIR` environment variable
- Sets `OPENCLAW_DEV_DIR` environment variable
**Usage**:
```bash
@ -87,7 +87,7 @@ ansible-playbook playbook.yml --ask-become-pass \
- Added Homebrew to PATH
- Enhanced security with ProtectSystem and ProtectHome
### 8. Clawdbot Installation via pnpm
### 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

View File

@ -184,7 +184,7 @@ The following aliases are added to `.bashrc`:
Plus an environment variable:
```bash
export CLAWDBOT_DEV_DIR="$HOME/code/openclaw"
export OPENCLAW_DEV_DIR="$HOME/code/openclaw"
```
## Configuration Variables

View File

@ -7,7 +7,7 @@ description: Firewall configuration, Docker isolation, and security hardening de
## Overview
This playbook implements a multi-layer defense strategy to secure Clawdbot installations.
This playbook implements a multi-layer defense strategy to secure OpenClaw installations.
## Security Layers
@ -71,24 +71,24 @@ Container processes run as unprivileged `openclaw` user.
### Layer 6: Systemd Hardening
The clawdbot service runs with security restrictions:
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 ~/.clawdbot is writable
- `ReadWritePaths` - Only ~/.openclaw is writable
### Layer 7: Scoped Sudo Access
The clawdbot user has limited sudo permissions (not full root):
The openclaw user has limited sudo permissions (not full root):
```bash
# Allowed commands only:
- systemctl start/stop/restart/status clawdbot
- systemctl start/stop/restart/status openclaw
- systemctl daemon-reload
- tailscale commands
- journalctl for clawdbot logs
- journalctl for openclaw logs
```
### Layer 8: Automatic Security Updates
@ -192,5 +192,5 @@ After installation, verify:
## Reporting Security Issues
If you discover a security vulnerability, please report it privately:
- Clawdbot: https://github.com/clawdbot/clawdbot/security
- This installer: https://github.com/openclaw/clawdbot-ansible/security
- OpenClaw: https://github.com/openclaw/openclaw/security
- This installer: https://github.com/openclaw/openclaw-ansible/security

View File

@ -31,15 +31,11 @@ echo -e "${GREEN}╚════════════════════
echo ""
# Detect operating system
if [[ "$OSTYPE" == "darwin"* ]]; then
OS_TYPE="macos"
echo -e "${GREEN}Detected: macOS${NC}"
elif command -v apt-get &> /dev/null; then
OS_TYPE="linux"
echo -e "${GREEN}Detected: Debian/Ubuntu Linux${NC}"
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 and macOS${NC}"
echo -e "${RED}✗ Error: Unsupported operating system${NC}"
echo -e "${RED} This installer supports: Debian/Ubuntu Linux only${NC}"
exit 1
fi

View File

@ -18,28 +18,31 @@
- name: Detect operating system
ansible.builtin.set_fact:
is_macos: "{{ ansible_os_family == 'Darwin' }}"
is_linux: "{{ ansible_os_family == 'Debian' }}"
is_debian: "{{ ansible_distribution in ['Debian', 'Ubuntu'] }}"
is_redhat: "{{ ansible_os_family == 'RedHat' }}"
- 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: Display detected OS
ansible.builtin.debug:
msg: |
Detected OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
OS Family: {{ ansible_os_family }}
macOS: {{ is_macos }}
Linux (Debian/Ubuntu): {{ is_debian }}
Linux (RedHat/CentOS): {{ is_redhat }}
- 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
when: is_debian and not ci_test
register: apt_upgrade_result
ignore_errors: true
- name: Display apt upgrade results
ansible.builtin.debug:
@ -71,42 +74,13 @@
run_once: true
changed_when: false
- name: Check if Homebrew is installed
ansible.builtin.stat:
path: "{{ '/opt/homebrew/bin/brew' if is_macos else '/home/linuxbrew/.linuxbrew/bin/brew' }}"
register: homebrew_check
- name: Install Homebrew (macOS and Linux)
ansible.builtin.shell: |
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
args:
creates: "{{ '/opt/homebrew/bin/brew' if is_macos else '/home/linuxbrew/.linuxbrew/bin/brew' }}"
when: not homebrew_check.stat.exists
register: homebrew_install
become: false
- name: Add Homebrew to PATH for current session (Linux)
ansible.builtin.set_fact:
ansible_env: "{{ ansible_env | combine({'PATH': '/home/linuxbrew/.linuxbrew/bin:' + (ansible_env.PATH | default(lookup('env', 'PATH')))}) }}"
when: is_linux and not is_macos
- name: Add Homebrew to PATH for current session (macOS)
ansible.builtin.set_fact:
ansible_env: "{{ ansible_env | combine({'PATH': '/opt/homebrew/bin:' + (ansible_env.PATH | default(lookup('env', 'PATH')))}) }}"
when: is_macos
- name: Display Homebrew installation status
ansible.builtin.debug:
msg: "✅ Homebrew installed successfully"
when: homebrew_install is defined and homebrew_install.changed
roles:
- openclaw
post_tasks:
- name: Copy ASCII art script
ansible.builtin.copy:
src: roles/openclaw/files/show-lobster.sh
ansible.builtin.template:
src: roles/openclaw/templates/show-lobster.sh.j2
dest: /tmp/show-lobster.sh
mode: '0755'
@ -133,7 +107,6 @@
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 " ✓ Homebrew: $(which brew 2>/dev/null || echo 'not found')"
echo " ✓ OpenClaw: $(openclaw --version 2>/dev/null || echo 'not found')"
echo ""
echo "────────────────────────────────────────────────────────"
@ -179,18 +152,19 @@
echo " • Troubleshoot: openclaw doctor"
echo " • List agents: openclaw agents list"
echo ""
echo "────────────────────────────────────────────────────────"
{% if tailscale_enabled | default(false) %}echo "────────────────────────────────────────────────────────"
echo "🌐 Connect Tailscale VPN (optional):"
echo "────────────────────────────────────────────────────────"
echo ""
echo " exit"
echo " sudo tailscale up"
echo ""
echo "────────────────────────────────────────────────────────"
{% endif %}echo "────────────────────────────────────────────────────────"
echo ""
echo "Type 'exit' to return to your previous user"
echo ""
rm -f ~/.openclaw-welcome
# 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:

View File

@ -1,7 +1,12 @@
---
# 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
@ -17,10 +22,6 @@ openclaw_config_dir: "{{ openclaw_home }}/.openclaw"
openclaw_user: openclaw
openclaw_home: /home/openclaw
# OS-specific settings (set dynamically in tasks)
homebrew_prefix: "{{ '/opt/homebrew' if ansible_os_family == 'Darwin' else '/home/linuxbrew/.linuxbrew' }}"
package_manager: "{{ 'brew' if ansible_os_family == 'Darwin' else 'apt' }}"
# Installation mode: 'release' or 'development'
# release: Install via pnpm install -g openclaw@latest
# development: Clone repo, build from source, link globally

View File

@ -1,40 +0,0 @@
---
# macOS-specific Docker installation (Docker Desktop)
- name: Check if Docker Desktop is installed (macOS)
ansible.builtin.stat:
path: /Applications/Docker.app
register: docker_desktop
- name: Install Docker Desktop via Homebrew Cask (macOS)
community.general.homebrew_cask:
name: docker
state: present
environment:
PATH: "/opt/homebrew/bin:{{ ansible_env.PATH }}"
when: not docker_desktop.stat.exists
- name: Wait for Docker Desktop to be available (macOS)
ansible.builtin.wait_for:
path: /var/run/docker.sock
timeout: 120
when: not docker_desktop.stat.exists
- name: Display Docker Desktop installation note (macOS)
ansible.builtin.debug:
msg: |
Docker Desktop installed on macOS.
Please ensure Docker Desktop is running and has been configured.
You may need to start it manually from Applications.
when: not docker_desktop.stat.exists
- name: Verify Docker is accessible
ansible.builtin.command: docker --version
register: docker_version
changed_when: false
failed_when: false
- name: Display Docker version
ansible.builtin.debug:
msg: "Docker installed: {{ docker_version.stdout }}"
when: docker_version.rc == 0

View File

@ -1,10 +0,0 @@
---
# Main Docker orchestration - delegates to OS-specific tasks
- name: Include Linux Docker installation
ansible.builtin.include_tasks: docker-linux.yml
when: ansible_os_family == 'Debian'
- name: Include macOS Docker installation
ansible.builtin.include_tasks: docker-macos.yml
when: ansible_os_family == 'Darwin'

View File

@ -103,6 +103,7 @@
port: '41641'
proto: udp
comment: 'Tailscale'
when: tailscale_enabled | bool
- name: Get default network interface
ansible.builtin.shell:
@ -113,6 +114,14 @@
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

View File

@ -1,31 +0,0 @@
---
# macOS-specific firewall configuration (pf)
- name: Display macOS firewall information
ansible.builtin.debug:
msg: |
macOS uses built-in Application Firewall and pf.
Docker Desktop on macOS handles its own network isolation.
Consider configuring the Application Firewall via System Preferences.
- name: Check macOS firewall status
ansible.builtin.command: /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate
register: macos_firewall_status
changed_when: false
become: true
- name: Display firewall status
ansible.builtin.debug:
msg: "macOS Firewall Status: {{ macos_firewall_status.stdout }}"
- name: Enable macOS Application Firewall (if disabled)
ansible.builtin.command: /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on
when: "'disabled' in macos_firewall_status.stdout.lower()"
become: true
changed_when: true
- name: Allow Tailscale through firewall
ansible.builtin.command: /usr/libexec/ApplicationFirewall/socketfilterfw --add /Applications/Tailscale.app
failed_when: false
become: true
changed_when: false

View File

@ -1,10 +0,0 @@
---
# Main firewall orchestration - delegates to OS-specific tasks
- name: Include Linux firewall configuration
ansible.builtin.include_tasks: firewall-linux.yml
when: ansible_os_family == 'Debian'
- name: Include macOS firewall configuration
ansible.builtin.include_tasks: firewall-macos.yml
when: ansible_os_family == 'Darwin'

View File

@ -3,16 +3,19 @@
ansible.builtin.include_tasks: system-tools.yml
- name: Include Tailscale installation tasks
ansible.builtin.include_tasks: tailscale.yml
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.yml
ansible.builtin.include_tasks: docker-linux.yml
when: not ci_test
- name: Include firewall configuration tasks
ansible.builtin.include_tasks: firewall.yml
ansible.builtin.include_tasks: firewall-linux.yml
when: not ci_test
- name: Include Node.js installation tasks
ansible.builtin.include_tasks: nodejs.yml

View File

@ -42,10 +42,15 @@
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
args:
creates: /usr/local/bin/pnpm
when: pnpm_check.rc != 0
- name: Verify Node.js installation
ansible.builtin.command: node --version

View File

@ -49,7 +49,7 @@
become_user: "{{ openclaw_user }}"
environment:
PNPM_HOME: "{{ openclaw_home }}/.local/share/pnpm"
PATH: "{{ openclaw_home }}/.local/bin:/home/linuxbrew/.linuxbrew/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
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"
@ -63,7 +63,7 @@
become_user: "{{ openclaw_user }}"
environment:
PNPM_HOME: "{{ openclaw_home }}/.local/share/pnpm"
PATH: "{{ openclaw_home }}/.local/bin:/home/linuxbrew/.linuxbrew/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
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
@ -106,12 +106,14 @@
- name: Verify openclaw installation from development build
ansible.builtin.shell:
cmd: "{{ openclaw_home }}/.local/bin/openclaw --version"
cmd: openclaw --version
executable: /bin/bash
become: true
become_user: "{{ openclaw_user }}"
environment:
PATH: "{{ openclaw_home }}/.local/bin:/usr/local/bin:/usr/bin:/bin"
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

View File

@ -9,17 +9,21 @@
become_user: "{{ openclaw_user }}"
environment:
PNPM_HOME: "{{ openclaw_home }}/.local/share/pnpm"
PATH: "{{ openclaw_home }}/.local/bin:/home/linuxbrew/.linuxbrew/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
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_home }}/.local/bin/openclaw --version"
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

View File

@ -1,4 +1,11 @@
---
- 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: Create OpenClaw directories (structure only, no config files)
ansible.builtin.file:
path: "{{ item.path }}"
@ -37,12 +44,24 @@
- name: Configure pnpm for openclaw user
ansible.builtin.shell:
cmd: |
pnpm config set global-dir {{ openclaw_home }}/.local/share/pnpm
pnpm config set global-bin-dir {{ openclaw_home }}/.local/bin
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 }}"
changed_when: true # Always consider changed as pnpm config may update
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:

View File

@ -4,8 +4,6 @@
- name: Install essential system tools (Linux - apt)
ansible.builtin.apt:
name:
# Shells
- zsh
# Editors
- vim
- nano
@ -40,17 +38,12 @@
- unzip
- rsync
- less
# Build essentials for Homebrew on Linux
# Build essentials for development
- build-essential
- file
state: present
update_cache: true
- name: Set zsh as default shell for openclaw user (Linux)
ansible.builtin.user:
name: "{{ openclaw_user }}"
shell: /usr/bin/zsh
- name: Deploy global vim configuration (Linux)
ansible.builtin.template:
src: vimrc.j2
@ -58,61 +51,3 @@
owner: root
group: root
mode: '0644'
- name: Configure .bashrc for openclaw user (Linux)
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 Homebrew to PATH (Linux)
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
# 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: Configure .zshrc for openclaw user (Linux)
ansible.builtin.blockinfile:
path: "{{ openclaw_home }}/.zshrc"
marker: "# {mark} ANSIBLE MANAGED BLOCK - OpenClaw config"
block: |
# Enable 256 colors
export TERM=xterm-256color
export COLORTERM=truecolor
# Add Homebrew to PATH (Linux)
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
# 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'

View File

@ -1,70 +0,0 @@
---
# macOS-specific system tools installation (Homebrew-based)
- name: Install essential system tools (macOS - Homebrew)
community.general.homebrew:
name:
# Shells
- zsh
# Editors
- vim
- nano
# Version control
- git
- git-lfs
# Network tools
- curl
- wget
- netcat
- nmap
- socat
- telnet
# Debugging tools
- htop
# System utilities
- tmux
- tree
- jq
- unzip
- rsync
state: present
environment:
PATH: "/opt/homebrew/bin:{{ ansible_env.PATH }}"
- name: Get current user shell (macOS)
ansible.builtin.command: dscl . -read /Users/{{ openclaw_user }} UserShell
register: current_shell
changed_when: false
failed_when: false
- name: Set zsh as default shell for openclaw user (macOS)
ansible.builtin.command: chsh -s /bin/zsh {{ openclaw_user }}
when: "'/bin/zsh' not in current_shell.stdout"
changed_when: true
- name: Configure .zshrc for openclaw user (macOS)
ansible.builtin.blockinfile:
path: "{{ openclaw_home }}/.zshrc"
marker: "# {mark} ANSIBLE MANAGED BLOCK - OpenClaw config"
block: |
# Enable 256 colors
export TERM=xterm-256color
export COLORTERM=truecolor
# Add Homebrew to PATH (macOS)
eval "$(/opt/homebrew/bin/brew shellenv)"
# Add pnpm to PATH
export PATH="{{ openclaw_home }}/.local/bin:$PATH"
# Color support for common tools
export CLICOLOR=1
export LSCOLORS=ExFxCxDxBxegedabagacad
# Aliases
alias ls='ls -G'
alias grep='grep --color=auto'
alias ll='ls -lah'
create: true
owner: "{{ openclaw_user }}"
mode: '0644'

View File

@ -1,33 +1,11 @@
---
# Main system tools orchestration - delegates to OS-specific tasks
# Main system tools orchestration - Linux only
- name: Include Linux system tools installation
ansible.builtin.include_tasks: system-tools-linux.yml
when: ansible_os_family == 'Debian'
- name: Include macOS system tools installation
ansible.builtin.include_tasks: system-tools-macos.yml
when: ansible_os_family == 'Darwin'
- name: Display unsupported OS warning
ansible.builtin.fail:
msg: "Unsupported OS family: {{ ansible_os_family }}. Only Debian/Ubuntu and macOS are supported."
when: ansible_os_family not in ['Debian', 'Darwin']
# Common tasks for all operating systems
- name: Install oh-my-zsh for openclaw user
ansible.builtin.shell:
cmd: |
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended
creates: "{{ openclaw_home }}/.oh-my-zsh"
executable: /bin/bash
become: true
become_user: "{{ openclaw_user }}"
environment:
HOME: "{{ openclaw_home }}"
USER: "{{ openclaw_user }}"
- name: Configure git globally
community.general.git_config:
name: "{{ item.name }}"

View File

@ -1,43 +0,0 @@
---
# macOS-specific Tailscale installation (Homebrew Cask)
- name: Check if Tailscale is already installed (macOS)
ansible.builtin.stat:
path: /Applications/Tailscale.app
register: tailscale_app_macos
- name: Install Tailscale via Homebrew Cask (macOS)
community.general.homebrew_cask:
name: tailscale
state: present
environment:
PATH: "/opt/homebrew/bin:{{ ansible_env.PATH }}"
when: not tailscale_app_macos.stat.exists
- name: Check if Tailscale is running (macOS)
ansible.builtin.command: /Applications/Tailscale.app/Contents/MacOS/Tailscale status --json
register: tailscale_status_macos
changed_when: false
failed_when: false
- name: Display Tailscale setup instructions (macOS)
ansible.builtin.debug:
msg:
- "============================================"
- "Tailscale installed on macOS"
- "============================================"
- ""
- "To connect this Mac to your Tailnet:"
- ""
- "Option 1 - GUI:"
- " 1. Open Tailscale from Applications"
- " 2. Click 'Log in' and follow the web flow"
- ""
- "Option 2 - CLI:"
- " sudo /Applications/Tailscale.app/Contents/MacOS/Tailscale up"
- ""
- "With auth key:"
- " sudo /Applications/Tailscale.app/Contents/MacOS/Tailscale up --authkey tskey-auth-xxxxx"
- ""
- "Get auth key from: https://login.tailscale.com/admin/settings/keys"
when: tailscale_status_macos.rc != 0

View File

@ -1,10 +0,0 @@
---
# Main Tailscale orchestration - delegates to OS-specific tasks
- name: Include Linux Tailscale installation
ansible.builtin.include_tasks: tailscale-linux.yml
when: ansible_os_family == 'Debian'
- name: Include macOS Tailscale installation
ansible.builtin.include_tasks: tailscale-macos.yml
when: ansible_os_family == 'Darwin'

View File

@ -9,6 +9,40 @@
home: /home/openclaw
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
@ -60,22 +94,35 @@
openclaw_user: openclaw
openclaw_home: /home/openclaw
- 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
register: openclaw_uid
changed_when: false
when: ansible_os_family == 'Debian'
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'
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
changed_when: false
when: ansible_os_family == 'Debian'
when: ansible_os_family == 'Debian' and not ci_test
- name: Create runtime directory for openclaw user
ansible.builtin.file:
@ -84,12 +131,12 @@
owner: openclaw
group: openclaw
mode: '0700'
when: ansible_os_family == 'Debian'
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'
when: ansible_os_family == 'Debian' and not ci_test
# SSH key configuration
- name: Create .ssh directory for openclaw user
@ -127,7 +174,7 @@
owner: openclaw
group: openclaw
mode: '0644'
when: ansible_os_family == 'Debian'
when: ansible_os_family == 'Debian' and not ci_test
- name: Set DBUS_SESSION_BUS_ADDRESS in .bashrc for openclaw user
ansible.builtin.blockinfile:
@ -144,4 +191,4 @@
owner: openclaw
group: openclaw
mode: '0644'
when: ansible_os_family == 'Debian'
when: ansible_os_family == 'Debian' and not ci_test

View File

@ -10,16 +10,16 @@ Group={{ openclaw_user }}
WorkingDirectory={{ openclaw_home }}
# Environment variables
Environment="PATH={{ openclaw_home }}/.local/bin:/home/linuxbrew/.linuxbrew/bin:/usr/local/bin:/usr/bin:/bin"
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 | default('1000') }}"
Environment="XDG_RUNTIME_DIR=/run/user/{{ openclaw_uid_value }}"
# DBus session bus
Environment="DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{{ openclaw_uid_value | default('1000') }}/bus"
Environment="DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{{ openclaw_uid_value }}/bus"
# Start command
ExecStart={{ openclaw_home }}/.local/bin/openclaw gateway
ExecStart=openclaw gateway
# Restart policy
Restart=always

View File

@ -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 %}

View File

@ -32,7 +32,6 @@ if [ $PLAYBOOK_EXIT -eq 0 ]; then
echo " • Configure OpenClaw (~/.openclaw/config.yml)"
echo " • Login to messaging provider (WhatsApp/Telegram/Signal)"
echo " • Test the gateway"
echo " • Connect Tailscale VPN"
echo ""
echo "═══════════════════════════════════════════════════════════"
echo ""

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
tests/README.md Normal file
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
tests/entrypoint.sh Executable file
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
tests/run-tests.sh Executable file
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"

70
tests/verify.yml Normal file
View File

@ -0,0 +1,70 @@
---
- 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
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 }}/.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 credentials dir has restricted permissions
ansible.builtin.assert:
that: dir_checks.results[2].stat.mode == '0700'
fail_msg: "credentials dir should be 0700"
- name: Verify sudoers file exists and is valid
ansible.builtin.command: visudo -cf /etc/sudoers.d/openclaw
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'