From 6be2a89480a7262cabafcd3f1b3b630d7b61b0b5 Mon Sep 17 00:00:00 2001 From: alkatrazz Date: Wed, 13 May 2026 16:03:43 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20r=C3=B4le=20hermes=5Fuser=20+=20ask-age?= =?UTF-8?q?nt=20+=20skills=20d=C3=A9l=C3=A9gation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- ansible/playbooks/gpu-01.yml | 1 + ansible/playbooks/storage-01.yml | 1 + ansible/roles/hermes_user/defaults/main.yml | 9 ++ ansible/roles/hermes_user/tasks/main.yml | 151 ++++++++++++++++++ .../roles/hermes_user/templates/ssh_config.j2 | 6 + ansible/roles/litellm/files/ask-agent | 73 +++++++++ hermes-skills/funk/agent-delegation/SKILL.md | 84 ++++++++++ 7 files changed, 325 insertions(+) create mode 100644 ansible/roles/hermes_user/defaults/main.yml create mode 100644 ansible/roles/hermes_user/tasks/main.yml create mode 100644 ansible/roles/hermes_user/templates/ssh_config.j2 create mode 100644 ansible/roles/litellm/files/ask-agent create mode 100644 hermes-skills/funk/agent-delegation/SKILL.md diff --git a/ansible/playbooks/gpu-01.yml b/ansible/playbooks/gpu-01.yml index 29ddb5b..eb3cf23 100644 --- a/ansible/playbooks/gpu-01.yml +++ b/ansible/playbooks/gpu-01.yml @@ -2,6 +2,7 @@ - name: Configure gpu-01 hosts: gpu_hosts roles: + - { role: hermes_user, tags: [hermes_user] } - { role: common, tags: [common] } - { role: rocm, tags: [rocm] } - { role: nfs_client, tags: [nfs_client] } diff --git a/ansible/playbooks/storage-01.yml b/ansible/playbooks/storage-01.yml index a0eb900..1d97376 100644 --- a/ansible/playbooks/storage-01.yml +++ b/ansible/playbooks/storage-01.yml @@ -2,6 +2,7 @@ - name: Configure storage-01 hosts: gateway roles: + - { role: hermes_user, tags: [hermes_user] } - { role: common, tags: [common] } - { role: gateway, tags: [gateway] } - { role: dnsmasq, tags: [dnsmasq] } diff --git a/ansible/roles/hermes_user/defaults/main.yml b/ansible/roles/hermes_user/defaults/main.yml new file mode 100644 index 0000000..36d77cf --- /dev/null +++ b/ansible/roles/hermes_user/defaults/main.yml @@ -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" diff --git a/ansible/roles/hermes_user/tasks/main.yml b/ansible/roles/hermes_user/tasks/main.yml new file mode 100644 index 0000000..9d6ca08 --- /dev/null +++ b/ansible/roles/hermes_user/tasks/main.yml @@ -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 diff --git a/ansible/roles/hermes_user/templates/ssh_config.j2 b/ansible/roles/hermes_user/templates/ssh_config.j2 new file mode 100644 index 0000000..cbd8721 --- /dev/null +++ b/ansible/roles/hermes_user/templates/ssh_config.j2 @@ -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 diff --git a/ansible/roles/litellm/files/ask-agent b/ansible/roles/litellm/files/ask-agent new file mode 100644 index 0000000..8f68781 --- /dev/null +++ b/ansible/roles/litellm/files/ask-agent @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# ask-agent — Délègue une question à un agent Funk via LiteLLM. +# +# Usage: +# ask-agent "" +# ask-agent "" [--max-tokens N] [--system ""] +# +# 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 \"\" [--max-tokens N] [--system \"\"]" + 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' diff --git a/hermes-skills/funk/agent-delegation/SKILL.md b/hermes-skills/funk/agent-delegation/SKILL.md new file mode 100644 index 0000000..039f1d2 --- /dev/null +++ b/hermes-skills/funk/agent-delegation/SKILL.md @@ -0,0 +1,84 @@ +--- +name: agent-delegation +description: "Délègue des sous-tâches aux agents spécialisés du cluster Funk. Utilise system/monitor pour les tâches rapides et légères, brain pour l'analyse complexe." +version: 1.0.0 +author: Funk Lab +license: MIT +platforms: [linux] +metadata: + hermes: + tags: [multi-agent, delegation, funk, system, monitor, brain, local] +--- + +# Délégation inter-agents — Cluster Funk + +**Contexte important** : Tu tournes EN LOCAL sur `storage-01`. Ne jamais SSH vers `storage-01` depuis toi-même — lance les commandes directement via le terminal. `systemctl`, `journalctl`, `cat /proc/mdstat`, etc. s'exécutent localement sans SSH. + +Le cluster Funk dispose de 4 agents avec des capacités différentes. Utilise la commande shell `ask-agent` (disponible dans `/usr/local/bin/`) pour déléguer des sous-tâches aux agents spécialisés. + +## Agents disponibles + +| Agent | Modèle | Vitesse | Coût | Usage idéal | +|---|---|---|---|---| +| `system` | Qwen3-1.7B CPU (port 1236) | ~21 tok/s | Gratuit | Calculs rapides, parsing, formatage, résumés courts | +| `monitor` | Qwen3-1.7B CPU (port 1237) | ~21 tok/s | Gratuit | Analyse métriques, logs, santé système, alertes | +| `brain` | Claude Sonnet 4.6 (API) | Variable | Payant | Raisonnement complexe, architecture, revue de code | +| `funk-ai` | Qwen3-8B GPU (port 1234) | ~35 tok/s | Gratuit | Tâches générales (toi-même) | + +## Règle fondamentale + +**`ask-agent` est une commande shell.** Pour déléguer, utilise l'outil Terminal avec exactement cette syntaxe : + +``` +Terminal: ask-agent system "ta question" +Terminal: ask-agent monitor "ta question" +Terminal: ask-agent brain "ta question" +``` + +Ne jamais SSH vers storage-01 pour des commandes locales. Utilise `Terminal` directement. + +## Pattern type : collecter + déléguer + +```bash +# 1. Collecter les données localement (PAS via SSH) +Terminal: systemctl --state=failed --no-pager + +# 2. Passer le résultat à un agent pour analyse +Terminal: ask-agent monitor "voici les services en erreur systemd : — résume les problèmes" +``` + +## Utilisation + +```bash +# Services en erreur sur storage-01 (LOCAL, pas SSH) +Terminal: systemctl --state=failed --no-pager +Terminal: ask-agent monitor "analyse ces services en erreur : " + +# Déléguer une analyse de logs +Terminal: journalctl -u hermes-agent -n 20 --no-pager +Terminal: ask-agent system "résume ces logs en 3 points : " + +# Question simple rapide +Terminal: ask-agent system "quelle commande pour vider le cache DNS sous AlmaLinux ?" + +# Analyse complexe +Terminal: ask-agent brain "explique pourquoi ce log Kubernetes indique un crash : " + +# Avec prompt système +Terminal: ask-agent system "état du RAID normal ?" --system "Tu es expert Linux AlmaLinux, réponds en 1 phrase." +``` + +## Quand déléguer à quel agent + +| Situation | Agent | +|---|---| +| Question Linux simple, formatage, résumé court | `system` | +| Analyse logs, métriques, santé système | `monitor` | +| Raisonnement complexe, revue critique, architecture | `brain` | + +## Notes techniques + +- `ask-agent` appelle LiteLLM sur `http://127.0.0.1:4000/v1` avec la clé `lm-studio` +- Timeout : 120s +- Les agents `system` et `monitor` tournent sur des CPUs isolés (14 threads chacun) — ils ne bloquent pas le GPU +- `brain` consomme des tokens API Anthropic — réserver aux cas qui le justifient