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>
349 lines
13 KiB
YAML
349 lines
13 KiB
YAML
---
|
||
# 设计:全程在「目标主机」上执行——没有任何 delegate_to: localhost。
|
||
# 因此两种执行模型行为完全一致:
|
||
# - 本地/pull:curl|bash → ansible-playbook -c local(localhost 即主机)
|
||
# - 远程 controller:ansible-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.
|