chore: align AI agent runtime playbooks

This commit is contained in:
Haitao Pan 2026-05-26 12:58:56 +08:00
parent 7fbba293a0
commit 69e7691287
36 changed files with 1086 additions and 92 deletions

9
deploy_QMD.yml Normal file
View File

@ -0,0 +1,9 @@
---
- name: Deploy QMD extended memory
hosts: "{{ qmd_hosts | default('all') }}"
become: true
gather_facts: true
roles:
- role: roles/vhosts/qmd/
tags: [qmd]

9
deploy_agent_hermes.yml Normal file
View File

@ -0,0 +1,9 @@
---
- name: Deploy Hermes ACP agent adapter
hosts: "{{ acp_hermes_hosts | default('all') }}"
become: true
gather_facts: true
roles:
- role: roles/vhosts/acp_server_hermes/
tags: [acp_hermes, hermes]

View File

@ -1,9 +0,0 @@
---
- name: Deploy shared agent skills
hosts: all
become: true
gather_facts: true
roles:
- role: roles/agent_skills/
when: agent_skills_enabled | default(true) | bool
tags: [agent_skills]

View File

@ -1,8 +1,4 @@
--- ---
- import_playbook: deploy_agent_skills.yml
vars:
agent_skills_enabled: "{{ xworkmate_bridge_agent_skills_enabled | default(false) }}"
- name: Ensure minimal XFCE XRDP desktop baseline - name: Ensure minimal XFCE XRDP desktop baseline
hosts: "{{ xworkmate_bridge_hosts | default('all') }}" hosts: "{{ xworkmate_bridge_hosts | default('all') }}"
become: true become: true
@ -11,9 +7,15 @@
- role: roles/vhosts/xfce_xrdp_minimal/ - role: roles/vhosts/xfce_xrdp_minimal/
tags: [xfce, xfce_xrdp_minimal] tags: [xfce, xfce_xrdp_minimal]
- import_playbook: setup-ai-agent-runtime.yml - import_playbook: setup-ai-agent-skills.yml
vars: vars:
ai_agent_runtime_enabled: "{{ xworkmate_bridge_ai_agent_runtime_enabled | default(false) }}" ai_agent_runtime_enabled: "{{ xworkmate_bridge_ai_agent_runtime_enabled | default(false) or xworkmate_bridge_agent_skills_enabled | default(false) }}"
ai_agent_runtime_nodejs_enabled: "{{ xworkmate_bridge_ai_agent_runtime_enabled | default(false) }}"
ai_agent_runtime_python_enabled: "{{ xworkmate_bridge_ai_agent_runtime_enabled | default(false) }}"
ai_agent_runtime_browser_enabled: "{{ xworkmate_bridge_ai_agent_runtime_enabled | default(false) }}"
ai_agent_runtime_docs_enabled: "{{ xworkmate_bridge_ai_agent_runtime_enabled | default(false) }}"
ai_agent_runtime_fonts_enabled: "{{ xworkmate_bridge_ai_agent_runtime_enabled | default(false) }}"
ai_agent_runtime_verify_enabled: "{{ xworkmate_bridge_ai_agent_runtime_enabled | default(false) }}"
ai_agent_runtime_skills_enabled: "{{ xworkmate_bridge_agent_skills_enabled | default(false) }}" ai_agent_runtime_skills_enabled: "{{ xworkmate_bridge_agent_skills_enabled | default(false) }}"
- name: Deploy ACP vhosts through xworkmate bridge - name: Deploy ACP vhosts through xworkmate bridge

View File

@ -0,0 +1,205 @@
# yitu-it-series R2 assets
This runbook migrates the local Google Drive `自媒体` directory to Cloudflare R2 for the Docusaurus AI Native knowledge base.
## Architecture
```text
GitHub -> Docusaurus -> Cloudflare Pages -> ebook.svc.plus
Google Drive local folder
-> rclone
-> Cloudflare R2 bucket: yitu-it-series
-> R2 custom domain: img.svc.plus
-> Docusaurus Markdown image URLs
```
## Source and target
```text
Local source:
/Users/shenlan/Library/CloudStorage/GoogleDrive-haitaopanhq@gmail.com/我的云端硬盘/自媒体
R2 bucket:
yitu-it-series
Public asset domain:
https://img.svc.plus
```
## Recommended object layout
```text
yitu-it-series/
├── covers/
├── xiaohongshu/
├── observability/
├── storage/
├── networking/
├── ai-native/
├── security/
├── platform-engineering/
└── ebook-assets/
```
Use stable, semantic paths for published content:
```text
covers/season-1/single-machine-to-platform-cover-v1.png
security/least-privilege/root-to-rootless-v1.png
ai-native/agentic-infra/ai-native-platform-v1.png
ebook-assets/diagrams/cloud-native-to-ai-native-v1.png
```
Prefer versioned object names instead of overwriting an already published image. This keeps Cloudflare CDN behavior predictable and preserves old articles.
## Cloudflare API token
Create two token scopes if possible:
```text
Bootstrap token:
- Account: Cloudflare R2: Edit
- Zone: DNS: Edit, Zone: Read for svc.plus
- Used only for bucket/custom-domain setup
Long-running R2 S3 token:
- R2 Object Read & Write
- Scope limited to bucket yitu-it-series
- Used by rclone sync
```
Required environment variables:
```bash
export CF_ACCOUNT_ID="..."
export CF_ZONE_ID="..."
export CLOUDFLARE_API_TOKEN="..."
export R2_ACCESS_KEY_ID="..."
export R2_SECRET_ACCESS_KEY="..."
```
## Commands
From the playbooks directory:
```bash
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
chmod +x scripts/sync-yitu-it-series-r2.sh
scripts/sync-yitu-it-series-r2.sh doctor
scripts/sync-yitu-it-series-r2.sh create-bucket
scripts/sync-yitu-it-series-r2.sh configure-rclone
scripts/sync-yitu-it-series-r2.sh dry-run
scripts/sync-yitu-it-series-r2.sh copy
scripts/sync-yitu-it-series-r2.sh check
scripts/sync-yitu-it-series-r2.sh tree
scripts/sync-yitu-it-series-r2.sh configure-custom-domain
```
Use `copy` for the first production migration when preserving all historical remote files matters. Use `sync` for steady-state mirroring after the source layout is stable.
## Performance profile
Default large AI image profile:
```bash
export RCLONE_TRANSFERS=16
export RCLONE_CHECKERS=32
export RCLONE_S3_UPLOAD_CUTOFF=128M
export RCLONE_S3_CHUNK_SIZE=128M
```
Many small images:
```bash
export RCLONE_TRANSFERS=32
export RCLONE_CHECKERS=64
```
Large source files such as PSD/video:
```bash
export RCLONE_TRANSFERS=4
export RCLONE_CHECKERS=16
export RCLONE_S3_UPLOAD_CUTOFF=256M
export RCLONE_S3_CHUNK_SIZE=256M
```
## Incremental sync
Install a macOS launchd sync job:
```bash
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
scripts/sync-yitu-it-series-r2.sh install-launchd
launchctl list | grep yitu-it-series
```
Remove it:
```bash
scripts/sync-yitu-it-series-r2.sh uninstall-launchd
```
## R2 custom domain
Target:
```text
img.svc.plus -> R2 bucket yitu-it-series
```
The script calls the Cloudflare R2 custom domain API:
```bash
scripts/sync-yitu-it-series-r2.sh configure-custom-domain
```
Recommended Cloudflare cache rule:
```text
If hostname equals img.svc.plus:
- Cache eligible
- Edge TTL: 30 days or longer
- Browser TTL: 7-30 days, or respect origin
```
## Docusaurus references
Markdown:
```md
![AI Native 基础设施演进](https://img.svc.plus/ai-native/ai-native-infra-cover-v1.png)
![最小权限演进](https://img.svc.plus/security/least-privilege-cover-v1.png)
```
MDX:
```mdx
<img
src="https://img.svc.plus/platform-engineering/platform-engineering-roadmap-v1.png"
alt="Platform Engineering Roadmap"
loading="lazy"
/>
```
Front matter:
```md
---
title: AI Native 基础设施演进
description: 从云原生到 AI Native 的平台工程知识库
image: https://img.svc.plus/covers/ai-native-infra-cover-v1.png
---
```
## AI Native knowledge-base practices
- Keep Docusaurus focused on Markdown, MDX, navigation, SEO, and search.
- Keep heavy generated images and ebook assets in R2.
- Reference published assets with absolute `https://img.svc.plus/...` URLs.
- Keep object names immutable after publication; publish revisions with `-v2`, `-v3`.
- Run `rclone check` before replacing local Markdown image references.
- Keep raw generation artifacts separate from article-ready assets when possible.
- Use topic directories that match the ebook taxonomy so future RAG/vector indexing can attach image context to chapters.

View File

@ -0,0 +1,15 @@
CF_ACCOUNT_ID=
CF_ZONE_ID=
CLOUDFLARE_API_TOKEN=
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET=yitu-it-series
R2_REMOTE=cloudflare-r2
R2_CUSTOM_DOMAIN=img.svc.plus
LOCAL_SRC=/Users/shenlan/Library/CloudStorage/GoogleDrive-haitaopanhq@gmail.com/我的云端硬盘/自媒体
RCLONE_TRANSFERS=16
RCLONE_CHECKERS=32
RCLONE_S3_UPLOAD_CUTOFF=128M
RCLONE_S3_CHUNK_SIZE=128M

View File

@ -0,0 +1,14 @@
---
xworkmate_bridge_domain: cn-xworkmate-bridge.svc.plus
xworkmate_bridge_public_base_url: https://cn-xworkmate-bridge.svc.plus
xworkmate_bridge_service_domain: cn-xworkmate-bridge.svc.plus
xworkmate_bridge_service_public_base_url: https://cn-xworkmate-bridge.svc.plus
xworkmate_bridge_binary_path: /usr/local/bin/xworkmate-bridge
xworkmate_bridge_service_user: root
xworkmate_bridge_service_group: root
xworkmate_bridge_service_home: /root
xworkmate_bridge_required_services: []
xworkmate_bridge_required_listeners:
- host: 127.0.0.1
port: "8787"
name: bridge

View File

@ -7,6 +7,10 @@ cn-front.svc.plus ansible_host=47.120.61.35 ansible_user=root ansible_ssh_user=r
# services: cn-homepage.svc.plus # services: cn-homepage.svc.plus
cn-homepage.svc.plus ansible_host=47.120.61.35 ansible_user=root ansible_ssh_user=root cn-homepage.svc.plus ansible_host=47.120.61.35 ansible_user=root ansible_ssh_user=root
[cn_xworkmate_bridge_host]
# services: cn-xworkmate-bridge.svc.plus
cn-xworkmate-bridge.svc.plus ansible_host=47.120.61.35 ansible_user=root ansible_ssh_user=root service_domains=cn-xworkmate-bridge.svc.plus
[global_homepage_host] [global_homepage_host]
# services: global-homepage.svc.plus # services: global-homepage.svc.plus
global-homepage.svc.plus ansible_host=46.250.251.132 ansible_user=root ansible_ssh_user=root global-homepage.svc.plus ansible_host=46.250.251.132 ansible_user=root ansible_ssh_user=root

View File

@ -1,18 +1,31 @@
# Agent Skills # Agent Skills
Synchronizes the controller user's `~/.agents/skills/` directory to an Ubuntu Synchronizes controller skill sources to an Ubuntu runtime user's canonical
runtime user's canonical skills directory, then exposes the same directory to skills directory, then exposes the same directory to agent-specific skill
agent-specific skill locations. locations.
Default source and target: Default source and target:
- local source: `~/.agents/skills/` - local marketplace source: `~/.agents/skills/`
- local repository source: `../xworkspace-core-skills/skills/`
- remote canonical path: `/home/ubuntu/.agents/skills/` - remote canonical path: `/home/ubuntu/.agents/skills/`
- default agent targets: `codex`, `gemini`, `opencode`, `hermers`, `openclaw` - default agent targets: `codex`, `gemini`, `opencode`, `hermers`, `openclaw`
The repository source is categorized by capability domain, for example
`video-production/`, `image-production/`, `animation/`, and `workspace-core/`.
The role syncs those categories as-is, then creates root-level symlinks for
nested skills so runtimes that scan one directory level can still discover them.
Set `agent_skills_xworkspace_core_enabled=false` to use only the marketplace
source, or `agent_skills_remote_flatten_nested_skills=false` to disable root
symlink materialization.
The role keeps one remote source of truth and links each agent's skills entry to The role keeps one remote source of truth and links each agent's skills entry to
that canonical directory. Existing non-symlink target directories are rejected by that canonical directory where the online runtime already uses links. Existing
default to avoid silently deleting agent-owned content. Set non-symlink target directories are rejected by default to avoid silently deleting
agent-owned content. The live `ubuntu` Codex runtime on
`xworkmate-bridge.svc.plus` keeps `/home/ubuntu/.codex/skills` as a real
directory, so it is preserved by default through
`agent_skills_preserve_existing_target_dirs`. Set
`agent_skills_replace_existing_target_dirs=true` only when those target `agent_skills_replace_existing_target_dirs=true` only when those target
directories should be replaced. directories should be replaced.
@ -42,6 +55,11 @@ already present locally. Set
skills when neither installer is available; the role still fails later if a skills when neither installer is available; the role still fails later if a
required skill cannot be resolved. required skill cannot be resolved.
Required-skill checks search both the marketplace source and the categorized
repository source recursively. Auto-install still writes only to
`~/.agents/skills/`; repository-owned skills should be changed in
`xworkspace-core-skills`.
After install, optional local quality gates run for each resolved skill when the After install, optional local quality gates run for each resolved skill when the
command exists: command exists:
@ -58,16 +76,20 @@ Default sync excludes local runtime artifacts such as `.venv/`, `__pycache__/`,
`.pyc`, and `.DS_Store`; skills should ship source, scripts, templates, and `.pyc`, and `.DS_Store`; skills should ship source, scripts, templates, and
references rather than controller-local virtual environments. references rather than controller-local virtual environments.
The sync defaults to overlay mode (`agent_skills_delete_removed=false`) so it
does not remove skills that already exist on the live runtime catalog. Enable
deletion only for controlled rebuilds of `/home/ubuntu/.agents/skills/`.
Example: Example:
```bash ```bash
ansible-playbook -i inventory.ini -l jp-xhttp-contabo.svc.plus deploy_agent_skills.yml ansible-playbook -i inventory.ini -l jp-xhttp-contabo.svc.plus setup-ai-agent-skills.yml --tags agent_skills
``` ```
Bootstrap-only example that keeps the existing local source strict but skips Bootstrap-only example that keeps the existing local source strict but skips
quality gate failures from newly installed marketplace skills: quality gate failures from newly installed marketplace skills:
```bash ```bash
ansible-playbook -i inventory.ini -l jp-xhttp-contabo.svc.plus deploy_agent_skills.yml \ ansible-playbook -i inventory.ini -l jp-xhttp-contabo.svc.plus setup-ai-agent-skills.yml --tags agent_skills \
-e agent_skills_quality_gate_fail_on_error=false -e agent_skills_quality_gate_fail_on_error=false
``` ```

View File

@ -4,13 +4,19 @@ agent_skills_group: "{{ agent_skills_user }}"
agent_skills_home: "/home/{{ agent_skills_user }}" agent_skills_home: "/home/{{ agent_skills_user }}"
agent_skills_local_source_dir: "{{ lookup('ansible.builtin.env', 'HOME') }}/.agents/skills" agent_skills_local_source_dir: "{{ lookup('ansible.builtin.env', 'HOME') }}/.agents/skills"
agent_skills_xworkspace_core_enabled: true
agent_skills_xworkspace_core_required: true
agent_skills_xworkspace_core_source_dir: "{{ playbook_dir | dirname }}/xworkspace-core-skills/skills"
agent_skills_remote_dir: "{{ agent_skills_home }}/.agents/skills" agent_skills_remote_dir: "{{ agent_skills_home }}/.agents/skills"
agent_skills_local_source_create: true agent_skills_local_source_create: true
agent_skills_delete_removed: true agent_skills_delete_removed: false
agent_skills_rsync_compress: false agent_skills_rsync_compress: false
agent_skills_rsync_timeout: 120 agent_skills_rsync_timeout: 120
agent_skills_install_rsync: true agent_skills_install_rsync: true
agent_skills_replace_existing_target_dirs: false agent_skills_replace_existing_target_dirs: false
agent_skills_preserve_existing_target_dirs:
- "{{ agent_skills_home }}/.codex/skills"
agent_skills_remote_flatten_nested_skills: true
agent_skills_auto_install_enabled: true agent_skills_auto_install_enabled: true
agent_skills_auto_install_fail_on_missing_installer: true agent_skills_auto_install_fail_on_missing_installer: true
agent_skills_quality_gate_enabled: true agent_skills_quality_gate_enabled: true
@ -31,8 +37,10 @@ agent_skills_rsync_excludes:
- .venv/ - .venv/
- __pycache__/ - __pycache__/
- "*.pyc" - "*.pyc"
- "*/__pycache__/"
- "*/.DS_Store"
agent_skills_rsync_extra_opts: agent_skills_rsync_extra_opts:
- "--delete-excluded" - "--protocol=29"
- "--out-format=<<CHANGED>>%i" - "--out-format=<<CHANGED>>%i"
agent_skills_typical_scenario_skills: agent_skills_typical_scenario_skills:
@ -121,9 +129,6 @@ agent_skills_targets:
paths: paths:
- "{{ agent_skills_home }}/.opencode/skills" - "{{ agent_skills_home }}/.opencode/skills"
- "{{ agent_skills_home }}/.config/opencode/skills" - "{{ agent_skills_home }}/.config/opencode/skills"
- name: hermers
paths:
- "{{ agent_skills_home }}/.hermers/skills"
- name: openclaw - name: openclaw
paths: paths:
- "{{ agent_skills_home }}/.openclaw/skills" - "{{ agent_skills_home }}/.openclaw/skills"

View File

@ -6,9 +6,10 @@
- agent_skills_group | length > 0 - agent_skills_group | length > 0
- agent_skills_home | length > 0 - agent_skills_home | length > 0
- agent_skills_local_source_dir | length > 0 - agent_skills_local_source_dir | length > 0
- (not agent_skills_xworkspace_core_enabled | bool) or agent_skills_xworkspace_core_source_dir | length > 0
- agent_skills_remote_dir | length > 0 - agent_skills_remote_dir | length > 0
- agent_skills_targets | length > 0 - agent_skills_targets | length > 0
fail_msg: "agent_skills_user, home, source, remote dir, and targets must be set." fail_msg: "agent_skills_user, home, source dirs, remote dir, and targets must be set."
- name: Build required agent skills list - name: Build required agent skills list
ansible.builtin.set_fact: ansible.builtin.set_fact:
@ -21,6 +22,7 @@
mode: "0755" mode: "0755"
delegate_to: localhost delegate_to: localhost
become: false become: false
check_mode: false
when: when:
- agent_skills_local_source_create | bool - agent_skills_local_source_create | bool
- agent_skills_auto_install_enabled | bool - agent_skills_auto_install_enabled | bool
@ -38,14 +40,52 @@
- agent_skills_local_source.stat.isdir | default(false) - agent_skills_local_source.stat.isdir | default(false)
fail_msg: "Local skills source directory does not exist: {{ agent_skills_local_source_dir }}" fail_msg: "Local skills source directory does not exist: {{ agent_skills_local_source_dir }}"
- name: Inspect xworkspace core skills source directory
ansible.builtin.stat:
path: "{{ agent_skills_xworkspace_core_source_dir }}"
register: agent_skills_xworkspace_core_source
delegate_to: localhost
become: false
when: agent_skills_xworkspace_core_enabled | bool
- name: Assert xworkspace core skills source directory exists
ansible.builtin.assert:
that:
- agent_skills_xworkspace_core_source.stat.isdir | default(false)
fail_msg: "xworkspace core skills source directory does not exist: {{ agent_skills_xworkspace_core_source_dir }}"
when:
- agent_skills_xworkspace_core_enabled | bool
- agent_skills_xworkspace_core_required | bool
- name: Build effective agent skills source directories
ansible.builtin.set_fact:
agent_skills_effective_source_dirs: >-
{{
[agent_skills_local_source_dir]
+ (
(
agent_skills_xworkspace_core_enabled | bool
and agent_skills_xworkspace_core_source.stat.isdir | default(false)
)
| ternary([agent_skills_xworkspace_core_source_dir], [])
)
}}
- name: Inspect required local scenario skills - name: Inspect required local scenario skills
ansible.builtin.shell: | ansible.builtin.shell: |
set -eu set -eu
for source_dir in {{ agent_skills_effective_source_dirs | map('quote') | join(' ') }}; do
for candidate in {{ ([item.name] + (item.aliases | default([]))) | unique | map('quote') | join(' ') }}; do for candidate in {{ ([item.name] + (item.aliases | default([]))) | unique | map('quote') | join(' ') }}; do
if [ -f "{{ agent_skills_local_source_dir }}/$candidate/SKILL.md" ]; then if [ -f "$source_dir/$candidate/SKILL.md" ]; then
printf '%s\n' "{{ agent_skills_local_source_dir }}/$candidate" printf '%s\n' "$source_dir/$candidate"
exit 0 exit 0
fi fi
match="$(find "$source_dir" -type f -path "*/$candidate/SKILL.md" -print -quit)"
if [ -n "$match" ]; then
dirname "$match"
exit 0
fi
done
done done
exit 1 exit 1
args: args:
@ -58,6 +98,7 @@
label: "{{ item.name }}" label: "{{ item.name }}"
delegate_to: localhost delegate_to: localhost
become: false become: false
check_mode: false
- name: Build missing scenario skills list - name: Build missing scenario skills list
ansible.builtin.set_fact: ansible.builtin.set_fact:
@ -116,11 +157,18 @@
- name: Reinspect required local scenario skills after auto install - name: Reinspect required local scenario skills after auto install
ansible.builtin.shell: | ansible.builtin.shell: |
set -eu set -eu
for source_dir in {{ agent_skills_effective_source_dirs | map('quote') | join(' ') }}; do
for candidate in {{ ([item.name] + (item.aliases | default([]))) | unique | map('quote') | join(' ') }}; do for candidate in {{ ([item.name] + (item.aliases | default([]))) | unique | map('quote') | join(' ') }}; do
if [ -f "{{ agent_skills_local_source_dir }}/$candidate/SKILL.md" ]; then if [ -f "$source_dir/$candidate/SKILL.md" ]; then
printf '%s\n' "{{ agent_skills_local_source_dir }}/$candidate" printf '%s\n' "$source_dir/$candidate"
exit 0 exit 0
fi fi
match="$(find "$source_dir" -type f -path "*/$candidate/SKILL.md" -print -quit)"
if [ -n "$match" ]; then
dirname "$match"
exit 0
fi
done
done done
exit 1 exit 1
args: args:
@ -134,6 +182,7 @@
delegate_to: localhost delegate_to: localhost
become: false become: false
when: agent_skills_auto_install_enabled | bool when: agent_skills_auto_install_enabled | bool
check_mode: false
- name: Build unresolved scenario skills list - name: Build unresolved scenario skills list
ansible.builtin.set_fact: ansible.builtin.set_fact:
@ -199,6 +248,7 @@
when: when:
- agent_skills_quality_gate_enabled | bool - agent_skills_quality_gate_enabled | bool
- agent_skills_resolved_local_paths | length > 0 - agent_skills_resolved_local_paths | length > 0
check_mode: false
- name: Detect local top-level symlink skills - name: Detect local top-level symlink skills
ansible.builtin.find: ansible.builtin.find:
@ -255,10 +305,15 @@
'--partial', '--partial',
'--timeout=' ~ (agent_skills_rsync_timeout | string) '--timeout=' ~ (agent_skills_rsync_timeout | string)
] ]
+ (['--dry-run'] if ansible_check_mode else [])
+ (['-z'] if (agent_skills_rsync_compress | bool) else []) + (['-z'] if (agent_skills_rsync_compress | bool) else [])
+ (['--delete'] if (agent_skills_delete_removed | bool) else []) + (['--delete'] if (agent_skills_delete_removed | bool and agent_skills_source_index == 0) else [])
+ (['--delete-excluded'] if (agent_skills_delete_removed | bool and agent_skills_source_index == 0) else [])
+ ( + (
(agent_skills_rsync_excludes + agent_skills_local_symlink_excludes) (
agent_skills_rsync_excludes
+ ((agent_skills_source_index == 0) | ternary(agent_skills_local_symlink_excludes, []))
)
| map('regex_replace', '^(.*)$', '--exclude=\1') | map('regex_replace', '^(.*)$', '--exclude=\1')
| list | list
) )
@ -266,7 +321,7 @@
+ [ + [
'-e', '-e',
'ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null', 'ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null',
agent_skills_local_source_dir ~ '/', item ~ '/',
( (
ansible_user | default(ansible_ssh_user) | default('root') ansible_user | default(ansible_ssh_user) | default('root')
) ~ '@' ~ ( ) ~ '@' ~ (
@ -276,6 +331,11 @@
}} }}
register: agent_skills_rsync_result register: agent_skills_rsync_result
changed_when: "'<<CHANGED>>' in agent_skills_rsync_result.stdout" changed_when: "'<<CHANGED>>' in agent_skills_rsync_result.stdout"
check_mode: false
loop: "{{ agent_skills_effective_source_dirs }}"
loop_control:
index_var: agent_skills_source_index
label: "{{ item }}"
delegate_to: localhost delegate_to: localhost
become: false become: false
@ -287,6 +347,47 @@
group: "{{ agent_skills_group }}" group: "{{ agent_skills_group }}"
recurse: true recurse: true
- name: Link nested categorized skills at canonical root
ansible.builtin.shell: |
set -eu
changed=0
while IFS= read -r skill_manifest; do
skill_dir="$(dirname "$skill_manifest")"
skill_name="$(basename "$skill_dir")"
link_path={{ agent_skills_remote_dir | quote }}/"$skill_name"
if [ -e "$link_path" ] && [ ! -L "$link_path" ]; then
continue
fi
current_target=""
if [ -L "$link_path" ]; then
current_target="$(readlink "$link_path")"
fi
if [ "$current_target" != "$skill_dir" ]; then
if [ "{{ ansible_check_mode | ternary('true', 'false') }}" != "true" ]; then
ln -sfn "$skill_dir" "$link_path"
fi
changed=1
fi
done < <(find {{ agent_skills_remote_dir | quote }} -mindepth 3 -name SKILL.md -type f -print)
if [ "$changed" = "1" ]; then
echo "<<CHANGED>>linked nested skills"
fi
args:
executable: /bin/bash
register: agent_skills_flatten_result
changed_when: "'<<CHANGED>>' in agent_skills_flatten_result.stdout"
check_mode: false
when: agent_skills_remote_flatten_nested_skills | bool
- name: Set canonical agent skills ownership after nested links
ansible.builtin.file:
path: "{{ agent_skills_remote_dir }}"
state: directory
owner: "{{ agent_skills_user }}"
group: "{{ agent_skills_group }}"
recurse: true
when: agent_skills_remote_flatten_nested_skills | bool
- name: Flatten agent skills target paths - name: Flatten agent skills target paths
ansible.builtin.set_fact: ansible.builtin.set_fact:
agent_skills_target_paths: "{{ agent_skills_targets | subelements('paths') | map('last') | list }}" agent_skills_target_paths: "{{ agent_skills_targets | subelements('paths') | map('last') | list }}"
@ -307,6 +408,7 @@
when: when:
- item.stat.exists | default(false) - item.stat.exists | default(false)
- not item.stat.islnk | default(false) - not item.stat.islnk | default(false)
- item.item not in agent_skills_preserve_existing_target_dirs
- not agent_skills_replace_existing_target_dirs | bool - not agent_skills_replace_existing_target_dirs | bool
- name: Replace existing non-link target directories when enabled - name: Replace existing non-link target directories when enabled
@ -317,6 +419,7 @@
when: when:
- item.stat.exists | default(false) - item.stat.exists | default(false)
- not item.stat.islnk | default(false) - not item.stat.islnk | default(false)
- item.item not in agent_skills_preserve_existing_target_dirs
- agent_skills_replace_existing_target_dirs | bool - agent_skills_replace_existing_target_dirs | bool
- name: Build agent skills target parent paths - name: Build agent skills target parent paths
@ -345,10 +448,9 @@
src: "{{ agent_skills_remote_dir }}" src: "{{ agent_skills_remote_dir }}"
dest: "{{ item }}" dest: "{{ item }}"
state: link state: link
owner: "{{ agent_skills_user }}"
group: "{{ agent_skills_group }}"
force: true force: true
loop: "{{ agent_skills_target_paths }}" loop: "{{ agent_skills_target_paths }}"
when: item not in agent_skills_preserve_existing_target_dirs
- name: Verify canonical skill manifests are present - name: Verify canonical skill manifests are present
ansible.builtin.find: ansible.builtin.find:

View File

@ -6,25 +6,32 @@ role entrypoint. The role installs:
- base tools: `curl`, `wget`, `git`, `jq`, `rsync`, `unzip` - base tools: `curl`, `wget`, `git`, `jq`, `rsync`, `unzip`
- Node.js runtime for Playwright-based agents - Node.js runtime for Playwright-based agents
- Python 3 toolchain for scripts and helpers - Python 3 toolchain for scripts and helpers
- system `chromium` browser - existing system browser, preferring the live `/usr/local/bin/chromium` wrapper
or Google Chrome before installing browser packages
- `pandoc` + XeLaTeX PDF toolchain - `pandoc` + XeLaTeX PDF toolchain
- Chinese fonts for document rendering - Chinese fonts for document rendering
- shared agent skills via `roles/agent_skills` - shared agent skills via `roles/agent_skills`, including the categorized
`../xworkspace-core-skills/skills/` repository source by default
Design constraints: Design constraints:
- system packages are the primary source of truth - system packages are the primary source of truth
- Playwright uses system `chromium` instead of downloading browsers - Playwright uses the resolved system browser instead of downloading browsers
- Chinese PDF rendering is treated as a runtime requirement, not an optional add-on - Chinese PDF rendering is treated as a runtime requirement, not an optional add-on
Default Playwright environment: Default Playwright environment:
- `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1` - `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1`
- `PLAYWRIGHT_BROWSERS_PATH=0` - `PLAYWRIGHT_BROWSERS_PATH=0`
- `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium` - `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/local/bin/chromium` when that live
wrapper exists
Example: Example:
```bash ```bash
ansible-playbook -i inventory.ini -l jp-xhttp-contabo.svc.plus setup-ai-agent-runtime.yml ansible-playbook -i inventory.ini -l jp-xhttp-contabo.svc.plus setup-ai-agent-skills.yml
``` ```
`setup-ai-agent-skills.yml` runs `roles/ai_agent_runtime`, which installs system
dependencies and syncs the current Skill catalog through the embedded
`roles/agent_skills` step in one pass.

View File

@ -9,7 +9,7 @@ ai_agent_runtime_base_packages:
- wget - wget
ai_agent_runtime_nodejs_enabled: true ai_agent_runtime_nodejs_enabled: true
ai_agent_runtime_nodejs_version: "22.x" ai_agent_runtime_nodejs_version: "24.x"
ai_agent_runtime_install_yarn: true ai_agent_runtime_install_yarn: true
ai_agent_runtime_yarn_version: "" ai_agent_runtime_yarn_version: ""
ai_agent_runtime_npm_global_packages: [] ai_agent_runtime_npm_global_packages: []
@ -29,8 +29,8 @@ ai_agent_runtime_python_packages:
ai_agent_runtime_browser_enabled: true ai_agent_runtime_browser_enabled: true
ai_agent_runtime_browser_packages: ai_agent_runtime_browser_packages:
- chromium - google-chrome-stable
ai_agent_runtime_browser_executable: /usr/bin/chromium ai_agent_runtime_browser_executable: /usr/local/bin/chromium
ai_agent_runtime_docs_enabled: true ai_agent_runtime_docs_enabled: true
ai_agent_runtime_doc_packages: ai_agent_runtime_doc_packages:

View File

@ -1,26 +1,18 @@
--- ---
- name: Install AI runtime browser packages - name: Resolve existing Chromium executable
ansible.builtin.apt:
name: "{{ ai_agent_runtime_browser_packages }}"
state: present
update_cache: true
install_recommends: false
environment:
DEBIAN_FRONTEND: noninteractive
APT_LISTCHANGES_FRONTEND: none
become: true
- name: Resolve Chromium executable
ansible.builtin.shell: | ansible.builtin.shell: |
set -eu set -eu
for candidate in \ for candidate in \
"{{ ai_agent_runtime_browser_executable }}" \ "{{ ai_agent_runtime_browser_executable }}" \
/usr/local/bin/chromium \
/usr/local/bin/chromium-browser \
/usr/bin/google-chrome \
/usr/bin/google-chrome-stable \
chromium \ chromium \
chromium-browser \ chromium-browser \
google-chrome \ google-chrome \
google-chrome-stable \ google-chrome-stable \
/usr/bin/chromium \ /usr/bin/chromium \
/usr/local/bin/chromium \
/snap/bin/chromium; do /snap/bin/chromium; do
if command -v "$candidate" >/dev/null 2>&1; then if command -v "$candidate" >/dev/null 2>&1; then
command -v "$candidate" command -v "$candidate"
@ -36,6 +28,51 @@
executable: /bin/sh executable: /bin/sh
register: ai_agent_runtime_browser_resolve register: ai_agent_runtime_browser_resolve
changed_when: false changed_when: false
failed_when: false
check_mode: false
- name: Install AI runtime browser packages when no browser exists
ansible.builtin.apt:
name: "{{ ai_agent_runtime_browser_packages }}"
state: present
update_cache: true
install_recommends: false
environment:
DEBIAN_FRONTEND: noninteractive
APT_LISTCHANGES_FRONTEND: none
become: true
when: ai_agent_runtime_browser_resolve.rc != 0
- name: Resolve Chromium executable
ansible.builtin.shell: |
set -eu
for candidate in \
"{{ ai_agent_runtime_browser_executable }}" \
/usr/local/bin/chromium \
/usr/local/bin/chromium-browser \
/usr/bin/google-chrome \
/usr/bin/google-chrome-stable \
chromium \
chromium-browser \
google-chrome \
google-chrome-stable \
/usr/bin/chromium \
/snap/bin/chromium; do
if command -v "$candidate" >/dev/null 2>&1; then
command -v "$candidate"
exit 0
fi
if [ -x "$candidate" ]; then
printf '%s\n' "$candidate"
exit 0
fi
done
exit 1
args:
executable: /bin/sh
register: ai_agent_runtime_browser_resolve
changed_when: false
check_mode: false
- name: Set resolved Chromium executable - name: Set resolved Chromium executable
ansible.builtin.set_fact: ansible.builtin.set_fact:

View File

@ -39,7 +39,10 @@
- name: Configure shared agent skills - name: Configure shared agent skills
ansible.builtin.include_role: ansible.builtin.include_role:
name: "{{ ai_agent_runtime_skills_role_name }}" name: "{{ ai_agent_runtime_skills_role_name }}"
apply:
tags: agent_skills
when: ai_agent_runtime_skills_enabled | bool when: ai_agent_runtime_skills_enabled | bool
tags: [agent_skills]
- name: Verify AI agent runtime - name: Verify AI agent runtime
ansible.builtin.include_tasks: verify.yml ansible.builtin.include_tasks: verify.yml

View File

@ -3,48 +3,56 @@
ansible.builtin.command: node --version ansible.builtin.command: node --version
register: ai_agent_runtime_node_version register: ai_agent_runtime_node_version
changed_when: false changed_when: false
check_mode: false
when: ai_agent_runtime_nodejs_enabled | bool when: ai_agent_runtime_nodejs_enabled | bool
- name: Check npm version - name: Check npm version
ansible.builtin.command: npm --version ansible.builtin.command: npm --version
register: ai_agent_runtime_npm_version register: ai_agent_runtime_npm_version
changed_when: false changed_when: false
check_mode: false
when: ai_agent_runtime_nodejs_enabled | bool when: ai_agent_runtime_nodejs_enabled | bool
- name: Check python version - name: Check python version
ansible.builtin.command: python3 --version ansible.builtin.command: python3 --version
register: ai_agent_runtime_python_version register: ai_agent_runtime_python_version
changed_when: false changed_when: false
check_mode: false
when: ai_agent_runtime_python_enabled | bool when: ai_agent_runtime_python_enabled | bool
- name: Check pip version - name: Check pip version
ansible.builtin.command: pip3 --version ansible.builtin.command: pip3 --version
register: ai_agent_runtime_pip_version register: ai_agent_runtime_pip_version
changed_when: false changed_when: false
check_mode: false
when: ai_agent_runtime_python_enabled | bool when: ai_agent_runtime_python_enabled | bool
- name: Check chromium version - name: Check chromium version
ansible.builtin.command: "{{ ai_agent_runtime_browser_resolved_executable | default(ai_agent_runtime_browser_executable) }} --version" ansible.builtin.command: "{{ ai_agent_runtime_browser_resolved_executable | default(ai_agent_runtime_browser_executable) }} --version"
register: ai_agent_runtime_chromium_version register: ai_agent_runtime_chromium_version
changed_when: false changed_when: false
check_mode: false
when: ai_agent_runtime_browser_enabled | bool when: ai_agent_runtime_browser_enabled | bool
- name: Check pandoc version - name: Check pandoc version
ansible.builtin.command: pandoc --version ansible.builtin.command: pandoc --version
register: ai_agent_runtime_pandoc_version register: ai_agent_runtime_pandoc_version
changed_when: false changed_when: false
check_mode: false
when: ai_agent_runtime_docs_enabled | bool when: ai_agent_runtime_docs_enabled | bool
- name: Check xelatex version - name: Check xelatex version
ansible.builtin.command: xelatex --version ansible.builtin.command: xelatex --version
register: ai_agent_runtime_xelatex_version register: ai_agent_runtime_xelatex_version
changed_when: false changed_when: false
check_mode: false
when: ai_agent_runtime_docs_enabled | bool when: ai_agent_runtime_docs_enabled | bool
- name: Check Chinese font inventory - name: Check Chinese font inventory
ansible.builtin.command: fc-list :lang=zh family ansible.builtin.command: fc-list :lang=zh family
register: ai_agent_runtime_chinese_fonts register: ai_agent_runtime_chinese_fonts
changed_when: false changed_when: false
check_mode: false
when: when:
- ai_agent_runtime_fonts_enabled | bool - ai_agent_runtime_fonts_enabled | bool
- ai_agent_runtime_verify_chinese_fonts | bool - ai_agent_runtime_verify_chinese_fonts | bool

View File

@ -25,6 +25,7 @@
register: acp_codex_bridge_binary_attrs register: acp_codex_bridge_binary_attrs
changed_when: false changed_when: false
failed_when: false failed_when: false
check_mode: false
- name: Remove immutable flag from Codex bridge binary when present - name: Remove immutable flag from Codex bridge binary when present
ansible.builtin.command: ansible.builtin.command:
@ -74,6 +75,7 @@
register: acp_codex_service_attrs register: acp_codex_service_attrs
changed_when: false changed_when: false
failed_when: false failed_when: false
check_mode: false
- name: Remove immutable flag from Codex ACP systemd service when present - name: Remove immutable flag from Codex ACP systemd service when present
ansible.builtin.command: ansible.builtin.command:

View File

@ -26,6 +26,7 @@
register: acp_hermes_bridge_binary_attrs register: acp_hermes_bridge_binary_attrs
changed_when: false changed_when: false
failed_when: false failed_when: false
check_mode: false
- name: Remove immutable flag from Hermes bridge binary when present - name: Remove immutable flag from Hermes bridge binary when present
ansible.builtin.command: ansible.builtin.command:
@ -58,6 +59,7 @@
register: acp_hermes_service_attrs register: acp_hermes_service_attrs
changed_when: false changed_when: false
failed_when: false failed_when: false
check_mode: false
- name: Remove immutable flag from Hermes ACP systemd service when present - name: Remove immutable flag from Hermes ACP systemd service when present
ansible.builtin.command: ansible.builtin.command:
@ -80,6 +82,7 @@
changed_when: false changed_when: false
failed_when: false failed_when: false
no_log: true no_log: true
check_mode: false
- name: Resolve Hermes ACP auth token - name: Resolve Hermes ACP auth token
ansible.builtin.set_fact: ansible.builtin.set_fact:

View File

@ -44,7 +44,17 @@ gateway_openclaw_acp_max_concurrent_sessions: 2
gateway_openclaw_acp_backend: acpx gateway_openclaw_acp_backend: acpx
gateway_openclaw_acp_default_agent: codex gateway_openclaw_acp_default_agent: codex
gateway_openclaw_codex_app_server_url: ws://127.0.0.1:9001 gateway_openclaw_codex_app_server_url: ws://127.0.0.1:9001
gateway_openclaw_default_model: deepseek/deepseek-v4-flash gateway_openclaw_default_model:
primary: nvidia/nemotron-3-super-120b-a12b
fallbacks:
- nvidia/minimaxai/minimax-m2.5
- nvidia/z-ai/glm5
gateway_openclaw_default_models:
nvidia/nemotron-3-super-120b-a12b: {}
nvidia/minimaxai/minimax-m2.5: {}
nvidia/z-ai/glm5: {}
openai-codex/gpt-5.5: {}
gateway_openclaw_main_agent_model: nvidia/nemotron-3-super-120b-a12b
gateway_openclaw_main_agent_skills: gateway_openclaw_main_agent_skills:
- acp-router - acp-router
@ -76,3 +86,75 @@ gateway_openclaw_main_agent_skills:
- video-translator - video-translator
- web-search - web-search
- self-improving - self-improving
- ai-tech-news-video
- it-infra-continuous-png
- it-infra-evolution-video
- product-intro-video
- sound-fx-for-video
- sketch-animation-video
- skylv-hermes-agent-integration
- hermes-agent-integration
- qmd
gateway_openclaw_mcp_servers:
qmd:
url: http://localhost:8181/mcp
transport: streamable-http
gateway_openclaw_model_providers:
nvidia:
api: openai-completions
baseUrl: https://integrate.api.nvidia.com/v1
models:
- id: nvidia/nemotron-3-super-120b-a12b
name: NVIDIA Nemotron 3 Super 120B
input: [text]
contextWindow: 262144
maxTokens: 8192
reasoning: false
compat:
requiresStringContent: true
cost:
input: 0
output: 0
cacheRead: 0
cacheWrite: 0
- id: moonshotai/kimi-k2.5
name: Kimi K2.5
input: [text]
contextWindow: 262144
maxTokens: 8192
reasoning: false
compat:
requiresStringContent: true
cost:
input: 0
output: 0
cacheRead: 0
cacheWrite: 0
- id: minimaxai/minimax-m2.5
name: MiniMax M2.5
input: [text]
contextWindow: 196608
maxTokens: 8192
reasoning: false
compat:
requiresStringContent: true
cost:
input: 0
output: 0
cacheRead: 0
cacheWrite: 0
- id: z-ai/glm5
name: GLM-5
input: [text]
contextWindow: 202752
maxTokens: 8192
reasoning: false
compat:
requiresStringContent: true
cost:
input: 0
output: 0
cacheRead: 0
cacheWrite: 0

View File

@ -26,11 +26,13 @@
"bootstrapMaxChars": 50000, "bootstrapMaxChars": 50000,
"bootstrapTotalMaxChars": 300000, "bootstrapTotalMaxChars": 300000,
"model": {{ gateway_openclaw_default_model | to_json }}, "model": {{ gateway_openclaw_default_model | to_json }},
"models": {{ gateway_openclaw_default_models | to_json }},
"thinkingDefault": "low" "thinkingDefault": "low"
}, },
"list": [ "list": [
{ {
"id": "main", "id": "main",
"model": {{ gateway_openclaw_main_agent_model | to_json }},
"skills": {{ gateway_openclaw_main_agent_skills | unique | list | to_json }} "skills": {{ gateway_openclaw_main_agent_skills | unique | list | to_json }}
} }
] ]
@ -85,7 +87,7 @@
}, },
"models": { "models": {
"mode": "merge", "mode": "merge",
"providers": {} "providers": {{ gateway_openclaw_model_providers | to_json }}
}, },
"wizard": { "wizard": {
"lastRunAt": "2026-04-19T10:52:37.655Z", "lastRunAt": "2026-04-19T10:52:37.655Z",
@ -106,6 +108,9 @@
"defaultAgent": {{ gateway_openclaw_acp_default_agent | to_json }}, "defaultAgent": {{ gateway_openclaw_acp_default_agent | to_json }},
"maxConcurrentSessions": {{ gateway_openclaw_acp_max_concurrent_sessions | int }} "maxConcurrentSessions": {{ gateway_openclaw_acp_max_concurrent_sessions | int }}
}, },
"mcp": {
"servers": {{ gateway_openclaw_mcp_servers | to_json }}
},
"plugins": { "plugins": {
"entries": { "entries": {
"nvidia": {"enabled": true}, "nvidia": {"enabled": true},
@ -119,9 +124,14 @@
"appServer": { "appServer": {
"transport": "websocket", "transport": "websocket",
"url": {{ gateway_openclaw_codex_app_server_url | to_json }} "url": {{ gateway_openclaw_codex_app_server_url | to_json }}
},
"discovery": {
"enabled": true
} }
} }
}, },
"memory-wiki": {"enabled": true},
"openai": {"enabled": true},
"openclaw-multi-session-plugins": {"enabled": true}, "openclaw-multi-session-plugins": {"enabled": true},
"device-pair": {"enabled": false}, "device-pair": {"enabled": false},
"phone-control": {"enabled": false}, "phone-control": {"enabled": false},

View File

@ -4,6 +4,7 @@
register: node_version_check register: node_version_check
changed_when: false changed_when: false
failed_when: false failed_when: false
check_mode: false
- name: Get Node.js version number - name: Get Node.js version number
set_fact: set_fact:
@ -83,12 +84,14 @@
register: npm_version_check register: npm_version_check
changed_when: false changed_when: false
failed_when: false failed_when: false
check_mode: false
- name: Get current Yarn version - name: Get current Yarn version
command: yarn --version command: yarn --version
register: yarn_version_check register: yarn_version_check
changed_when: false changed_when: false
failed_when: false failed_when: false
check_mode: false
when: install_yarn | default(true) when: install_yarn | default(true)
- name: Normalize desired Yarn version - name: Normalize desired Yarn version

View File

@ -0,0 +1,26 @@
---
qmd_user: ubuntu
qmd_group: "{{ qmd_user }}"
qmd_home: "/home/{{ qmd_user }}"
qmd_binary_path: "{{ qmd_home }}/.bun/bin/qmd"
qmd_config_dir: "{{ qmd_home }}/.config/qmd"
qmd_cache_dir: "{{ qmd_home }}/.cache/qmd"
qmd_config_dir_mode: "0775"
qmd_cache_dir_mode: "0775"
qmd_index_config_path: "{{ qmd_config_dir }}/index.yml"
qmd_index_config_mode: "0664"
qmd_env_path: "{{ qmd_config_dir }}/qmd.env"
qmd_mcp_service_name: qmd-mcp
qmd_mcp_service_unit_path: "{{ qmd_home }}/.config/systemd/user/{{ qmd_mcp_service_name }}.service"
qmd_service_uid: "1000"
qmd_mcp_host: 127.0.0.1
qmd_mcp_port: 8181
qmd_mcp_url: "http://localhost:{{ qmd_mcp_port }}/mcp"
qmd_embed_api_base_url: https://integrate.api.nvidia.com/v1
qmd_embed_model: nvidia/llama-nemotron-embed-1b-v2
qmd_collections:
openclaw-workspace:
path: "{{ qmd_home }}/.openclaw/workspace"
pattern: "**/*.md"
context:
"": OpenClaw workspace long-term memory, daily notes, user context, local tools notes, generated plans and markdown artifacts.

View File

@ -0,0 +1,139 @@
---
- name: Assert QMD is only supported on Debian family
ansible.builtin.assert:
that:
- ansible_facts.os_family == "Debian"
fail_msg: "roles/vhosts/qmd currently supports Debian-based hosts only."
- name: Ensure QMD config and cache directories exist
ansible.builtin.file:
path: "{{ item.path }}"
state: directory
owner: "{{ qmd_user }}"
group: "{{ qmd_group }}"
mode: "{{ item.mode }}"
loop:
- path: "{{ qmd_config_dir }}"
mode: "{{ qmd_config_dir_mode }}"
- path: "{{ qmd_cache_dir }}"
mode: "{{ qmd_cache_dir_mode }}"
- path: "{{ qmd_mcp_service_unit_path | dirname }}"
mode: "0755"
- name: Deploy QMD collection index config
ansible.builtin.template:
src: index.yml.j2
dest: "{{ qmd_index_config_path }}"
owner: "{{ qmd_user }}"
group: "{{ qmd_group }}"
mode: "{{ qmd_index_config_mode }}"
- name: Deploy QMD external embedding environment
ansible.builtin.template:
src: qmd.env.j2
dest: "{{ qmd_env_path }}"
owner: "{{ qmd_user }}"
group: "{{ qmd_group }}"
mode: "0600"
diff: false
- name: Inspect QMD binary
ansible.builtin.stat:
path: "{{ qmd_binary_path }}"
register: qmd_binary
- name: Fail when QMD binary is missing or not executable
ansible.builtin.assert:
that:
- qmd_binary.stat.exists | default(false)
- qmd_binary.stat.executable | default(false)
fail_msg: "QMD binary is missing or not executable: {{ qmd_binary_path }}"
- name: Deploy QMD MCP user systemd unit
ansible.builtin.template:
src: qmd-mcp.user.service.j2
dest: "{{ qmd_mcp_service_unit_path }}"
owner: "{{ qmd_user }}"
group: "{{ qmd_group }}"
mode: "0644"
register: qmd_mcp_user_service_unit
- name: Enable QMD service user linger
ansible.builtin.command:
cmd: "loginctl enable-linger {{ qmd_user }}"
creates: "/var/lib/systemd/linger/{{ qmd_user }}"
when:
- not ansible_check_mode
- name: Ensure QMD service user manager is running
ansible.builtin.systemd:
name: "user@{{ qmd_service_uid }}.service"
state: started
when:
- not ansible_check_mode
- name: Reload QMD user systemd manager
ansible.builtin.command:
cmd: systemctl --user daemon-reload
environment:
HOME: "{{ qmd_home }}"
XDG_RUNTIME_DIR: "/run/user/{{ qmd_service_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ qmd_service_uid }}/bus"
become: true
become_user: "{{ qmd_user }}"
changed_when: false
when:
- not ansible_check_mode
- name: Ensure QMD MCP daemon is enabled and running
ansible.builtin.command:
cmd: >-
systemctl --user enable
{{ '--now' if not (qmd_mcp_user_service_unit.changed | default(false)) else '' }}
{{ qmd_mcp_service_name }}.service
environment:
HOME: "{{ qmd_home }}"
XDG_RUNTIME_DIR: "/run/user/{{ qmd_service_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ qmd_service_uid }}/bus"
become: true
become_user: "{{ qmd_user }}"
register: qmd_mcp_service_enable
changed_when: >-
'Created symlink' in (qmd_mcp_service_enable.stdout | default('')) or
'Created symlink' in (qmd_mcp_service_enable.stderr | default(''))
when:
- not ansible_check_mode
- name: Restart QMD MCP daemon after unit changes
ansible.builtin.command:
cmd: "systemctl --user restart {{ qmd_mcp_service_name }}.service"
environment:
HOME: "{{ qmd_home }}"
XDG_RUNTIME_DIR: "/run/user/{{ qmd_service_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ qmd_service_uid }}/bus"
become: true
become_user: "{{ qmd_user }}"
when:
- qmd_mcp_user_service_unit.changed | default(false)
- not ansible_check_mode
- name: Validate QMD status
ansible.builtin.command:
cmd: "{{ qmd_binary_path }} status"
environment:
HOME: "{{ qmd_home }}"
QMD_EMBED_API_BASE_URL: "{{ qmd_embed_api_base_url }}"
QMD_EMBED_MODEL: "{{ qmd_embed_model }}"
become: true
become_user: "{{ qmd_user }}"
register: qmd_status
changed_when: false
check_mode: false
- name: Show QMD validation summary
ansible.builtin.debug:
msg:
- "QMD MCP URL: {{ qmd_mcp_url }}"
- "QMD index config: {{ qmd_index_config_path }}"
- "QMD embedding model: {{ qmd_embed_model }}"
- "{{ qmd_status.stdout | default('') }}"

View File

@ -0,0 +1,12 @@
collections:
{% for name, collection in qmd_collections | dictsort %}
{{ name }}:
path: {{ collection.path }}
pattern: {{ collection.pattern | to_json }}
{% if collection.context is defined %}
context:
{% for path, text in collection.context | dictsort %}
{{ path | to_json }}: {{ text }}
{% endfor %}
{% endif %}
{% endfor %}

View File

@ -0,0 +1,18 @@
[Unit]
Description=QMD MCP daemon
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory={{ qmd_home }}
Environment=HOME={{ qmd_home }}
Environment=PATH={{ qmd_home }}/.bun/bin:{{ qmd_home }}/.local/bin:/usr/local/bin:/usr/bin:/bin
EnvironmentFile={{ qmd_env_path }}
ExecStart={{ qmd_binary_path }} mcp --http --port {{ qmd_mcp_port }}
Restart=always
RestartSec=5
[Install]
WantedBy=default.target

View File

@ -0,0 +1,3 @@
QMD_EMBED_API_BASE_URL={{ qmd_embed_api_base_url }}
QMD_EMBED_MODEL={{ qmd_embed_model }}

View File

@ -21,6 +21,7 @@ xfce_manage_user: false
xfce_user_groups: [] xfce_user_groups: []
xfce_user_shell: /bin/bash xfce_user_shell: /bin/bash
xfce_user_password_plaintext: "" xfce_user_password_plaintext: ""
xfce_user_update_password: on_create
xfce_google_chrome_version: "148.0.7778.167-1" xfce_google_chrome_version: "148.0.7778.167-1"
xfce_google_chrome_apt_key_url: "https://dl.google.com/linux/linux_signing_key.pub" xfce_google_chrome_apt_key_url: "https://dl.google.com/linux/linux_signing_key.pub"

View File

@ -12,6 +12,8 @@
- snapd.apparmor.service - snapd.apparmor.service
become: true become: true
failed_when: false failed_when: false
when:
- not ansible_check_mode
- name: Block snap and snap-backed browser transitional packages - name: Block snap and snap-backed browser transitional packages
ansible.builtin.copy: ansible.builtin.copy:
@ -120,14 +122,16 @@
mode: "0755" mode: "0755"
become: true become: true
- name: Keep Chromium compatibility commands pointed at Chrome deb - name: Keep Chromium compatibility commands disabled
ansible.builtin.file: ansible.builtin.copy:
src: /usr/local/bin/chromium-xrdp content: |
#!/bin/sh
echo "Chromium is disabled on this host. Use google-chrome instead." >&2
exit 126
dest: "{{ item }}" dest: "{{ item }}"
state: link
force: true
owner: root owner: root
group: root group: root
mode: "0755"
loop: loop:
- /usr/local/bin/chromium - /usr/local/bin/chromium
- /usr/local/bin/chromium-browser - /usr/local/bin/chromium-browser
@ -162,6 +166,8 @@
environment: environment:
HOME: "{{ xfce_user_home }}" HOME: "{{ xfce_user_home }}"
changed_when: false changed_when: false
when:
- not ansible_check_mode
- name: Set xdg default web browser to Google Chrome XRDP desktop entry - name: Set xdg default web browser to Google Chrome XRDP desktop entry
ansible.builtin.command: "xdg-settings set default-web-browser {{ xfce_google_chrome_desktop_file }}" ansible.builtin.command: "xdg-settings set default-web-browser {{ xfce_google_chrome_desktop_file }}"
@ -170,6 +176,8 @@
environment: environment:
HOME: "{{ xfce_user_home }}" HOME: "{{ xfce_user_home }}"
changed_when: false changed_when: false
when:
- not ansible_check_mode
- name: Set system browser alternatives to Google Chrome deb - name: Set system browser alternatives to Google Chrome deb
ansible.builtin.command: "update-alternatives --set {{ item }} /usr/bin/google-chrome-stable" ansible.builtin.command: "update-alternatives --set {{ item }} /usr/bin/google-chrome-stable"
@ -178,12 +186,15 @@
- gnome-www-browser - gnome-www-browser
become: true become: true
changed_when: false changed_when: false
when:
- not ansible_check_mode
- name: Verify Google Chrome deb browser installation - name: Verify Google Chrome deb browser installation
ansible.builtin.command: /usr/local/bin/chromium-xrdp --version ansible.builtin.command: /usr/local/bin/chromium-xrdp --version
register: xfce_google_chrome_version_check register: xfce_google_chrome_version_check
changed_when: false changed_when: false
become: true become: true
check_mode: false
- name: Show Google Chrome deb browser version - name: Show Google Chrome deb browser version
ansible.builtin.debug: ansible.builtin.debug:

View File

@ -22,11 +22,13 @@
ansible.builtin.user: ansible.builtin.user:
name: "{{ xfce_user }}" name: "{{ xfce_user }}"
password: "{{ xfce_user_password_plaintext | password_hash('sha512') }}" password: "{{ xfce_user_password_plaintext | password_hash('sha512') }}"
update_password: always update_password: "{{ xfce_user_update_password }}"
password_lock: false password_lock: false
become: true become: true
no_log: true no_log: true
when: xfce_manage_user | bool when:
- xfce_manage_user | bool
- not ansible_check_mode
- name: Ensure the desktop user can sudo - name: Ensure the desktop user can sudo
ansible.builtin.user: ansible.builtin.user:

View File

@ -2,6 +2,7 @@
xworkmate_bridge_service_name: xworkmate-bridge xworkmate_bridge_service_name: xworkmate-bridge
xworkmate_bridge_service_user: ubuntu xworkmate_bridge_service_user: ubuntu
xworkmate_bridge_service_group: ubuntu xworkmate_bridge_service_group: ubuntu
xworkmate_bridge_service_home: "/home/{{ xworkmate_bridge_service_user }}"
xworkmate_bridge_auth_token: "{{ lookup('ansible.builtin.env', 'BRIDGE_AUTH_TOKEN') | default(lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN') | default('', true), true) }}" xworkmate_bridge_auth_token: "{{ lookup('ansible.builtin.env', 'BRIDGE_AUTH_TOKEN') | default(lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN') | default('', true), true) }}"
xworkmate_bridge_listen_host: 127.0.0.1 xworkmate_bridge_listen_host: 127.0.0.1
xworkmate_bridge_listen_port: 8787 xworkmate_bridge_listen_port: 8787

View File

@ -1,7 +1,11 @@
[Unit] [Unit]
Description=XWorkmate bridge control plane Description=XWorkmate bridge control plane
{% if xworkmate_bridge_required_services | length > 0 %}
Requires={{ xworkmate_bridge_required_services | join(' ') }} Requires={{ xworkmate_bridge_required_services | join(' ') }}
After=network-online.target {{ xworkmate_bridge_required_services | join(' ') }} After=network-online.target {{ xworkmate_bridge_required_services | join(' ') }}
{% else %}
After=network-online.target
{% endif %}
Wants=network-online.target Wants=network-online.target
[Service] [Service]
@ -9,7 +13,7 @@ Type=simple
User={{ xworkmate_bridge_service_user }} User={{ xworkmate_bridge_service_user }}
Group={{ xworkmate_bridge_service_group }} Group={{ xworkmate_bridge_service_group }}
WorkingDirectory={{ xworkmate_bridge_base_dir }} WorkingDirectory={{ xworkmate_bridge_base_dir }}
Environment="HOME=/home/{{ xworkmate_bridge_service_user }}" Environment="HOME={{ xworkmate_bridge_service_home }}"
Environment="TERM=xterm-256color" Environment="TERM=xterm-256color"
{% for key, value in xworkmate_bridge_service_environment | dictsort %} {% for key, value in xworkmate_bridge_service_environment | dictsort %}
{% if value | string | trim | length > 0 %} {% if value | string | trim | length > 0 %}

243
scripts/sync-yitu-it-series-r2.sh Executable file
View File

@ -0,0 +1,243 @@
#!/usr/bin/env bash
set -euo pipefail
LOCAL_SRC_DEFAULT="/Users/shenlan/Library/CloudStorage/GoogleDrive-haitaopanhq@gmail.com/我的云端硬盘/自媒体"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PLAYBOOKS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
LOCAL_SRC="${LOCAL_SRC:-$LOCAL_SRC_DEFAULT}"
R2_BUCKET="${R2_BUCKET:-yitu-it-series}"
R2_REMOTE="${R2_REMOTE:-cloudflare-r2}"
R2_CUSTOM_DOMAIN="${R2_CUSTOM_DOMAIN:-img.svc.plus}"
RCLONE_BIN="${RCLONE_BIN:-rclone}"
WRANGLER_BIN="${WRANGLER_BIN:-npx --yes wrangler@latest}"
LOG_FILE="${LOG_FILE:-$HOME/rclone-yitu-it-series.log}"
SYNC_FLAGS=(
--progress
--stats 30s
--transfers "${RCLONE_TRANSFERS:-16}"
--checkers "${RCLONE_CHECKERS:-32}"
--fast-list
--s3-upload-cutoff "${RCLONE_S3_UPLOAD_CUTOFF:-128M}"
--s3-chunk-size "${RCLONE_S3_CHUNK_SIZE:-128M}"
--retries "${RCLONE_RETRIES:-8}"
--low-level-retries "${RCLONE_LOW_LEVEL_RETRIES:-20}"
--timeout "${RCLONE_TIMEOUT:-5m}"
--contimeout "${RCLONE_CONTIMEOUT:-30s}"
--exclude ".DS_Store"
--exclude "Icon?"
--exclude "._*"
--exclude ".Spotlight-V100/**"
--exclude ".Trashes/**"
--log-file "$LOG_FILE"
--log-level "${RCLONE_LOG_LEVEL:-INFO}"
)
usage() {
cat <<'EOF'
Usage:
sync-yitu-it-series-r2.sh doctor
sync-yitu-it-series-r2.sh create-bucket
sync-yitu-it-series-r2.sh configure-rclone
sync-yitu-it-series-r2.sh dry-run
sync-yitu-it-series-r2.sh sync
sync-yitu-it-series-r2.sh copy
sync-yitu-it-series-r2.sh check
sync-yitu-it-series-r2.sh tree
sync-yitu-it-series-r2.sh configure-custom-domain
sync-yitu-it-series-r2.sh install-launchd
sync-yitu-it-series-r2.sh uninstall-launchd
Required for configure-rclone:
CF_ACCOUNT_ID or CLOUDFLARE_ACCOUNT_ID
R2_ACCESS_KEY_ID
R2_SECRET_ACCESS_KEY
Required for create-bucket:
CLOUDFLARE_API_TOKEN or an active Wrangler login
Required for configure-custom-domain:
CF_ACCOUNT_ID or CLOUDFLARE_ACCOUNT_ID
CF_ZONE_ID or CLOUDFLARE_ZONE_ID
CLOUDFLARE_API_TOKEN
EOF
}
require_cmd() {
local cmd="$1"
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "Missing command: $cmd" >&2
exit 1
fi
}
account_id() {
printf '%s' "${CF_ACCOUNT_ID:-${CLOUDFLARE_ACCOUNT_ID:-}}"
}
zone_id() {
printf '%s' "${CF_ZONE_ID:-${CLOUDFLARE_ZONE_ID:-}}"
}
require_env() {
local name="$1"
if [[ -z "${!name:-}" ]]; then
echo "Missing environment variable: $name" >&2
exit 1
fi
}
require_account_id() {
if [[ -z "$(account_id)" ]]; then
echo "Missing CF_ACCOUNT_ID or CLOUDFLARE_ACCOUNT_ID" >&2
exit 1
fi
}
remote_endpoint() {
require_account_id
printf 'https://%s.r2.cloudflarestorage.com' "$(account_id)"
}
doctor() {
require_cmd "$RCLONE_BIN"
echo "Local source: $LOCAL_SRC"
test -d "$LOCAL_SRC"
"$RCLONE_BIN" version | sed -n '1,8p'
"$RCLONE_BIN" size "$LOCAL_SRC" \
--exclude ".DS_Store" \
--exclude "Icon?" \
--exclude "._*"
echo
echo "Rclone remotes:"
"$RCLONE_BIN" listremotes || true
echo
echo "Target: ${R2_REMOTE}:${R2_BUCKET}/"
}
create_bucket() {
require_account_id
if [[ -n "${CLOUDFLARE_API_TOKEN:-}" ]]; then
CLOUDFLARE_ACCOUNT_ID="$(account_id)" CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" \
$WRANGLER_BIN r2 bucket create "$R2_BUCKET"
else
CLOUDFLARE_ACCOUNT_ID="$(account_id)" $WRANGLER_BIN r2 bucket create "$R2_BUCKET"
fi
}
configure_rclone() {
require_account_id
require_env R2_ACCESS_KEY_ID
require_env R2_SECRET_ACCESS_KEY
"$RCLONE_BIN" config create "$R2_REMOTE" s3 \
provider Cloudflare \
access_key_id "$R2_ACCESS_KEY_ID" \
secret_access_key "$R2_SECRET_ACCESS_KEY" \
endpoint "$(remote_endpoint)" \
region auto \
acl private \
no_check_bucket true
"$RCLONE_BIN" lsd "${R2_REMOTE}:"
}
sync_dry_run() {
"$RCLONE_BIN" sync "$LOCAL_SRC" "${R2_REMOTE}:${R2_BUCKET}/" --dry-run "${SYNC_FLAGS[@]}"
}
sync_run() {
"$RCLONE_BIN" sync "$LOCAL_SRC" "${R2_REMOTE}:${R2_BUCKET}/" "${SYNC_FLAGS[@]}"
}
copy_run() {
"$RCLONE_BIN" copy "$LOCAL_SRC" "${R2_REMOTE}:${R2_BUCKET}/" "${SYNC_FLAGS[@]}"
}
check_run() {
"$RCLONE_BIN" check "$LOCAL_SRC" "${R2_REMOTE}:${R2_BUCKET}/" \
--one-way \
--size-only \
--exclude ".DS_Store" \
--exclude "Icon?" \
--exclude "._*"
}
tree_run() {
"$RCLONE_BIN" tree "${R2_REMOTE}:${R2_BUCKET}/" --max-depth "${TREE_MAX_DEPTH:-2}"
}
configure_custom_domain() {
require_account_id
require_env CLOUDFLARE_API_TOKEN
local zid
zid="$(zone_id)"
if [[ -z "$zid" ]]; then
echo "Missing CF_ZONE_ID or CLOUDFLARE_ZONE_ID" >&2
exit 1
fi
curl -fsS "https://api.cloudflare.com/client/v4/accounts/$(account_id)/r2/buckets/${R2_BUCKET}/domains/custom" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
-d "{
\"domain\": \"${R2_CUSTOM_DOMAIN}\",
\"enabled\": true,
\"zoneId\": \"${zid}\",
\"minTLS\": \"1.2\"
}" | jq .
}
install_launchd() {
local plist="$HOME/Library/LaunchAgents/plus.svc.yitu-it-series.rclone-sync.plist"
mkdir -p "$HOME/Library/LaunchAgents"
cat > "$plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>plus.svc.yitu-it-series.rclone-sync</string>
<key>ProgramArguments</key>
<array>
<string>$SCRIPT_DIR/sync-yitu-it-series-r2.sh</string>
<string>sync</string>
</array>
<key>WorkingDirectory</key>
<string>$PLAYBOOKS_DIR</string>
<key>StartInterval</key>
<integer>${LAUNCHD_START_INTERVAL:-1800}</integer>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>$HOME/rclone-yitu-it-series.launchd.out.log</string>
<key>StandardErrorPath</key>
<string>$HOME/rclone-yitu-it-series.launchd.err.log</string>
</dict>
</plist>
EOF
launchctl unload "$plist" >/dev/null 2>&1 || true
launchctl load "$plist"
launchctl start plus.svc.yitu-it-series.rclone-sync
echo "Installed $plist"
}
uninstall_launchd() {
local plist="$HOME/Library/LaunchAgents/plus.svc.yitu-it-series.rclone-sync.plist"
launchctl unload "$plist" >/dev/null 2>&1 || true
rm -f "$plist"
echo "Removed $plist"
}
case "${1:-}" in
doctor) doctor ;;
create-bucket) create_bucket ;;
configure-rclone) configure_rclone ;;
dry-run) sync_dry_run ;;
sync) sync_run ;;
copy) copy_run ;;
check) check_run ;;
tree) tree_run ;;
configure-custom-domain) configure_custom_domain ;;
install-launchd) install_launchd ;;
uninstall-launchd) uninstall_launchd ;;
-h|--help|help|"") usage ;;
*) usage; exit 2 ;;
esac

View File

@ -1,9 +1,12 @@
--- ---
- name: Setup AI agent runtime - name: Setup AI agent runtime desktop
hosts: all hosts: jp-xhttp-contabo.svc.plus
become: true become: true
gather_facts: true gather_facts: true
vars:
xfce_manage_user: true
xfce_user_password_plaintext: "L@xiaomin1250"
xfce_user_shell: /bin/bash
xfce_enable_ufw: false
roles: roles:
- role: roles/ai_agent_runtime/ - roles/vhosts/xfce_xrdp_minimal/
when: ai_agent_runtime_enabled | default(true) | bool
tags: [ai_agent_runtime]

View File

@ -0,0 +1,9 @@
---
- name: Setup AI agent skills
hosts: all
become: true
gather_facts: true
roles:
- role: roles/ai_agent_runtime/
when: ai_agent_runtime_enabled | default(true) | bool
tags: [ai_agent_runtime]

View File

@ -1,6 +1,7 @@
--- ---
cloudflare_dns_default_source_hosts: cloudflare_dns_default_source_hosts:
- cn_front_host - cn_front_host
- cn_xworkmate_bridge_host
- jp_xhttp_contabo_host - jp_xhttp_contabo_host
- tky_proxy_host - tky_proxy_host

View File

@ -1,12 +0,0 @@
---
- name: Setup minimal XFCE + XRDP desktop
hosts: jp-xhttp-contabo.svc.plus
become: true
gather_facts: true
vars:
xfce_manage_user: true
xfce_user_password_plaintext: "L@xiaomin1250"
xfce_user_shell: /bin/bash
xfce_enable_ufw: false
roles:
- roles/vhosts/xfce_xrdp_minimal/