Décisions Architecturales¶
Décisions architecturales clés et leur justification.
Vue d'ensemble¶
Merlya est construit sur une architecture modulaire conçue pour l'extensibilité, la sécurité et la facilité d'utilisation. La version 0.8.0 introduit l'architecture des centres DIAGNOSTIC/CHANGE pour un routage intelligent entre l'investigation en lecture seule et les mutations contrôlées.
graph TB
subgraph "Interface Utilisateur"
CLI[CLI / REPL]
end
subgraph "Couche de Routage"
SmartExtractor[SmartExtractor<br/>Détection d'Hôtes]
CenterClassifier[CenterClassifier<br/>DIAGNOSTIC/CHANGE]
end
subgraph "Centres Opérationnels"
DiagCenter[Centre DIAGNOSTIC<br/>Lecture Seule]
ChangeCenter[Centre CHANGE<br/>Mutations + HITL]
end
subgraph "Pipelines"
Ansible[AnsiblePipeline]
Terraform[TerraformPipeline]
K8s[KubernetesPipeline]
Bash[BashPipeline]
end
subgraph "Infrastructure"
SSH[Pool SSH]
Capabilities[Détecteur de Capacités]
end
subgraph "Fournisseurs LLM"
Brain[Modèle Brain<br/>Raisonnement]
Fast[Modèle Fast<br/>Routage]
end
CLI --> SmartExtractor
SmartExtractor --> CenterClassifier
CenterClassifier -->|lecture seule| DiagCenter
CenterClassifier -->|mutations| ChangeCenter
DiagCenter --> SSH
ChangeCenter --> Capabilities
Capabilities --> Ansible
Capabilities --> Terraform
Capabilities --> K8s
Capabilities --> Bash
CenterClassifier --> Fast
DiagCenter --> Brain
ChangeCenter --> Brain ADR-001 : PydanticAI pour le Framework Agent¶
Statut : Accepté
Contexte : Nous avions besoin d'un framework pour construire des agents alimentés par LLM avec des capacités d'appel d'outils.
Décision : Utiliser PydanticAI comme framework d'agent.
Justification : - Typage sûr avec les modèles Pydantic - Support natif de l'async - API propre pour la définition d'outils - Support multi-fournisseurs - Développement actif et communauté
Conséquences : - Dépendance à PydanticAI - Bénéficie des améliorations en amont - Peut nécessiter une adaptation aux changements d'API
ADR-002 : AsyncSSH pour les Connexions SSH¶
Statut : Accepté
Contexte : La connectivité SSH est au cœur des fonctionnalités de Merlya.
Décision : Utiliser asyncssh pour toutes les opérations SSH.
Justification : - Python pur, pas de dépendances externes - Natif async pour les connexions concurrentes - Support complet du protocole SSH2 - Support intégré des jump hosts - Maintenance active
Conséquences : - API async uniquement - Pooling de connexions nécessaire pour les performances - Surcharge mémoire pour de nombreuses connexions
ADR-003 : Pooling de Connexions¶
Statut : Accepté
Contexte : Les connexions SSH fréquentes sont lentes et consommatrices de ressources.
Décision : Implémenter un pooling de connexions avec nettoyage automatique.
Justification : - Éviter la surcharge de connexion pour les commandes répétées - Limiter les connexions concurrentes - Nettoyage automatique des connexions inactives - Gestion gracieuse de l'arrêt
Implémentation :
class SSHPool:
max_connections: int = 10
idle_timeout: int = 300 # 5 minutes
async def get_connection(host) -> SSHConnection
async def release_connection(conn)
async def cleanup_idle()
ADR-004 : Keyring pour le Stockage des Identifiants¶
Statut : Accepté
Contexte : Les clés API et les identifiants nécessitent un stockage sécurisé.
Décision : Utiliser le keyring système (via la bibliothèque keyring).
Justification : - Sécurité au niveau OS (Keychain, Secret Service, Credential Manager) - Pas de secrets en clair dans les fichiers de configuration - Solution standard multi-plateforme - Fallback en mémoire avec avertissement
Conséquences : - Nécessite un backend keyring sur Linux - Peut nécessiter une interaction utilisateur pour la première configuration - Les serveurs headless nécessitent une configuration alternative
ADR-005 : Classification d'Intent Locale¶
Statut : Remplacé par ADR-013 (v0.8.0)
Contexte : Toutes les entrées utilisateur ne nécessitent pas un traitement LLM.
Décision : Utiliser un classificateur local basé sur des patterns pour le routage d'intent avec fallback LLM.
Note : Cet ADR a été remplacé par ADR-013 (Architecture des Centres DIAGNOSTIC/CHANGE) qui fournit un système de routage plus sophistiqué avec CenterClassifier.
ADR-006 : Configuration YAML avec Stockage SQLite¶
Statut : Accepté
Contexte : La configuration doit être lisible et modifiable par l'humain. L'inventaire des hôtes nécessite un stockage structuré avec des capacités de requêtage.
Décision : Utiliser YAML pour les fichiers de configuration et SQLite pour l'inventaire des hôtes.
Justification : - YAML : Syntaxe conviviale, largement utilisée en DevOps - YAML : Support natif des structures complexes - SQLite : Requêtage rapide pour la recherche d'hôtes - SQLite : Support du tagging, filtrage et recherche - SQLite : Fichier unique, pas de serveur nécessaire
Emplacements des Fichiers : - ~/.merlya/config.yaml - Configuration principale - ~/.merlya/merlya.db - Inventaire des hôtes (SQLite) - ~/.merlya/logs/ - Fichiers de logs - ~/.merlya/history - Historique des commandes
Exemple de Configuration :
ADR-007 : Architecture de Plugins¶
Statut : Proposé
Contexte : Les utilisateurs ont besoin d'étendre Merlya avec des outils personnalisés.
Décision : Système de plugins basé sur les entry-points.
Justification : - Approche standard du packaging Python - Distribution facile via PyPI - Isolation des espaces de noms - Chargement paresseux
Interface Proposée :
# pyproject.toml
[project.entry-points."merlya.tools"]
my_tool = "my_package:MyTool"
# Implémentation
class MyTool(MerlyaTool):
name = "my_tool"
description = "Fait quelque chose d'utile"
async def execute(self, params: dict) -> ToolResult:
...
ADR-008 : Loguru pour la Journalisation¶
Statut : Accepté
Contexte : Le débogage et la surveillance nécessitent une bonne journalisation avec une configuration minimale.
Décision : Utiliser loguru pour la journalisation.
Justification : - Zéro configuration prête à l'emploi - Rotation et rétention automatiques - Sortie colorée lisible pour le développement - Capture d'exceptions avec traceback complet - API facile à utiliser (logger.info(), logger.error()) - Sûr pour l'async
Implémentation :
from loguru import logger
logger.info("✅ Opération terminée avec succès")
logger.warning("⚠️ Nouvelle tentative de connexion requise")
logger.error("❌ Échec de la connexion SSH : {error}", error=e)
Niveaux de Log : - DEBUG : Flux d'exécution détaillé - INFO : Opérations clés et résultats - WARNING : Problèmes récupérables - ERROR : Échecs nécessitant une attention
Convention des Emojis : Tous les messages de log utilisent des emojis pour la clarté visuelle (voir CONTRIBUTING.md).
ADR-009 : Support Multi-Fournisseurs LLM¶
Statut : Accepté
Contexte : Les utilisateurs ont différentes préférences de fournisseurs LLM et contraintes de coûts.
Décision : Interface abstraite de fournisseur LLM avec plusieurs implémentations. OpenRouter par défaut pour l'accès au tier gratuit.
Fournisseurs Supportés : - OpenRouter (défaut) - 100+ modèles, tier gratuit disponible - OpenAI (GPT-4o, GPT-4o-mini) - Anthropic (Claude 3.5 Sonnet, Haiku) - Ollama (modèles locaux, pas de clé API nécessaire)
Interface :
class LLMProvider(Protocol):
async def generate(prompt: str) -> str
async def generate_with_tools(prompt: str, tools: list) -> ToolCall
ADR-010 : Modèle de Sécurité¶
Statut : Accepté
Contexte : Merlya exécute des commandes sur des systèmes distants.
Décision : Implémenter un modèle de sécurité en défense en profondeur.
Couches : 1. Protection des Identifiants - Stockage keyring 2. Revue des Commandes - Confirmation utilisateur pour les commandes destructives 3. Validation des Entrées - Modèles Pydantic pour toutes les entrées 4. Journalisation d'Audit - Toutes les commandes journalisées 5. Principe du Moindre Privilège - Permissions minimales
Détection des Commandes Dangereuses :
ADR-011 : Gestion des Identifiants en Mode Non-Interactif¶
Statut : Accepté (v0.7.8)
Contexte : En mode non-interactif (merlya run --yes), l'agent ne peut pas demander d'identifiants aux utilisateurs. Auparavant, cela menait à des boucles de retry infinies quand les commandes sudo/su nécessitaient des mots de passe.
Décision : Échec rapide avec des messages d'erreur clairs quand les identifiants sont nécessaires mais ne peuvent pas être obtenus.
Justification :
- L'échec immédiat évite les appels API gaspillés et les timeouts
- Les messages d'erreur clairs guident les utilisateurs vers les bonnes solutions
- Trois chemins de résolution documentés : keyring, NOPASSWD, mode interactif
- Le flag
permanent_failureindique à l'agent d'arrêter les retries
Implémentation :
# Dans request_credentials() et ssh_execute()
if ctx.auto_confirm and missing_credentials:
return CommandResult(
success=False,
message="Impossible d'obtenir les identifiants en mode non-interactif",
data={
"non_interactive": True,
"permanent_failure": True, # Signal : ne pas réessayer
}
)
Solutions pour les utilisateurs :
- Stocker les identifiants dans le keyring :
merlya secret set sudo:host:password - Configurer sudo NOPASSWD sur les hôtes cibles
- Exécuter en mode interactif (sans
--yes)
Conséquences :
- Échec clair au lieu de boucles de timeout
- Meilleure intégration CI/CD (échec rapide)
- Nécessite une pré-configuration pour les commandes élevées automatisées
ADR-012 : Enum ElevationMethod pour la Configuration des Hôtes¶
Statut : Accepté (v0.7.8)
Contexte : La méthode d'élévation était stockée sous forme de chaînes avec une validation incohérente, causant des valeurs NULL et des erreurs de validation.
Décision : Utiliser l'enum ElevationMethod avec des valeurs explicites et une gestion appropriée des NULL.
Valeurs de l'Enum :
class ElevationMethod(str, Enum):
NONE = "none" # Pas d'élévation
SUDO = "sudo" # sudo (NOPASSWD)
SUDO_PASSWORD = "sudo_password" # sudo avec mot de passe
DOAS = "doas" # doas (NOPASSWD)
DOAS_PASSWORD = "doas_password" # doas avec mot de passe
SU = "su" # su avec mot de passe
Gestion :
- NULL dans la base de données →
ElevationMethod.NONE - Chaînes invalides →
ElevationMethod.NONE /hosts editutilise le mapping de l'enum- L'import (TOML/CSV) mappe les chaînes vers les valeurs de l'enum
Conséquences :
- Configuration d'élévation type-safe
- Plus d'erreurs de validation sur NULL
- Comportement cohérent sur tous les chemins de code
ADR-013 : Architecture des Centres DIAGNOSTIC/CHANGE¶
Statut : Remplacé par ADR-018 (v0.8.3)
Contexte : Merlya avait besoin d'une séparation claire entre l'investigation en lecture seule et les opérations modifiant l'état pour améliorer la sécurité et fournir des garde-fous appropriés pour chaque type d'opération.
Décision : Implémenter deux centres opérationnels avec des modèles de sécurité différents :
| Centre | Objectif | Niveau de Risque | HITL Requis |
|---|---|---|---|
| DIAGNOSTIC | Investigation en lecture seule | BAS | Non |
| CHANGE | Mutations contrôlées | ÉLEVÉ | Oui |
Centre DIAGNOSTIC :
class DiagnosticCenter(AbstractCenter):
"""Centre d'investigation en lecture seule."""
allowed_tools = [
"ssh_execute", # Avec validation lecture seule
"kubectl_get", "kubectl_describe", "kubectl_logs",
"read_file", "list_directory",
"check_disk_usage", "check_memory",
"analyze_logs", "tail_log",
]
blocked_commands = [
"rm ", "mv ", "chmod", "chown",
"systemctl start/stop/restart",
"kill", "reboot", "shutdown",
]
Centre CHANGE :
class ChangeCenter(AbstractCenter):
"""Centre de mutations contrôlées via Pipelines."""
async def execute(self, deps: CenterDeps) -> CenterResult:
# 1. Détecter les capacités
caps = await self.capabilities.detect_all(deps.target)
# 2. Sélectionner le pipeline approprié
pipeline = self._select_pipeline(caps, deps.task)
# 3. Exécuter le pipeline (inclut le HITL obligatoire)
return await pipeline.execute()
Conséquences :
- Séparation claire des préoccupations
- Sécurité appropriée pour chaque type d'opération
- Toutes les mutations passent par Pipeline + HITL
- Les opérations en lecture seule sont rapides (pas d'approbation nécessaire)
ADR-014 : Système de Pipelines pour les Opérations CHANGE¶
Statut : Accepté (v0.8.0)
Contexte : Toutes les opérations modifiant l'état nécessitent une validation cohérente, un aperçu, une approbation et des capacités de rollback.
Décision : Implémenter des étapes de pipeline obligatoires pour toutes les opérations CHANGE :
Types de Pipelines :
| Pipeline | Commande Dry-run | Cas d'Usage |
|---|---|---|
| AnsiblePipeline | ansible-playbook --check --diff | Configuration, packages, services |
| TerraformPipeline | terraform plan | Infrastructure cloud |
| KubernetesPipeline | kubectl diff | Orchestration de conteneurs |
| BashPipeline | Aperçu uniquement | Fallback pour commandes simples |
Implémentation :
class AbstractPipeline(ABC):
@abstractmethod
async def plan(self) -> PlanResult: ...
@abstractmethod
async def diff(self) -> DiffResult: ...
@abstractmethod
async def apply(self) -> ApplyResult: ...
@abstractmethod
async def rollback(self) -> RollbackResult: ...
async def execute(self) -> PipelineResult:
"""Exécute le pipeline complet avec HITL."""
plan = await self.plan()
diff = await self.diff()
summary = self._generate_summary(plan, diff)
if not await self._request_hitl(summary):
return PipelineResult(aborted=True)
result = await self.apply()
post_check = await self._post_check()
if not post_check.success:
await self.rollback()
return result
Conséquences :
- Gestion cohérente des changements sur tous les outils
- Aperçu obligatoire avant application
- Rollback automatique en cas d'échec
- Piste d'audit complète
ADR-015 : CenterClassifier pour le Routage d'Intent¶
Statut : Remplacé par ADR-018 (v0.8.3)
Contexte : Les requêtes utilisateur doivent être routées vers le centre approprié (DIAGNOSTIC ou CHANGE) en fonction de l'intent.
Décision : Utiliser un classificateur hybride pattern-matching + LLM :
- Pattern Matching (Chemin Rapide) : Patterns regex pour les intents clairs
- Fallback LLM : Modèle fast pour les cas ambigus
- Clarification : Prompt utilisateur quand la confiance < 0.7
Implémentation :
class CenterClassifier:
CONFIDENCE_THRESHOLD = 0.7
async def classify(self, user_input: str) -> CenterClassification:
# 1. Essayer la classification par patterns
result = self._classify_patterns(user_input)
# 2. Fallback LLM si confiance basse
if result.confidence < self.CONFIDENCE_THRESHOLD:
llm_result = await self._classify_with_llm(user_input)
if llm_result.confidence > result.confidence:
result = llm_result
# 3. Demander clarification si toujours ambigu
if result.confidence < self.CONFIDENCE_THRESHOLD:
result.clarification_needed = True
return result
Exemples de Patterns :
| Pattern | Centre |
|---|---|
vérifier statut, afficher logs, qu'est-ce que | DIAGNOSTIC |
redémarrer, déployer, corriger, mettre à jour | CHANGE |
Conséquences :
- Routage rapide pour les intents clairs (< 1ms)
- Classification précise via le fallback LLM
- Défaut sûr vers DIAGNOSTIC en cas d'incertitude
- La clarification utilisateur évite les erreurs de routage
ADR-016 : Configuration des Modèles Brain/Fast¶
Statut : Accepté (v0.8.0)
Contexte : Différentes tâches nécessitent différentes capacités de modèle et compromis de coût.
Décision : Deux rôles de modèle avec configuration séparée :
| Rôle | Objectif | Exemples de Modèles |
|---|---|---|
| brain | Raisonnement complexe, planification, analyse | Claude Sonnet, GPT-4o |
| fast | Routage, fingerprinting, décisions rapides | Claude Haiku, GPT-4o-mini |
Configuration :
Commandes CLI :
/model brain claude-sonnet-4 # Définir le modèle brain
/model fast claude-haiku # Définir le modèle fast
/model show # Afficher la config actuelle
Utilisation dans le Code :
# CenterClassifier utilise le modèle fast
model = ctx.config.get_model("fast")
# Orchestrator utilise le modèle brain
model = ctx.config.get_model("brain")
Conséquences :
- Optimisation des coûts (modèle fast pour les tâches simples)
- Meilleures performances (routage rapide)
- Flexibilité (différents modèles par rôle)
ADR-017 : Détection des Capacités¶
Statut : Accepté (v0.8.0)
Contexte : Le centre CHANGE a besoin de savoir quels outils sont disponibles sur les hôtes cibles pour sélectionner le pipeline approprié.
Décision : Implémenter un module de détection des capacités (merlya/capabilities/) :
class CapabilityDetector:
async def detect_host(self, host: Host) -> HostCapabilities:
return HostCapabilities(
ssh=await self._detect_ssh(host),
tools=[
await self._detect_ansible(),
await self._detect_terraform(),
await self._detect_kubectl(),
await self._detect_git(),
],
web_access=await self._detect_web_access(),
)
Capacités Détectées :
| Outil | Détection | Validation de Config |
|---|---|---|
| Ansible | which ansible | L'inventaire existe |
| Terraform | which terraform | Fichiers .tf présents |
| kubectl | which kubectl | kubeconfig valide |
| git | which git | Répertoire .git |
Cache : - Cache basé sur TTL (24h par défaut) - Invalidé lors des changements d'hôte - Stockage des capacités par hôte
Conséquences :
- Sélection automatique du pipeline
- Pas de configuration manuelle des outils
- Fallback gracieux vers BashPipeline
ADR-018 : Architecture Agent basée sur les Spécialistes (v0.8.3)¶
Statut : Accepté (v0.8.3)
Contexte : L'architecture des Centres (v0.8.0) n'était jamais appelée depuis le REPL. Le MerlyaAgent principal avait tous les outils enregistrés à plat, et l'Orchestrateur était du code mort. Cela créait 3 mécanismes de prévention de boucles contradictoires (seuils 25/50/500) et une surface d'outils surchargée.
Décision : Remplacer les Centres et l'Orchestrateur par un seul MerlyaAgent qui délègue aux agents spécialistes :
| Spécialiste | Objectif | HITL |
|---|---|---|
| DiagnosticSpecialist | Investigation lecture seule | Non |
| ExecutionSpecialist | Mutations (write/restart) | Oui |
| SecuritySpecialist | Audits de sécurité | Non |
| QuerySpecialist | Requêtes inventaire | Non |
Architecture :
MerlyaAgent (prompt système : quand déléguer)
├── delegate_diagnostic(target, task) → DiagnosticSpecialist (blocked_commands appliqués)
├── delegate_execution(target, task) → ExecutionSpecialist (HITL obligatoire)
├── delegate_security(target, task) → SecuritySpecialist
├── delegate_query(question) → QuerySpecialist
└── list_hosts / get_host / ask_user (outils directs)
Raison :
- Séparation claire des responsabilités sans code mort
- Garde-fous appliqués au niveau du spécialiste (HITL, commandes bloquées)
- Mécanisme de routage unique (prompt système de l'agent)
- Limites rationalisées :
DEFAULT_TOOL_RETRIES=3,DEFAULT_TOOL_CALLS_LIMIT=50
Conséquences :
- Code des Centres supprimé (~500 lignes)
- Code mort de l'Orchestrateur supprimé
- Débogage simplifié : point d'entrée unique
- Spécialistes testables indépendamment
ADR-019 : Observabilité In-Memory (v0.8.3)¶
Statut : Accepté (v0.8.3)
Contexte : Aucune visibilité sur les métriques opérationnelles durant une session.
Décision : Ajouter merlya/core/metrics.py (métriques in-memory) et merlya/core/resilience.py (circuit breaker + retry), avec une commande slash /metrics.
Métriques suivies :
merlya_commands_total— exécutions par type/statutmerlya_ssh_duration_seconds— histogramme de latence SSHmerlya_llm_calls_total— appels API LLM par fournisseur/modèlemerlya_pipeline_executions— exécutions de pipelines par type/statutmerlya_retry_attempts_total— comptage des retries pour l'observabilité
Patterns de résilience :
@circuit_breaker(failure_threshold=5, recovery_timeout=60s)— s'ouvre après 5 échecs consécutifs, récupère après 60s@retry(max_attempts=3, exponential_base=2.0)— retries avec backoff exponentiel
Choix de conception :
- Pas de backend externe (Prometheus/Grafana reporté à V2.0)
- Thread-safe avec
threading.Lock(fonctionne en sync et async) - L'histogramme utilise une fenêtre glissante (max 10k observations) pour éviter les fuites mémoire
Conséquences :
- Visibilité opérationnelle via la commande
/metrics - Les circuit breakers préviennent les défaillances en cascade sur SSH/LLM
- Zéro dépendance externe
Considérations Futures¶
En Évaluation¶
- Module Fingerprint - Extraction de signature sémantique pour l'approbation des commandes
- Base de Connaissances - Système de connaissances à trois niveaux (général/validé/observé)
- Refactoring ElevationManager - Élévation explicite simplifiée (pas d'auto-détection)
Implémenté (v0.8.0)¶
Intégration Kubernetes→ KubernetesPipelineIntégration Terraform→ TerraformPipelineIntégration Ansible→ AnsiblePipeline
Implémenté (v0.8.3)¶
Architecture Centres→ Architecture Spécialistes (ADR-018)Pas d'observabilité→ Métriques in-memory + commande/metrics(ADR-019)
Rejeté¶
- Application GUI - Focus sur CLI/API pour l'automatisation
- Entraînement LLM personnalisé - Trop gourmand en ressources pour le scope
- SaaS Multi-tenant - Complexité de sécurité, hors scope
- Embeddings ONNX - Supprimé en v0.8.0 pour simplification (voir ADR-005)