fix(M9): corrections d'audit FIXME_M9 — secrets, messages, validation, tests

Cinq corrections de l'audit FIXME_M9 :
- redact_secrets() étendue avec extra_secrets pour masquer les clés brutes ;
  providers stockent SecretStr jusqu'à l'appel SDK.
- _build_prompt() inclut le contenu des messages (tronqué à 500 car.) ;
  prompt système renforcé contre l'injection.
- _validate_output() supprime les emojis et rejette titre/liste/HTML → None.
- Tests litellm utilisent importorskip + LITELLM_LOCAL_MODEL_COST_MAP=true.
- .env.example désactive l'IA par défaut (AI_ENABLED=false).
- Documentation §9.2-§9.5 alignée avec l'implémentation (SDK openai, SecretStr,
  factory réelle, validation sortie, politique hors réseau).

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
This commit is contained in:
2026-09-07 19:01:25 +02:00
parent 4b0e2858a6
commit 19cbf8f13f
10 changed files with 687 additions and 250 deletions

View File

@@ -8,8 +8,11 @@ les messages d'erreur ou les traces du pipeline ``pronote-sync``.
from __future__ import annotations
import re
from collections.abc import Iterable
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
from pydantic import SecretStr
_SENSITIVE_QUERY_KEYS = frozenset(
{
"icalsecurise",
@@ -73,7 +76,7 @@ def redact_url(url: str) -> str:
return _REDACTED_URL
def redact_secrets(text: str) -> str:
def redact_secrets(text: str, extra_secrets: Iterable[SecretStr | str] = ()) -> str:
"""Masque les secrets présents dans un texte arbitraire.
Les URLs sont d'abord traitées par :func:`redact_url`, puis les en-têtes
@@ -82,13 +85,28 @@ def redact_secrets(text: str) -> str:
(ex: ``icalsecurise=XXX``, ``"token": "XXX"``) sont masquées, sans
distinction de casse.
Les valeurs sensibles additionnelles fournies via ``extra_secrets``
(clés API brutes, jetons, mots de passe, etc.) sont ensuite remplacées
littéralement, par ``str.replace``, par ``REDACTED`` dans le texte, y
compris lorsqu'elles n'apparaissent pas sous une forme ``cle=valeur``
reconnue. Une valeur vide ou ``None`` est ignorée.
:param text: Texte pouvant contenir des URLs ou des secrets en clair.
:param extra_secrets: Itérable de secrets bruts (``str`` ou
:class:`pydantic.SecretStr`) à masquer. Les valeurs vides ou
``None`` sont ignorées.
:return: Texte avec les secrets remplacés par ``REDACTED``.
:rtype: str
"""
redacted = _URL_PATTERN.sub(lambda match: redact_url(match.group(0)), text)
redacted = _AUTH_HEADER_PATTERN.sub(r"\1: REDACTED", redacted)
return _ISOLATED_SECRET_PATTERN.sub(r"\1\2\3REDACTED", redacted)
redacted = _ISOLATED_SECRET_PATTERN.sub(r"\1\2\3REDACTED", redacted)
for secret in extra_secrets:
value: str | None = secret.get_secret_value() if isinstance(secret, SecretStr) else secret
if not value:
continue
redacted = redacted.replace(value, _REDACTED)
return redacted
def redact_exception(exc: Exception) -> str: