playbooks/roles/agent_skills/tasks/main.yml
Haitao Pan c3f3b8ac8e refactor(agent_skills): run on target host, git-clone sources, drop delegate_to localhost
Make the role work identically under both execution models:
- local/pull (curl|bash -> ansible-playbook -c local; localhost == host)
- remote controller (ansible-playbook -i inventory over ssh; tasks run on host)

Changes:
- Remove ALL delegate_to: localhost (the old raw 'command: rsync' detected
  local-vs-remote via ansible_connection, but delegate_to localhost forced it
  to 'local', so the user@host push branch was dead code -> remote runs wrote
  to the controller's /root and failed).
- Acquire xworkspace-core-skills via ansible.builtin.git clone ON THE HOST
  (most universal/cross-platform), instead of requiring a controller-side dir.
- Merge core skills into the canonical dir with ansible.builtin.copy
  (remote_src, host-local) instead of raw rsync; installer adapters install
  directly into the canonical dir on the host.
- Drop rsync-only vars/excludes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 14:57:49 +08:00

349 lines
13 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
# 设计:全程在「目标主机」上执行——没有任何 delegate_to: localhost。
# 因此两种执行模型行为完全一致:
# - 本地/pullcurl|bash → ansible-playbook -c locallocalhost 即主机)
# - 远程 controlleransible-playbook -i <inventory> over ssh任务在主机上跑
# 源以 git clone 获取(最通用、跨平台),不再依赖 controller 端预置目录,
# 合并用 ansible.builtin.copy无裸 rsync、无本地钉死
- name: Validate agent skills input
ansible.builtin.assert:
that:
- agent_skills_user | length > 0
- agent_skills_group | length > 0
- agent_skills_home | length > 0
- agent_skills_remote_dir | length > 0
- agent_skills_targets | length > 0
fail_msg: "agent_skills_user/group/home, remote_dir and targets must be set."
- name: Build required agent skills list
ansible.builtin.set_fact:
agent_skills_required_entries: "{{ agent_skills_typical_scenario_skills + agent_skills_extra_required_skills }}"
- name: Ensure agent skills owner home exists
ansible.builtin.file:
path: "{{ agent_skills_home }}"
state: directory
owner: "{{ agent_skills_user }}"
group: "{{ agent_skills_group }}"
mode: "0755"
- name: Ensure canonical agent skills directory exists
ansible.builtin.file:
path: "{{ agent_skills_remote_dir }}"
state: directory
owner: "{{ agent_skills_user }}"
group: "{{ agent_skills_group }}"
mode: "0755"
# --- 源获取:在目标主机 git clone最通用 ---------------------------------
- name: Ensure core skills checkout parent exists
ansible.builtin.file:
path: "{{ agent_skills_xworkspace_core_clone_dir | dirname }}"
state: directory
owner: "{{ agent_skills_user }}"
group: "{{ agent_skills_group }}"
mode: "0755"
when: agent_skills_xworkspace_core_enabled | bool
- name: Clone/update xworkspace core skills on the target host
ansible.builtin.git:
repo: "{{ agent_skills_xworkspace_core_repo_url }}"
dest: "{{ agent_skills_xworkspace_core_clone_dir }}"
version: "{{ agent_skills_xworkspace_core_version }}"
depth: 1
force: true
become_user: "{{ agent_skills_user }}"
register: agent_skills_core_clone
when: agent_skills_xworkspace_core_enabled | bool
- name: Inspect core skills directory
ansible.builtin.stat:
path: "{{ agent_skills_xworkspace_core_source_dir }}"
register: agent_skills_core_skills_stat
when: agent_skills_xworkspace_core_enabled | bool
- name: Require core skills directory when enabled and required
ansible.builtin.assert:
that:
- agent_skills_core_skills_stat.stat.isdir | default(false)
fail_msg: "core skills dir missing after clone: {{ agent_skills_xworkspace_core_source_dir }}"
when:
- agent_skills_xworkspace_core_enabled | bool
- agent_skills_xworkspace_core_required | bool
- name: Build skill search dirs (canonical + core checkout)
ansible.builtin.set_fact:
agent_skills_search_dirs: >-
{{
[agent_skills_remote_dir]
+ (
(
agent_skills_xworkspace_core_enabled | bool
and agent_skills_core_skills_stat.stat.isdir | default(false)
)
| ternary([agent_skills_xworkspace_core_source_dir], [])
)
}}
# --- 缺失场景技能:用 installer 适配器装到 canonical主机本地 --------------
- name: Inspect required scenario skills presence
ansible.builtin.shell: |
set -eu
for d in {{ agent_skills_search_dirs | map('quote') | join(' ') }}; do
for c in {{ ([item.name] + (item.aliases | default([]))) | unique | map('quote') | join(' ') }}; do
if [ -f "$d/$c/SKILL.md" ]; then printf '%s\n' "$d/$c"; exit 0; fi
m="$(find "$d" -type f -path "*/$c/SKILL.md" -print -quit 2>/dev/null || true)"
if [ -n "$m" ]; then dirname "$m"; exit 0; fi
done
done
exit 1
args:
executable: /bin/bash
register: agent_skills_presence
changed_when: false
failed_when: false
check_mode: false
loop: "{{ agent_skills_required_entries }}"
loop_control:
label: "{{ item.name }}"
- name: Build missing scenario skills list
ansible.builtin.set_fact:
agent_skills_missing_entries: >-
{{ agent_skills_presence.results | selectattr('rc', 'ne', 0) | map(attribute='item') | list }}
- name: Install missing scenario skills via installer adapters (clawhub/find-skills)
ansible.builtin.shell: |
set -eu
skill={{ item.name | quote }}
target_dir={{ agent_skills_remote_dir | quote }}
parent="$(dirname "$target_dir")"; base="$(basename "$target_dir")"
rc=1
if command -v clawhub >/dev/null 2>&1; then
for n in {{ ([item.install_name | default(item.name)] + (item.aliases | default([]))) | unique | map('quote') | join(' ') }}; do
if clawhub --workdir "$parent" --dir "$base" --no-input install {{ (item.install_force | default(false) | bool) | ternary('--force', '') }} "$n"; then rc=0; break; fi
done
exit "$rc"
elif command -v find-skills >/dev/null 2>&1; then
for n in {{ ([item.install_name | default(item.name)] + (item.aliases | default([]))) | unique | map('quote') | join(' ') }}; do
if find-skills install "$n" --target "$target_dir"; then rc=0; break; fi
done
exit "$rc"
elif [ "{{ agent_skills_auto_install_fail_on_missing_installer | bool | ternary('true', 'false') }}" = "true" ]; then
echo "No installer (clawhub/find-skills) for $skill; preseed $target_dir/$skill." >&2
exit 127
else
echo "Skipped missing $skill (no installer adapter)." >&2
fi
args:
executable: /bin/bash
become_user: "{{ agent_skills_user }}"
register: agent_skills_install_result
changed_when: agent_skills_install_result.rc == 0
loop: "{{ agent_skills_missing_entries }}"
loop_control:
label: "{{ item.name }}"
when:
- agent_skills_auto_install_enabled | bool
- agent_skills_missing_entries | length > 0
# --- 合并 core 技能到 canonical主机本地 copy无 rsync、无 delegate --------
- name: Merge core skills into canonical directory
ansible.builtin.copy:
src: "{{ agent_skills_xworkspace_core_source_dir }}/"
dest: "{{ agent_skills_remote_dir }}/"
remote_src: true
owner: "{{ agent_skills_user }}"
group: "{{ agent_skills_group }}"
mode: preserve
when:
- agent_skills_xworkspace_core_enabled | bool
- agent_skills_core_skills_stat.stat.isdir | default(false)
- name: Re-inspect required scenario skills in canonical dir
ansible.builtin.shell: |
set -eu
d={{ agent_skills_remote_dir | quote }}
for c in {{ ([item.name] + (item.aliases | default([]))) | unique | map('quote') | join(' ') }}; do
if [ -f "$d/$c/SKILL.md" ]; then printf '%s\n' "$d/$c"; exit 0; fi
m="$(find "$d" -type f -path "*/$c/SKILL.md" -print -quit 2>/dev/null || true)"
if [ -n "$m" ]; then dirname "$m"; exit 0; fi
done
exit 1
args:
executable: /bin/bash
register: agent_skills_presence_final
changed_when: false
failed_when: false
check_mode: false
loop: "{{ agent_skills_required_entries }}"
loop_control:
label: "{{ item.name }}"
- name: Assert required scenario skills are available
ansible.builtin.assert:
that:
- (agent_skills_presence_final.results | selectattr('rc', 'ne', 0) | list | length) == 0
fail_msg: >-
Required scenario skills still missing under {{ agent_skills_remote_dir }}:
{{ agent_skills_presence_final.results | selectattr('rc', 'ne', 0) | map(attribute='item.name') | join(', ') }}.
- name: Build resolved skill paths
ansible.builtin.set_fact:
agent_skills_resolved_paths: >-
{{ agent_skills_presence_final.results | selectattr('rc', 'eq', 0) | map(attribute='stdout') | list | unique }}
- name: Run optional scenario skill quality gates
ansible.builtin.shell: |
set -eu
skill_path={{ item.0 | quote }}
gate_name={{ item.1.name | quote }}
if command -v "$gate_name" >/dev/null 2>&1; then
{{ item.1.argv_prefix | map('quote') | join(' ') }} "$skill_path"
else
echo "Skipped missing quality gate: $gate_name"
fi
args:
executable: /bin/bash
become_user: "{{ agent_skills_user }}"
register: agent_skills_quality_gate_results
changed_when: false
failed_when: agent_skills_quality_gate_fail_on_error | bool and agent_skills_quality_gate_results.rc != 0
loop: "{{ agent_skills_resolved_paths | product(agent_skills_quality_gate_commands) | list }}"
loop_control:
label: "{{ item.1.name }} {{ item.0 | basename }}"
when:
- agent_skills_quality_gate_enabled | bool
- agent_skills_resolved_paths | length > 0
check_mode: false
- name: Set canonical agent skills ownership
ansible.builtin.file:
path: "{{ agent_skills_remote_dir }}"
state: directory
owner: "{{ agent_skills_user }}"
group: "{{ agent_skills_group }}"
recurse: true
# --- 把分类嵌套技能在 canonical 根做扁平 symlink主机本地 ------------------
- 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
# --- 把各 Agent 的 skills 目录 symlink 到 canonical ---------------------------
- name: Flatten agent skills target paths
ansible.builtin.set_fact:
agent_skills_target_paths: "{{ agent_skills_targets | subelements('paths') | map('last') | list }}"
- name: Inspect agent skills target paths
ansible.builtin.stat:
path: "{{ item }}"
register: agent_skills_target_path_stats
loop: "{{ agent_skills_target_paths }}"
- name: Reject existing non-link target directories unless replacement is enabled
ansible.builtin.fail:
msg: >-
Agent skills target already exists and is not a symlink: {{ item.item }}.
Set agent_skills_replace_existing_target_dirs=true to replace it with a link
to {{ agent_skills_remote_dir }}.
loop: "{{ agent_skills_target_path_stats.results }}"
when:
- item.stat.exists | 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
- name: Replace existing non-link target directories when enabled
ansible.builtin.file:
path: "{{ item.item }}"
state: absent
loop: "{{ agent_skills_target_path_stats.results }}"
when:
- item.stat.exists | 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
- name: Build agent skills target parent paths
ansible.builtin.set_fact:
agent_skills_target_parent_paths: "{{ agent_skills_target_paths | map('dirname') | list | unique }}"
- name: Inspect agent skills target parent directories
ansible.builtin.stat:
path: "{{ item }}"
register: agent_skills_target_parent_stats
loop: "{{ agent_skills_target_parent_paths }}"
- name: Ensure agent skills target parent directories exist
ansible.builtin.file:
path: "{{ item.item }}"
state: directory
owner: "{{ agent_skills_user }}"
group: "{{ agent_skills_group }}"
mode: "0755"
loop: "{{ agent_skills_target_parent_stats.results }}"
when:
- not item.stat.exists | default(false)
- name: Link agent skills targets to canonical directory
ansible.builtin.file:
src: "{{ agent_skills_remote_dir }}"
dest: "{{ item }}"
state: link
force: true
loop: "{{ agent_skills_target_paths }}"
when: item not in agent_skills_preserve_existing_target_dirs
- name: Verify canonical skill manifests are present
ansible.builtin.find:
paths: "{{ agent_skills_remote_dir }}"
patterns: SKILL.md
recurse: true
file_type: file
register: agent_skills_manifest_files
- name: Assert synced agent skills contain manifests
ansible.builtin.assert:
that:
- agent_skills_manifest_files.matched | int > 0
fail_msg: "No SKILL.md files found under {{ agent_skills_remote_dir }}."
- name: Report synced agent skills
ansible.builtin.debug:
msg: >-
{{ agent_skills_manifest_files.matched }} skill manifests under
{{ agent_skills_remote_dir }}; linked {{ agent_skills_target_paths | length }} agent targets.