feat(stt): actions cluster via Hermes — contexte « agent » + exécuteur hermes-exec (#42)

Asa peut AGIR sur le homelab (l'autre moitié de la vision), via le vrai agent Hermes,
profil par défaut, tous ses outils — avec confirmation et jeton.

Découverte : Hermes n'a pas d'API HTTP (appli TUI), mais un mode one-shot `hermes -z
"<prompt>" --yolo`. On s'appuie dessus.

storage-01 — exécuteur :
- tools/hermes-exec/server.py : service HTTP qui lance `hermes -z --yolo` en user hermes,
  derrière un jeton Bearer (compare_digest), timeout + audit, une action à la fois.
- rôle Ansible hermes_exec : systemd (User=hermes, env hermes-agent), jeton via
  EnvironmentFile 0640 (Vault vault_hermes_exec_token) ; ajouté au playbook storage-01.

STT-server (0.5.0 → 0.6.0) :
- agent.py : pont vers hermes-exec (jeton) + détection confirme/annule.
- contexts.py : contexte « agent » (court-circuite le LLM).
- app.py : flux dédié — handshake 2 temps par session (pending action) → sur « confirme »,
  appelle hermes-exec ; « annule » annule. /v1/contexts masque « agent » si désactivé.
- config : STT_ACTIONS_ENABLED (opt-in, défaut false) + URL + jeton (secret k8s).
- deployment : env actions + secret stt-server-secrets/hermes-exec-token (optionnel).

Sécurité : opt-in désactivé par défaut ; jeton obligatoire (sinon contexte caché + exécuteur
refuse tout) ; --yolo atteint seulement jeton+confirmation en main ; audit storage-01.
Client : AUCUN changement (le contexte « agent » apparaît tout seul dans le sélecteur).

Validé en local : exécuteur (401 sans/mauvais jeton, exec avec bon jeton via echo),
handshake serveur (TestClient : confirme→exécute, annule→annule, agent masqué si OFF).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ALI YESILKAYA 2026-06-21 04:00:14 +02:00 committed by GitHub
parent 5af51f8f66
commit 9a46f6cbcb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 457 additions and 2 deletions

View file

@ -17,3 +17,4 @@
- { role: alertmanager_webhook, tags: [alertmanager_webhook] }
- { role: postfix_relay, tags: [postfix_relay] }
- { role: hermes_auto_improve, tags: [hermes_auto_improve] }
- { role: hermes_exec, tags: [hermes_exec] }

View file

@ -0,0 +1,54 @@
# Rôle `hermes_exec`
Déploie **hermes-exec** sur storage-01 : un petit service HTTP qui lance le vrai agent
Hermes en one-shot (`hermes -z "<prompt>" --yolo`, profil par défaut, tous ses outils)
pour le compte de l'assistant vocal STT.
## Rôle dans la chaîne
```
Voix → STT-server (in-cluster) → [confirmation vocale] → POST /exec {prompt} + Bearer
→ hermes-exec (storage-01:9096)
└─ hermes -z "<prompt>" --yolo
```
## Sécurité
- **Jeton d'auth obligatoire** (`vault_hermes_exec_token`) : sans Bearer valide → `401`.
Sans jeton configuré, le service refuse **tout** (verrouillé par défaut).
- `--yolo` (auto-exécution des outils) n'est atteint qu'avec un jeton valide. La
**confirmation** qui protège des erreurs de transcription est faite **en amont** par le
STT-server (handshake « tu veux que… ? » → « confirme »).
- Jeton stocké dans `/etc/hermes-exec/env` (mode `0640`, groupe `hermes`), pas dans l'unit.
- **Audit** : `/var/log/hermes-exec.log` (prompt + sortie de chaque action).
- Une action à la fois (verrou).
## Variables
Voir `defaults/main.yml`. Le jeton vient du Vault :
```bash
make vault-edit # ajouter : vault_hermes_exec_token: "<chaîne aléatoire longue>"
```
## Déploiement
```bash
ansible-playbook -i inventory.yml playbooks/storage-01.yml --tags hermes_exec
```
## Endpoints
| Méthode | Chemin | Auth | Effet |
|---|---|---|---|
| GET | `/health` | non | `{"ok": true}` |
| POST | `/exec` `{prompt}` | Bearer | lance `hermes -z "<prompt>" --yolo``{"ok", "output"}` |
## Gaps IaC
- Le service suppose l'environnement de `hermes-agent` (HOME `/opt/hermes`, HERMES_HOME
`/srv/data/hermes`, LiteLLM `:4000`). Si l'install Hermes change de chemins, ajuster les
defaults.
- Joignable depuis le cluster sur `:9096` (le firewall gateway laisse passer cluster→s01
sur les ports hauts ; le jeton est la barrière). Si tu durcis nftables, ouvre `:9096`
pour le réseau cluster.

View file

@ -0,0 +1,18 @@
---
# hermes_exec — exécuteur d'actions Hermes pour l'assistant vocal STT.
# Lance `hermes -z "<prompt>" --yolo` (profil défaut, tous outils) derrière un jeton.
hermes_exec_port: 9096
# Binaire hermes (venv de l'agent) + environnement (mêmes valeurs que hermes-agent).
hermes_exec_bin: /opt/hermes/.hermes/hermes-agent/venv/bin/hermes
hermes_exec_user: hermes
hermes_exec_group: hermes
hermes_exec_home: /opt/hermes
hermes_exec_hermes_home: /srv/data/hermes
hermes_exec_lm_base_url: http://127.0.0.1:4000/v1
hermes_exec_lm_api_key: lm-studio
hermes_exec_timeout: 120 # secondes max par action
hermes_exec_log: /var/log/hermes-exec.log
# Jeton d'auth — DOIT venir du Vault (group_vars/all/vault.yml : vault_hermes_exec_token).
hermes_exec_token: "{{ vault_hermes_exec_token | default('') }}"

View file

@ -0,0 +1,9 @@
---
- name: reload systemd
ansible.builtin.systemd:
daemon_reload: true
- name: restart hermes-exec
ansible.builtin.systemd:
name: hermes-exec
state: restarted

View file

@ -0,0 +1,65 @@
---
# hermes_exec — déploie l'exécuteur d'actions Hermes (storage-01).
- name: Garde-fou — jeton hermes_exec obligatoire
ansible.builtin.assert:
that:
- hermes_exec_token | length > 0
fail_msg: >-
vault_hermes_exec_token est vide. Ajoute-le dans group_vars/all/vault.yml
(make vault-edit) avant de déployer hermes_exec.
- name: Installer le serveur hermes-exec
ansible.builtin.copy:
src: "{{ inventory_dir }}/../tools/hermes-exec/server.py"
dest: /usr/local/bin/hermes-exec-server
owner: root
group: root
mode: "0755"
notify: restart hermes-exec
- name: Créer le fichier de log (accessible au user hermes)
ansible.builtin.file:
path: "{{ hermes_exec_log }}"
state: touch
owner: "{{ hermes_exec_user }}"
group: "{{ hermes_exec_group }}"
mode: "0640"
modification_time: preserve
access_time: preserve
- name: Répertoire du fichier d'environnement (jeton)
ansible.builtin.file:
path: /etc/hermes-exec
state: directory
owner: root
group: "{{ hermes_exec_group }}"
mode: "0750"
- name: Déposer le jeton (EnvironmentFile à accès restreint)
ansible.builtin.template:
src: hermes-exec.env.j2
dest: /etc/hermes-exec/env
owner: root
group: "{{ hermes_exec_group }}"
mode: "0640"
no_log: true
notify: restart hermes-exec
- name: Unité systemd hermes-exec
ansible.builtin.template:
src: hermes-exec.service.j2
dest: /etc/systemd/system/hermes-exec.service
owner: root
group: root
mode: "0644"
notify:
- reload systemd
- restart hermes-exec
- name: Activer et démarrer hermes-exec
ansible.builtin.systemd:
name: hermes-exec
enabled: true
state: started
daemon_reload: true

View file

@ -0,0 +1,3 @@
# Géré par Ansible (rôle hermes_exec) — NE PAS éditer à la main.
# Jeton d'auth de l'exécuteur Hermes (Bearer attendu par hermes-exec).
HERMES_EXEC_TOKEN={{ hermes_exec_token }}

View file

@ -0,0 +1,29 @@
[Unit]
Description=hermes-exec — exécuteur d'actions Hermes (assistant vocal STT)
After=network-online.target hermes-agent.service litellm.service
Wants=network-online.target
[Service]
User={{ hermes_exec_user }}
Group={{ hermes_exec_group }}
# Jeton chargé depuis un fichier à accès restreint (pas en clair dans l'unit).
EnvironmentFile=/etc/hermes-exec/env
Environment=PYTHONUNBUFFERED=1
Environment=HOME={{ hermes_exec_home }}
Environment=HERMES_HOME={{ hermes_exec_hermes_home }}
Environment=LM_BASE_URL={{ hermes_exec_lm_base_url }}
Environment=LM_API_KEY={{ hermes_exec_lm_api_key }}
Environment=HERMES_BIN={{ hermes_exec_bin }}
Environment=HERMES_EXEC_PORT={{ hermes_exec_port }}
Environment=HERMES_EXEC_TIMEOUT={{ hermes_exec_timeout }}
Environment=HERMES_EXEC_LOG={{ hermes_exec_log }}
ExecStart=/usr/bin/python3 /usr/local/bin/hermes-exec-server
Restart=on-failure
RestartSec=15s
StandardOutput=journal
StandardError=journal
SyslogIdentifier=hermes-exec
[Install]
WantedBy=multi-user.target