Assistant IA optionnel (analyse de sortie, diagnostic, generation de commande)

Nouvelle couche app/ai/ (provider OpenAI-compatible via urllib, redaction de
secrets, detection de commandes dangereuses, parametres, worker QThread) et son
integration dans chaque onglet terminal via un bouton "Assistant IA" (memes
conventions que le bouton Snippets existant) :

- Expliquer cette sortie / Diagnostiquer cette erreur / Proposer une solution /
  Poser une question... : utilisent la selection courante du terminal, affichees
  dans un panneau lateral (memoire uniquement, jamais journalise) reutilisant le
  splitter comme le panneau SFTP.
- Generer une commande : ne s'execute jamais automatiquement (Copier / Inserer
  dans le terminal sans Entree / Annuler), avec avertissement si la commande
  proposee correspond a un motif destructeur connu.
- Avant tout envoi : avis unique (persist) puis detection de secrets evidents
  (mot de passe, cle, jeton) avec choix de masquer, envoyer tel quel ou annuler.

Desactive par defaut, sans liste de fournisseur figee (URL + modele libres,
compatible OpenAI/OpenRouter/LLM local). La cle API passe exclusivement par
CredentialStore (jamais dans settings.json ni journalisee). Parametrable via
le nouveau menu "Assistant IA > Parametres..." (bouton "Tester la connexion").

Bug trouve par les tests avant commit : un panneau nouvellement insere dans un
QSplitter deja visible est affiche automatiquement par Qt, ce qui inversait le
premier clic sur "Afficher le panneau IA" ; corrige en le masquant explicitement
a sa creation.

9 nouveaux fichiers de test (provider, redaction, dangers, parametres, worker,
confirmations de securite, dialogue de commande, panneau, integration onglet) :
39 verifications, toutes au vert. Suite complete (31 fichiers) executee sans
regression.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Florian
2026-09-05 05:48:13 +02:00
parent 9216f636bd
commit 0b2ff38bfd
22 changed files with 2081 additions and 8 deletions

52
app/ai/redaction.py Normal file
View File

@@ -0,0 +1,52 @@
"""Detection/redaction basique de secrets avant envoi a un fournisseur IA
externe : cles privees SSH, jetons Bearer, cles AWS, en-tetes Authorization,
password=/token=/api_key=, et longues chaines ressemblant a un secret.
Heuristique volontairement simple (comme la detection de commandes
dangereuses) : mieux vaut avertir a tort sur un hash ou un identifiant anodin
que laisser passer un vrai secret."""
from __future__ import annotations
import re
from dataclasses import dataclass
_PATTERNS: list[tuple[str, re.Pattern]] = [
("Cle privee (PEM/OpenSSH)", re.compile(
r"-----BEGIN (?:RSA |EC |OPENSSH |DSA |ENCRYPTED )?PRIVATE KEY-----"
)),
("Jeton Bearer", re.compile(r"\bBearer\s+[A-Za-z0-9\-_.=]{10,}", re.IGNORECASE)),
("Cle d'acces AWS", re.compile(r"\b(?:AKIA|ASIA)[0-9A-Z]{16}\b")),
("En-tete Authorization", re.compile(r"\bAuthorization\s*:\s*\S+", re.IGNORECASE)),
("Mot de passe (password=)", re.compile(r"\b(?:password|passwd|pwd)\s*[:=]\s*\S+", re.IGNORECASE)),
("Cle API (api_key=)", re.compile(r"\b(?:api[_-]?key|apikey)\s*[:=]\s*\S+", re.IGNORECASE)),
("Jeton (token=)", re.compile(r"\btoken\s*[:=]\s*\S+", re.IGNORECASE)),
("Chaine longue ressemblant a un secret", re.compile(r"\b[A-Za-z0-9_\-]{32,}\b")),
]
@dataclass
class DetectedSecret:
label: str
excerpt: str
def detect_secrets(text: str) -> list[DetectedSecret]:
"""Renvoie les categories detectees (au plus une occurrence par categorie,
pour un resume lisible), dans l'ordre des motifs (du plus specifique au
plus generique)."""
found: list[DetectedSecret] = []
for label, pattern in _PATTERNS:
match = pattern.search(text)
if match:
excerpt = match.group(0)
if len(excerpt) > 60:
excerpt = excerpt[:57] + "..."
found.append(DetectedSecret(label, excerpt))
return found
def redact(text: str) -> str:
redacted = text
for _label, pattern in _PATTERNS:
redacted = pattern.sub("[MASQUE]", redacted)
return redacted