feat: rôle hermes_user + ask-agent + skills délégation

- Nouveau rôle hermes_user : création user/group UID 1002 sur storage-01
  et gpu-01, sudoers limités par host, clé SSH ed25519 générée sur
  storage-01 et distribuée vers gpu-01, ssh_config + known_hosts
- Kubeconfig copié depuis ansible vers hermes sur storage-01
- ask-agent : script bash LiteLLM pour déléguer aux agents
  system/monitor/brain/funk-ai
- hermes-skills/funk/agent-delegation : skill Hermes pour les
  inter-profile calls avec pattern Terminal: ask-agent
- Playbooks storage-01 et gpu-01 : ajout du rôle hermes_user

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
alkatrazz 2026-05-13 16:03:43 +02:00
parent 0c89d262d1
commit 6be2a89480
7 changed files with 325 additions and 0 deletions

View file

@ -0,0 +1,9 @@
---
hermes_user: hermes
hermes_group: hermes
hermes_uid: 1002
hermes_gid: 1002
hermes_home: /opt/hermes
hermes_shell: /bin/bash
hermes_ssh_key_type: ed25519
hermes_ssh_key_comment: "hermes@funk-cluster"

View file

@ -0,0 +1,151 @@
---
# ── User creation (tous les hosts) ────────────────────────────────────────────
- name: Create hermes group
ansible.builtin.group:
name: "{{ hermes_group }}"
gid: "{{ hermes_gid }}"
state: present
- name: Create hermes user
ansible.builtin.user:
name: "{{ hermes_user }}"
group: "{{ hermes_group }}"
uid: "{{ hermes_uid }}"
home: "{{ hermes_home }}"
shell: "{{ hermes_shell }}"
create_home: true
state: present
- name: Create .ssh directory for hermes
ansible.builtin.file:
path: "{{ hermes_home }}/.ssh"
state: directory
owner: "{{ hermes_user }}"
group: "{{ hermes_group }}"
mode: '0700'
- name: Add hermes to systemd-journal group
ansible.builtin.user:
name: "{{ hermes_user }}"
groups: systemd-journal
append: true
# ── Sudoers limité ─────────────────────────────────────────────────────────────
- name: Allow hermes limited sudo on storage-01
ansible.builtin.copy:
dest: /etc/sudoers.d/hermes
content: |
# Hermes agent — monitoring local
hermes ALL=(ALL) NOPASSWD: /usr/sbin/smartctl *
hermes ALL=(ALL) NOPASSWD: /usr/bin/journalctl *
mode: '0440'
validate: /usr/sbin/visudo -cf %s
when: inventory_hostname in groups['gateway']
- name: Allow hermes limited sudo on gpu-01
ansible.builtin.copy:
dest: /etc/sudoers.d/hermes
content: |
# Hermes agent — GPU monitoring
hermes ALL=(ALL) NOPASSWD: /usr/bin/rocm-smi
hermes ALL=(ALL) NOPASSWD: /bin/systemctl status llama-server*
hermes ALL=(ALL) NOPASSWD: /usr/bin/journalctl *
mode: '0440'
validate: /usr/sbin/visudo -cf %s
when: inventory_hostname in groups['gpu_hosts']
# ── Génération clé SSH (storage-01 uniquement) ────────────────────────────────
- name: Generate SSH key for hermes
ansible.builtin.user:
name: "{{ hermes_user }}"
generate_ssh_key: true
ssh_key_type: "{{ hermes_ssh_key_type }}"
ssh_key_file: ".ssh/id_{{ hermes_ssh_key_type }}"
ssh_key_comment: "{{ hermes_ssh_key_comment }}"
when: inventory_hostname in groups['gateway']
- name: Read hermes public key
ansible.builtin.slurp:
src: "{{ hermes_home }}/.ssh/id_{{ hermes_ssh_key_type }}.pub"
register: hermes_pub_key
when: inventory_hostname in groups['gateway']
# ── Distribution clé publique → gpu-01 ────────────────────────────────────────
- name: Add hermes public key to gpu-01 authorized_keys
ansible.posix.authorized_key:
user: "{{ hermes_user }}"
key: "{{ hermes_pub_key.content | b64decode | trim }}"
state: present
delegate_to: "{{ item }}"
loop: "{{ groups['gpu_hosts'] }}"
when: inventory_hostname in groups['gateway']
# ── SSH config + known_hosts (storage-01) ─────────────────────────────────────
- name: Deploy SSH client config for hermes
ansible.builtin.template:
src: ssh_config.j2
dest: "{{ hermes_home }}/.ssh/config"
owner: "{{ hermes_user }}"
group: "{{ hermes_group }}"
mode: '0600'
when: inventory_hostname in groups['gateway']
- name: Scan gpu-01 SSH host key
ansible.builtin.command:
cmd: "ssh-keyscan -t ed25519 {{ hostvars['gpu-01'].ansible_host }}"
register: gpu01_hostkey
changed_when: false
when: inventory_hostname in groups['gateway']
- name: Add gpu-01 to hermes known_hosts
ansible.builtin.known_hosts:
name: "{{ hostvars['gpu-01'].ansible_host }}"
key: "{{ gpu01_hostkey.stdout }}"
path: "{{ hermes_home }}/.ssh/known_hosts"
state: present
when:
- inventory_hostname in groups['gateway']
- gpu01_hostkey.stdout | length > 0
- name: Fix ownership of hermes .ssh directory
ansible.builtin.file:
path: "{{ hermes_home }}/.ssh"
state: directory
owner: "{{ hermes_user }}"
group: "{{ hermes_group }}"
recurse: true
when: inventory_hostname in groups['gateway']
# ── kubeconfig pour kubectl (storage-01) ──────────────────────────────────────
- name: Create .kube directory for hermes
ansible.builtin.file:
path: "{{ hermes_home }}/.kube"
state: directory
owner: "{{ hermes_user }}"
group: "{{ hermes_group }}"
mode: '0700'
when: inventory_hostname in groups['gateway']
- name: Check if ansible kubeconfig exists
ansible.builtin.stat:
path: /home/ansible/.kube/config
register: ansible_kubeconfig
when: inventory_hostname in groups['gateway']
- name: Copy kubeconfig to hermes home
ansible.builtin.copy:
src: /home/ansible/.kube/config
dest: "{{ hermes_home }}/.kube/config"
owner: "{{ hermes_user }}"
group: "{{ hermes_group }}"
mode: '0600'
remote_src: true
when:
- inventory_hostname in groups['gateway']
- ansible_kubeconfig.stat.exists

View file

@ -0,0 +1,6 @@
Host gpu-01
HostName {{ hostvars['gpu-01'].ansible_host }}
User {{ hermes_user }}
IdentityFile ~/.ssh/id_{{ hermes_ssh_key_type }}
StrictHostKeyChecking accept-new
ConnectTimeout 10

View file

@ -0,0 +1,73 @@
#!/usr/bin/env bash
# ask-agent — Délègue une question à un agent Funk via LiteLLM.
#
# Usage:
# ask-agent <agent> "<question>"
# ask-agent <agent> "<question>" [--max-tokens N] [--system "<prompt>"]
#
# Agents: system | monitor | brain | funk-ai
set -euo pipefail
LITELLM_URL="http://127.0.0.1:4000/v1/chat/completions"
API_KEY="lm-studio"
MAX_TOKENS=1000
SYSTEM_PROMPT=""
declare -A MODELS=(
[system]="qwen3-1.7b-system"
[monitor]="qwen3-1.7b-monitor"
[brain]="claude-sonnet-4-6"
[funk-ai]="qwen3-8b"
)
usage() {
echo "Usage: ask-agent <agent> \"<question>\" [--max-tokens N] [--system \"<prompt>\"]"
echo "Agents: system | monitor | brain | funk-ai"
exit 1
}
[ $# -lt 2 ] && usage
AGENT="$1"
QUESTION="$2"
shift 2
while [ $# -gt 0 ]; do
case "$1" in
--max-tokens) MAX_TOKENS="$2"; shift 2 ;;
--system) SYSTEM_PROMPT="$2"; shift 2 ;;
*) echo "Option inconnue : $1" >&2; exit 1 ;;
esac
done
MODEL="${MODELS[$AGENT]:-}"
if [ -z "$MODEL" ]; then
echo "Agent inconnu : $AGENT. Agents valides : ${!MODELS[*]}" >&2
exit 1
fi
# Construire le tableau messages JSON
if [ -n "$SYSTEM_PROMPT" ]; then
MESSAGES=$(jq -n \
--arg sp "$SYSTEM_PROMPT" \
--arg q "$QUESTION" \
'[{"role":"system","content":$sp},{"role":"user","content":$q}]')
else
MESSAGES=$(jq -n \
--arg q "$QUESTION" \
'[{"role":"user","content":$q}]')
fi
PAYLOAD=$(jq -n \
--arg model "$MODEL" \
--argjson messages "$MESSAGES" \
--argjson max_tokens "$MAX_TOKENS" \
'{model:$model, messages:$messages, max_tokens:$max_tokens, stream:false}')
curl -s -f \
-X POST "$LITELLM_URL" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
| jq -r '.choices[0].message.content'