mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 05:34:43 +02:00
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:
parent
0c89d262d1
commit
6be2a89480
7 changed files with 325 additions and 0 deletions
|
|
@ -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] }
|
||||
|
|
|
|||
|
|
@ -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] }
|
||||
|
|
|
|||
9
ansible/roles/hermes_user/defaults/main.yml
Normal file
9
ansible/roles/hermes_user/defaults/main.yml
Normal 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"
|
||||
151
ansible/roles/hermes_user/tasks/main.yml
Normal file
151
ansible/roles/hermes_user/tasks/main.yml
Normal 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
|
||||
6
ansible/roles/hermes_user/templates/ssh_config.j2
Normal file
6
ansible/roles/hermes_user/templates/ssh_config.j2
Normal 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
|
||||
73
ansible/roles/litellm/files/ask-agent
Normal file
73
ansible/roles/litellm/files/ask-agent
Normal 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'
|
||||
84
hermes-skills/funk/agent-delegation/SKILL.md
Normal file
84
hermes-skills/funk/agent-delegation/SKILL.md
Normal file
|
|
@ -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ésultat> — 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 : <résultat>"
|
||||
|
||||
# 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 : <résultat>"
|
||||
|
||||
# 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 : <log>"
|
||||
|
||||
# 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue