Files
env-ansible/tasks/workstation/linux/software/zeroclaw.yml
T

498 lines
19 KiB
YAML

---
# 2026-10-05 Hyperling
# Install a local LLM for use offline.
# - https://zeroclaw.net/blog/install-zeroclaw
# - https://zeroclaw.net/blog/set-up-zeroclaw-bot-ollama-telegram
#
# Packages Details
# - https://archlinux.org/packages/extra/x86_64/zeroclaw/
# - https://archlinux.org/packages/extra/x86_64/ollama/
#
# Individual Guides
# - https://github.com/zeroclaw-labs/zeroclaw
# - https://wiki.archlinux.org/title/Ollama
#
# Model Explanations
# - https://webscraft.org/blog/ollama-na-8-gb-ram-yaki-modeli-pratsyuyut-u-2026?lang=en
#
# Models
# - https://ollama.com/search
#
# References
# - https://docs.zeroclaw.com/master/en/reference/config.html
# - https://docs.ollama.com
# Currently only testing this on Arch based distros (pacman).
- name: Workstation | Linux | Software | ZeroClaw
block:
- name: Workstation | Linux | Software | ZeroClaw | Facts 01
ansible.builtin.set_fact:
ollama: ollama
zeroclaw: zeroclaw
zerocode: zerocode
llm_concurrency: 1 # "{{ ansible_processor_vcpus / 4 | round(0, method='ceil') | int }}"
llm_installs: []
llm_tier0_cpu: 1
llm_tier0_ram: 2000
llm_tier0:
- { "model": "qwen3.5:0.8b", "provider": "qwen_35_0008" }
- { "model": "qwen2.5-coder:0.5b", "provider": "qwen_25_coder_0005" }
- { "model": "qwen2.5-coder:1.5b", "provider": "qwen_25_coder_0015" }
llm_tier1_cpu: 2
llm_tier1_ram: 4000
llm_tier1:
- { "model": "qwen3.5:2b", "provider": "qwen_35_0020" }
- { "model": "qwen2.5-coder:3b", "provider": "qwen_25_coder_0030" }
llm_tier2_cpu: 3
llm_tier2_ram: 6000
llm_tier2:
- { "model": "qwen3.5:4b", "provider": "qwen_35_0040" }
- { "model": "qwen2.5-coder:7b", "provider": "qwen_25_coder_0070" }
llm_tier3_cpu: 4
llm_tier3_ram: 8000
llm_tier3:
- { "model": "qwen3.5:9b", "provider": "qwen_35_0090" }
- { "model": "qwen2.5-coder:14b", "provider": "qwen_25_coder_0140" }
llm_tier4_cpu: 6
llm_tier4_ram: 12000
llm_tier4: []
llm_tier5_cpu: 10
llm_tier5_ram: 20000
llm_tier5:
#- { "model": "devstral-small-2:24b", "provider": "devstral_20_0240_small" }
#- { "model": "qwen3.8:27b", "provider": "qwen_38_0270" }
llm_tier6_cpu: 12
llm_tier6_ram: 30000
llm_tier6:
#- { "model": "qwen3.6:35b", "provider": "qwen_36_0350" }
- { "model": "qwen3.5:35b", "provider": "qwen_35_0350" }
- { "model": "qwen3-coder:30b", "provider": "qwen_30_coder_0300" }
- { "model": "qwen2.5-coder:32b", "provider": "qwen_25_coder_0320" }
llm_tier7_cpu: 16
llm_tier7_ram: 64000
llm_tier7: []
llm_tier8_cpu: 32
llm_tier8_ram: 320000
llm_tier8:
- { "model": "qwen3.5:122b", "provider": "qwen_35_1220" }
llm_tier9_cpu: 64
llm_tier9_ram: 999000
llm_tier9:
- { "model": "qwen3-coder:420b", "provider": "qwen_30_coder_4200" }
llm_removals:
# tier0 #
# tier1 #
- { "model": "qwen3:4b", "provider": "qwen_30_0040" }
# tier2 #
# tier3 #
# tier4 #
- { "model": "qwen3.6:27b", "provider": "qwen_36_0270" }
- { "model": "qwen3.5:27b", "provider": "qwen_35_0270" }
# tier5 #
# tier9 #
##### TBD #####
### 2.5, does not have "thinking" as an attribute, tempting!
#- qwen2.5:0.5b
#- qwen2.5:1.5b
#- qwen2.5:3b
#- qwen2.5:7b
#- qwen2.5:14b
#- qwen2.5:32b
#- qwen2.5:72b
### Others
#- deepseek-r1:1.5b
#- deepseek-r1:7b
#- mistral:7b
#- ministral-3:3b
#- ministral-3:8b
#- ministral-3:14b
#- glm4-flash
- name: Workstation | Linux | Software | ZeroClaw | Facts 02
ansible.builtin.set_fact:
ollama_runit_dir: "{{ runit_script_dir }}/{{ ollama }}"
ollama_runit_log_dest: "{{ global_log }}/{{ ollama }}"
zeroclaw_runit_dir: "{{ runit_script_dir }}/{{ zeroclaw }}"
zeroclaw_runit_log_dest: "{{ global_log }}/{{ zeroclaw }}"
- name: Workstation | Linux | Software | ZeroClaw | Facts 03
ansible.builtin.set_fact:
ollama_runit_script: "{{ ollama_runit_dir }}/{{ runit_service_script }}"
ollama_runit_log_dir: "{{ ollama_runit_dir }}/{{ runit_log_dir }}"
ollama_runit_log_config: "{{ ollama_runit_log_dest }}/{{ global_log_config }}"
zeroclaw_runit_script: "{{ zeroclaw_runit_dir }}/{{ runit_service_script }}"
zeroclaw_runit_log_dir: "{{ zeroclaw_runit_dir }}/{{ runit_log_dir }}"
zeroclaw_runit_log_config: "{{ zeroclaw_runit_log_dest }}/{{ global_log_config }}"
- name: Workstation | Linux | Software | ZeroClaw | Facts 04
ansible.builtin.set_fact:
ollama_runit_log_script: "{{ ollama_runit_log_dir }}/{{ runit_log_script }}"
zeroclaw_runit_log_script: "{{ zeroclaw_runit_log_dir }}/{{ runit_log_script }}"
- name: Workstation | Linux | Software | ZeroClaw | Facts | Merge | tier0
ansible.builtin.set_fact:
llm_installs: "{{ llm_installs + llm_tier0 }}"
when: ansible_processor_vcpus >= llm_tier0_cpu
and ansible_memtotal_mb >= llm_tier0_ram
- name: Workstation | Linux | Software | ZeroClaw | Facts | Merge | tier0 | Uninstall
ansible.builtin.set_fact:
llm_removals: "{{ llm_removals + llm_tier0 }}"
when: ansible_processor_vcpus < llm_tier0_cpu
or ansible_memtotal_mb < llm_tier0_ram
- name: Workstation | Linux | Software | ZeroClaw | Facts | Merge | tier1
ansible.builtin.set_fact:
llm_installs: "{{ llm_installs + llm_tier1 }}"
when: ansible_processor_vcpus >= llm_tier1_cpu
and ansible_memtotal_mb >= llm_tier1_ram
- name: Workstation | Linux | Software | ZeroClaw | Facts | Merge | tier1 | Uninstall
ansible.builtin.set_fact:
llm_removals: "{{ llm_removals + llm_tier1 }}"
when: ansible_processor_vcpus < llm_tier1_cpu
or ansible_memtotal_mb < llm_tier1_ram
- name: Workstation | Linux | Software | ZeroClaw | Facts | Merge | tier2
ansible.builtin.set_fact:
llm_installs: "{{ llm_installs + llm_tier2 }}"
when: ansible_processor_vcpus >= llm_tier2_cpu
and ansible_memtotal_mb >= llm_tier2_ram
- name: Workstation | Linux | Software | ZeroClaw | Facts | Merge | tier2 | Uninstalls
ansible.builtin.set_fact:
llm_removals: "{{ llm_removals + llm_tier2 }}"
when: ansible_processor_vcpus < llm_tier2_cpu
or ansible_memtotal_mb < llm_tier2_ram
- name: Workstation | Linux | Software | ZeroClaw | Facts | Merge | tier3
ansible.builtin.set_fact:
llm_installs: "{{ llm_installs + llm_tier3 }}"
when: ansible_processor_vcpus >= llm_tier3_cpu
and ansible_memtotal_mb >= llm_tier3_ram
- name: Workstation | Linux | Software | ZeroClaw | Facts | Merge | tier3 | Uninstalls
ansible.builtin.set_fact:
llm_removals: "{{ llm_removals + llm_tier3 }}"
when: ansible_processor_vcpus < llm_tier3_cpu
or ansible_memtotal_mb < llm_tier3_ram
- name: Workstation | Linux | Software | ZeroClaw | Facts | Merge | tier4
ansible.builtin.set_fact:
llm_installs: "{{ llm_installs + llm_tier4 }}"
when: ansible_processor_vcpus >= llm_tier4_cpu
and ansible_memtotal_mb >= llm_tier4_ram
- name: Workstation | Linux | Software | ZeroClaw | Facts | Merge | tier4 | Uninstalls
ansible.builtin.set_fact:
llm_removals: "{{ llm_removals + llm_tier4 }}"
when: ansible_processor_vcpus < llm_tier4_cpu
or ansible_memtotal_mb < llm_tier4_ram
- name: Workstation | Linux | Software | ZeroClaw | Facts | Merge | tier5
ansible.builtin.set_fact:
llm_installs: "{{ llm_installs + llm_tier5 }}"
when: ansible_processor_vcpus >= llm_tier5_cpu
and ansible_memtotal_mb >= llm_tier5_ram
- name: Workstation | Linux | Software | ZeroClaw | Facts | Merge | tier5 | Uninstalls
ansible.builtin.set_fact:
llm_removals: "{{ llm_removals + llm_tier5 }}"
when: ansible_processor_vcpus < llm_tier5_cpu
or ansible_memtotal_mb < llm_tier5_ram
- name: Workstation | Linux | Software | ZeroClaw | Facts | Merge | tier6
ansible.builtin.set_fact:
llm_installs: "{{ llm_installs + llm_tier6 }}"
when: ansible_processor_vcpus >= llm_tier6_cpu
and ansible_memtotal_mb >= llm_tier6_ram
- name: Workstation | Linux | Software | ZeroClaw | Facts | Merge | tier6 | Uninstalls
ansible.builtin.set_fact:
llm_removals: "{{ llm_removals + llm_tier6 }}"
when: ansible_processor_vcpus < llm_tier6_cpu
or ansible_memtotal_mb < llm_tier6_ram
- name: Workstation | Linux | Software | ZeroClaw | Facts | Merge | tier7
ansible.builtin.set_fact:
llm_installs: "{{ llm_installs + llm_tier7 }}"
when: ansible_processor_vcpus >= llm_tier7_cpu
and ansible_memtotal_mb >= llm_tier7_ram
- name: Workstation | Linux | Software | ZeroClaw | Facts | Merge | tier7 | Uninstalls
ansible.builtin.set_fact:
llm_removals: "{{ llm_removals + llm_tier7 }}"
when: ansible_processor_vcpus < llm_tier7_cpu
or ansible_memtotal_mb < llm_tier7_ram
- name: Workstation | Linux | Software | ZeroClaw | Facts | Merge | tier8
ansible.builtin.set_fact:
llm_installs: "{{ llm_installs + llm_tier8 }}"
when: ansible_processor_vcpus >= llm_tier8_cpu
and ansible_memtotal_mb >= llm_tier8_ram
- name: Workstation | Linux | Software | ZeroClaw | Facts | Merge | tier8 | Uninstalls
ansible.builtin.set_fact:
llm_removals: "{{ llm_removals + llm_tier8 }}"
when: ansible_processor_vcpus < llm_tier8_cpu
or ansible_memtotal_mb < llm_tier8_ram
- name: Workstation | Linux | Software | ZeroClaw | Facts | Merge | tier9
ansible.builtin.set_fact:
llm_installs: "{{ llm_installs + llm_tier9 }}"
when: ansible_processor_vcpus >= llm_tier9_cpu
and ansible_memtotal_mb >= llm_tier9_ram
- name: Workstation | Linux | Software | ZeroClaw | Facts | Merge | tier9 | Uninstalls
ansible.builtin.set_fact:
llm_removals: "{{ llm_removals + llm_tier9 }}"
when: ansible_processor_vcpus < llm_tier9_cpu
or ansible_memtotal_mb < llm_tier9_ram
- name: Workstation | Linux | Software | ZeroClaw | Facts | Merge | Print
ansible.builtin.debug:
var: "{{ item }}"
loop:
- ansible_processor_vcpus
- ansible_memtotal_mb
- llm_concurrency
- llm_installs
- llm_removals
- name: Workstation | Linux | Software | ZeroClaw | Install
ansible.builtin.package:
name:
- "{{ zeroclaw }}"
- "{{ ollama }}"
- "{{ zerocode }}"
state: present
- name: Workstation | Linux | Software | ZeroClaw | runit
block:
- name: Workstation | Linux | Software | ZeroClaw | runit | ollama | Dir | Run
ansible.builtin.file:
name: "{{ ollama_runit_dir }}"
state: directory
owner: root
mode: "0755"
- name: Workstation | Linux | Software | ZeroClaw | runit | ollama | Dir | Log Run
ansible.builtin.file:
name: "{{ ollama_runit_log_dir }}"
state: directory
owner: root
mode: "0755"
- name: Workstation | Linux | Software | ZeroClaw | runit | ollama | Dir | Log Config
ansible.builtin.file:
name: "{{ ollama_runit_log_dest }}"
state: directory
owner: "{{ user }}"
mode: "0755"
- name: Workstation | Linux | Software | ZeroClaw | runit | ollama | Script | Run
ansible.builtin.copy:
content: |
#!/bin/sh -e
# Allow pulling LLMs for use with ZeroClaw.
exec 2>&1
export HOME={{ user_user.home }}
export OLLAMA_KEEP_ALIVE=20m # default 5m
export OLLAMA_REQUEST_TIMEOUT=3600 # default 300 (unconfirmed, not in the list of --help vars)
export OLLAMA_LOAD_TIMEOUT=30m # default is 5m
export OLLAMA_MAX_LOADED_MODELS={{ llm_concurrency }} # default is 3 if using CPUs, or 3 * NumGPUs
export OLLAMA_MAX_QUEUE=999 # default is 512 (unconfirmed)
export OLLAMA_NUM_PARALLEL={{ llm_concurrency }} # default is 1 and applies per-model (unconfirmed)
export OLLAMA_CONTEXT_LENGTH=999999 # default is 4096, https://docs.ollama.com/faq#how-can-i-specify-the-context-window-size
exec chpst -u {{ user }} ollama serve
dest: "{{ ollama_runit_script }}"
mode: "0755"
- name: Workstation | Linux | Software | ZeroClaw | runit | ollama | Script | Log
ansible.builtin.copy:
content: |
#!/bin/sh -e
exec svlogd -tt {{ ollama_runit_log_dest }}
dest: "{{ ollama_runit_log_script }}"
mode: "0755"
- name: Workstation | Linux | Software | ZeroClaw | runit | ollama | Script | Log Config
ansible.builtin.copy:
content: |
s10000000 # rotate at 10 MB
n5 # keep 5 old files
dest: "{{ ollama_runit_log_config }}"
owner: "{{ user }}"
mode: "0755"
- name: Workstation | Linux | Software | ZeroClaw | runit | zeroclaw | Dir | Run
ansible.builtin.file:
name: "{{ zeroclaw_runit_dir }}"
state: directory
owner: root
mode: "0755"
- name: Workstation | Linux | Software | ZeroClaw | runit | zeroclaw | Dir | Log
ansible.builtin.file:
name: "{{ zeroclaw_runit_log_dir }}"
state: directory
owner: root
mode: "0755"
- name: Workstation | Linux | Software | ZeroClaw | runit | zeroclaw | Dir | Log Config
ansible.builtin.file:
name: "{{ zeroclaw_runit_log_dest }}"
state: directory
owner: "{{ user }}"
mode: "0755"
- name: Workstation | Linux | Software | ZeroClaw | runit | zeroclaw | Script | File
ansible.builtin.copy:
content: |
#!/bin/sh -e
# Allow using the ZeroClaw frontend.
exec 2>&1
export HOME={{ user_user.home }}
export SHELL={{ bash_exec.stdout }}
exec chpst -u {{ user }} zeroclaw daemon -v --log-level info
dest: "{{ zeroclaw_runit_script }}"
mode: "0755"
- name: Workstation | Linux | Software | ZeroClaw | runit | zeroclaw | Script | Log
ansible.builtin.copy:
content: |
#!/bin/sh -e
exec svlogd -tt {{ zeroclaw_runit_log_dest }}
dest: "{{ zeroclaw_runit_log_script }}"
mode: "0755"
- name: Workstation | Linux | Software | ZeroClaw | runit | zeroclaw | Script | Log Config
ansible.builtin.copy:
content: |
s1000000 # rotate at 1 MB
n5 # keep 5 old files
dest: "{{ zeroclaw_runit_log_config }}"
mode: "0755"
when: ansible_service_mgr == "runit"
- name: Workstation | Linux | Software | ZeroClaw | Enable
ansible.builtin.service:
name: "{{ item }}"
state: started
enabled: true
loop:
- "{{ ollama }}"
- "{{ zeroclaw }}"
- name: Workstation | Linux | Software | ZeroClaw | Verify
ansible.builtin.uri:
url: "{{ item }}"
status_code: 200
timeout: 5
register: health
until: health.status == 200
retries: 12
delay: 5
loop:
- http://127.0.0.1:11434/
- http://127.0.0.1:42617/health
#- name: Workstation | Linux | Software | ZeroClaw | Config | zeroclaw | Root
# ansible.builtin.blockinfile:
# path: "{{ user_user.home }}/.zeroclaw/config.toml"
# marker: "# {mark} ANSIBLE MANAGED BLOCK | zeroclaw | Defaults"
# insertbefore: '(?m)^schema_version'
# block: |
# default_provider = "ollama"
# default_model = "qwen3.5:4b"
# provider_timeout_secs = 3600
# state: present
# create: false
# backup: true
#- name: Workstation | Linux | Software | ZeroClaw | Config | zeroclaw | Channels | header
# ansible.builtin.lineinfile:
# path: "{{ user_user.home }}/.zeroclaw/config.toml"
# insertbefore: '(?m)^[channels]'
# block: |
# message_timeout_secs = 3600
# state: present
# create: false
# backup: true
#- name: Workstation | Linux | Software | ZeroClaw | Config | zeroclaw | Channels
# ansible.builtin.blockinfile:
# path: "{{ user_user.home }}/.zeroclaw/config.toml"
# marker: "# {mark} ANSIBLE MANAGED BLOCK | zeroclaw | Channels"
# insertbefore: '(?m)^[channels]'
# block: |
# message_timeout_secs = 3600
# state: present
# create: false
# backup: true
- name: Workstation | Linux | Software | ZeroClaw | Models | ollama | Install
ansible.builtin.shell: |
set -o pipefail
item='{{ item.model }}'
if ollama list | grep -q $item; then
echo "OKAY" &&
return
else
ollama pull $item 1>/dev/null &&
echo "INSTALLED" &&
return
fi
echo "FAILED"
loop: "{{ llm_installs }}"
register: shell_check
changed_when: "'INSTALLED' in shell_check.stdout"
failed_when: "'FAILED' in shell_check.stdout"
become: true
become_user: "{{ user }}"
when: llm_installs | length > 0
- name: Workstation | Linux | Software | ZeroClaw | Models | ollama | Remove
ansible.builtin.shell: |
set -o pipefail
item='{{ item.model }}'
if ollama list | grep -q $item; then
ollama rm $item &&
echo "INSTALLED" &&
return
else
echo "OKAY" &&
return
fi
echo "FAILED"
loop: "{{ llm_removals }}"
register: shell_check
changed_when: "'INSTALLED' in shell_check.stdout"
failed_when: "'FAILED' in shell_check.stdout"
become: true
become_user: "{{ user }}"
when: llm_removals | length > 0
# Cheat Sheet: https://docs.zeroclaw.com/master/en/providers/configuration.html
- name: Workstation | Linux | Software | ZeroClaw | Models | zeroclaw | Add Providers
ansible.builtin.blockinfile:
path: "{{ user_user.home }}/.zeroclaw/config.toml"
marker: "# {mark} ANSIBLE MANAGED BLOCK | zeroclaw | Providers"
insertafter: EOF
block: |
{% for llm in llm_installs %}
[providers.models.ollama.{{ llm.provider }}]
model = "{{ llm.model }}"
timeout_secs = 3600 # 1 hour, default is 120 (2 minutes), issue happens between 5 and 6 minutes though.
context_window = 999999999
{% endfor %}
state: present
create: false
backup: true
when: ansible_pkg_mgr == "pacman"