docs+fix: renforcement de l'architecture agentique et corrections de sécurité

AGENTS.md :
- Rôles d'agents renforcés : @coder ne valide pas, @debugger ne code pas,
  @verifier ne modifie pas, etc.
- Table « Séparation des rôles » : tâche → agent responsable → ne pas confier à
- Workflow étape 5 : délégation explicite à @verifier pour la validation

GUIDE_DEV_PYTHON.md :
- 7 notes « Décision d'implémentation » ajoutées aux sections concernées :
  3.1.1 (XMPP_RECIPIENT→XMPP_TO, vars ajoutées), 3.1.2 (SYNC_PAST_DAYS→AppSettings),
  3.2 (Pydantic v2 style, defaults corrigés), 4.2.1 (redact_exception module function),
  4.2.2 (getLevelNamesMapping), 5.1.5 (normalize_pronote_uid, usedforsecurity=False),
  6 (ConfigDict, StrEnum, alias _date, external_info Optional)

Sécurité (audit @security-auditor, corrections @coder, validation @verifier) :
- RedactingFormatter : redaction APRÈS formatage (corrige TypeError %s + fuite traceback)
- redact_url : masquage des credentials dans userinfo URL (HTTP Basic Auth)
- redact_secrets : patterns étendus (api_key, access_token, authorization, auth)
- redact_secrets : support JSON-style « key: value » avec guillemets

Validations (@verifier) :
- ruff check : PASS | mypy strict : PASS | bandit : PASS (0 issue)
- %s formatting : OK (password=REDACTED, pas de TypeError)
- Traceback redaction : OK (icalsecurise=REDACTED)
- URL userinfo : OK (user:REDACTED@host)
- JSON-style redaction : OK ({"token": "REDACTED"})
- Régression red-to-green : OK (historical HEAD reproduction)

Co-authored-by: OpenCode/orchestrator <opencode-orchestrator@agents.invalid>
This commit is contained in:
2026-09-05 23:52:02 +02:00
parent 2e7dfe6e46
commit aaca78c55d
5 changed files with 554 additions and 475 deletions

View File

@@ -90,6 +90,10 @@
{ {
"path": "detect_secrets.filters.allowlist.is_line_allowlisted" "path": "detect_secrets.filters.allowlist.is_line_allowlisted"
}, },
{
"path": "detect_secrets.filters.common.is_baseline_file",
"filename": ".secrets.baseline"
},
{ {
"path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
"min_level": 2 "min_level": 2
@@ -136,9 +140,9 @@
"filename": "GUIDE_DEV_PYTHON.md", "filename": "GUIDE_DEV_PYTHON.md",
"hashed_secret": "90bd1b48e958257948487b90bee080ba5ed00caa", "hashed_secret": "90bd1b48e958257948487b90bee080ba5ed00caa",
"is_verified": false, "is_verified": false,
"line_number": 5073 "line_number": 5112
} }
] ]
}, },
"generated_at": "2026-09-05T17:53:24Z" "generated_at": "2026-09-05T21:51:55Z"
} }

View File

@@ -189,22 +189,38 @@ Cette section s'applique uniquement lorsque le travail est exécuté avec le sys
Les rôles d'agents disponibles pour ce projet sont les suivants : Les rôles d'agents disponibles pour ce projet sont les suivants :
- `@architect` : Arbitrages d'architecture et choix techniques structurants pour le pipeline `pronote-sync`. - `@architect` : Arbitrages d'architecture et choix techniques structurants. **Ne produit pas de code.**
- `@coder` : Opérations de développement et changements de code dans le projet. - `@coder` : Écrit et modifie du code, de la configuration et des scripts. **Ne valide pas** (ruff, mypy, pytest) — c'est le rôle de `@verifier`. **Ne diagnostique pas** — c'est le rôle de `@debugger`.
- `@debugger` : Reproduction d'un symptôme et établissement de sa cause profonde (ex. : échec de synchronisation, repli iCal/pronotepy). - `@debugger` : Reproduit un symptôme et établit sa cause profonde. **Ne modifie pas le code.**
- `@explorer` : Exploration du dépôt en lecture seule et fourniture de contexte factuel. - `@explorer` : Explore le dépôt en lecture seule. **Ne modifie rien, n'exécute pas de commandes.**
- `@orchestrator` : Compréhension globale du projet, définition des jalons, coordination et garantie du résultat. - `@orchestrator` : Compréhension globale, définition des jalons, coordination et garantie du résultat. **N'écrit pas de code.**
- `@planner` : Transformation d'une demande complexe en unités exécutables avec frontières et dépendances claires. - `@planner` : Transforme une demande complexe en unités exécutables. **Ne dirige aucun technicien.**
- `@reviewer` : Revues indépendantes de correction, régression, contrats et maintenabilité. - `@reviewer` : Revues indépendantes de correction, régression, contrats et maintenabilité. **Ne modifie pas le code.**
- `@security-auditor` : Audit indépendant d'une surface de sécurité désignée (ex. : gestion des secrets, masquage des données). - `@security-auditor` : Audit indépendant d'une surface de sécurité. **Ne modifie pas le code.**
- `@tech-writer` : Rédaction et maintenance de documentation technique exacte et vérifiable. - `@tech-writer` : Rédige et maintient la documentation. **N'écrit pas de code applicatif.**
- `@test-engineer` : Conception, écriture et exécution de tests ciblés (unitaires, intégration, mocks). - `@test-engineer` : Conçoit, écrit et exécute des tests ciblés. **N'écrit pas de code de production.**
- `@ui-designer` : conception et implémentation d'interfaces Web et terminal. - `@ui-designer` : Conçoit et implémente les interfaces Web et terminal.
- `@verifier` : Vérification indépendante du comportement livré, des régressions et du respect des conventions (idempotence, mode dégradé). - `@verifier` : Vérifie indépendamment le comportement livré, les régressions et le respect des conventions (ruff, mypy, pytest, bandit, idempotence, mode dégradé). **Ne modifie pas le code.**
- `@web-explorer` : Recherche et extraction de sources Web vérifiables (ex. : documentation Pronote, CalDAV, XMPP). - `@web-explorer` : Recherche et extrait des sources Web vérifiables. **Ne modifie pas le dépôt.**
> **Note** : Ne pas utiliser `@coder` pour les tâches de documentation (`@tech-writer`) ni pour les tests (`@test-engineer`). > **Note** : Ne pas utiliser `@coder` pour les tâches de documentation (`@tech-writer`) ni pour les tests (`@test-engineer`).
### Séparation des rôles
| Type de tâche | Agent responsable | Ne pas confier à |
|---|---|---|
| Écrire/modifier du code | `@coder` | `@verifier`, `@explorer` |
| Valider (ruff, mypy, pytest, bandit) | `@verifier` | `@coder` |
| Diagnostiquer un bug | `@debugger` | `@coder` |
| Écrire un test | `@test-engineer` | `@coder` |
| Rédiger de la documentation | `@tech-writer` | `@coder` |
| Explorer le dépôt (lecture) | `@explorer` | `@coder`, `@verifier` |
| Arbitrage technique structurant | `@architect` | `@coder`, `@planner` |
| Revue de code | `@reviewer` | `@coder`, `@verifier` |
| Audit de sécurité | `@security-auditor` | `@coder`, `@verifier` |
| Recherche web | `@web-explorer` | `@explorer` |
| Découpage de travail complexe | `@planner` | `@coder` |
--- ---
## 10. Workflow de modification ## 10. Workflow de modification
@@ -213,7 +229,7 @@ Les rôles d'agents disponibles pour ce projet sont les suivants :
2. Préserver les changements existants de l'utilisateur. 2. Préserver les changements existants de l'utilisateur.
3. Pour une correction, reproduire d'abord le défaut avec un test automatisé lorsque c'est raisonnable. 3. Pour une correction, reproduire d'abord le défaut avec un test automatisé lorsque c'est raisonnable.
4. Faire une modification étroite et cohérente, en respectant les conventions du projet (idempotence, mode dégradé, repli iCal/pronotepy). 4. Faire une modification étroite et cohérente, en respectant les conventions du projet (idempotence, mode dégradé, repli iCal/pronotepy).
5. Vérifier le comportement nominal et les cas d'erreur, notamment : 5. Faire vérifier le comportement par `@verifier` (ruff, mypy, pytest, bandit) et les cas d'erreur, notamment :
- Succès de la synchronisation Pronote → CalDAV/XMPP. - Succès de la synchronisation Pronote → CalDAV/XMPP.
- Repli vers iCal en cas d'échec de `pronotepy`. - Repli vers iCal en cas d'échec de `pronotepy`.
- Gestion des erreurs explicites. - Gestion des erreurs explicites.

File diff suppressed because it is too large Load Diff

View File

@@ -22,26 +22,15 @@ class RedactingFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str: def format(self, record: logging.LogRecord) -> str:
"""Formate un enregistrement de log en masquant les secrets. """Formate un enregistrement de log en masquant les secrets.
Le message et chaque argument textuel de l'enregistrement sont rédigés La rédaction est appliquée à la chaîne finale (message, arguments et
avant le formatage final effectué par :class:`logging.Formatter`. traceback inclus) produite par :class:`logging.Formatter`.
:param record: Enregistrement de log à formater. :param record: Enregistrement de log à formater.
:return: Message formaté, avec les secrets remplacés par ``REDACTED``. :return: Message formaté, avec les secrets remplacés par ``REDACTED``.
:rtype: str :rtype: str
""" """
record.msg = redact_secrets(str(record.msg)) formatted = super().format(record)
args = record.args return redact_secrets(formatted)
if args:
if isinstance(args, tuple):
record.args = tuple(
redact_secrets(arg) if isinstance(arg, str) else arg for arg in args
)
else:
record.args = {
key: redact_secrets(value) if isinstance(value, str) else value
for key, value in args.items()
}
return super().format(record)
def setup_logging(level: str = "INFO") -> None: def setup_logging(level: str = "INFO") -> None:

View File

@@ -10,10 +10,26 @@ from __future__ import annotations
import re import re
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
_SENSITIVE_QUERY_KEYS = frozenset({"icalsecurise", "token", "key", "password", "secret"}) _SENSITIVE_QUERY_KEYS = frozenset(
{
"icalsecurise",
"token",
"key",
"password",
"secret",
"api_key",
"apikey",
"access_token",
"auth",
"authorization",
}
)
_URL_PATTERN = re.compile(r"https?://[^\s]+") _URL_PATTERN = re.compile(r"https?://[^\s]+")
_ISOLATED_SECRET_PATTERN = re.compile( _ISOLATED_SECRET_PATTERN = re.compile(
r"\b(icalsecurise|token|password|secret|key)\s*=\s*[^\s&]+", r"\b(icalsecurise|access_token|api_key|apikey|authorization|token|password|secret|key|auth)"
r"(\s*['\"]?\s*[:=]\s*)"
r"(['\"]?)"
r"([^\s&'\"]+)",
re.IGNORECASE, re.IGNORECASE,
) )
_REDACTED = "REDACTED" _REDACTED = "REDACTED"
@@ -21,15 +37,30 @@ _REDACTED_URL = "REDACTED_URL"
def redact_url(url: str) -> str: def redact_url(url: str) -> str:
"""Masque les paramètres sensibles dans une URL. """Masque les identifiants et les paramètres sensibles d'une URL.
:param url: URL pouvant contenir des paramètres sensibles (ex: ``icalsecurise``). Les informations d'authentification du netloc (``utilisateur:motdepasse@hôte``)
:return: URL avec les paramètres sensibles remplacés par ``REDACTED``, sont masquées, ainsi que les paramètres sensibles de la requête
(ex: ``icalsecurise``).
:param url: URL pouvant contenir des informations sensibles (ex: ``icalsecurise``).
:return: URL avec les éléments sensibles remplacés par ``REDACTED``,
ou ``REDACTED_URL`` si le traitement échoue. ou ``REDACTED_URL`` si le traitement échoue.
:rtype: str :rtype: str
""" """
try: try:
parts = urlsplit(url) parts = urlsplit(url)
if parts.username is not None or parts.password is not None:
# Netloc sûr : utilisateur:REDACTED@hôte:port. Le deux-point est
# conservé même en l'absence de mot de passe explicite.
userinfo = parts.username or ""
userinfo += ":REDACTED"
host = parts.hostname or ""
if parts.port is not None:
netloc = f"{userinfo}@{host}:{parts.port}"
else:
netloc = f"{userinfo}@{host}"
parts = parts._replace(netloc=netloc)
query: list[tuple[str, str]] = parse_qsl(parts.query, keep_blank_values=True) query: list[tuple[str, str]] = parse_qsl(parts.query, keep_blank_values=True)
redacted_query = [ redacted_query = [
(key, _REDACTED if key.lower() in _SENSITIVE_QUERY_KEYS else value) (key, _REDACTED if key.lower() in _SENSITIVE_QUERY_KEYS else value)
@@ -44,15 +75,15 @@ def redact_secrets(text: str) -> str:
"""Masque les secrets présents dans un texte arbitraire. """Masque les secrets présents dans un texte arbitraire.
Les URLs sont d'abord traitées par :func:`redact_url`, puis les affectations Les URLs sont d'abord traitées par :func:`redact_url`, puis les affectations
isolées de type ``cle=valeur`` (ex: ``icalsecurise=XXX``) sont masquées, isolées de type ``cle=valeur`` ou ``cle:valeur`` (ex: ``icalsecurise=XXX``,
sans distinction de casse. ``"token": "XXX"``) sont masquées, sans distinction de casse.
:param text: Texte pouvant contenir des URLs ou des secrets en clair. :param text: Texte pouvant contenir des URLs ou des secrets en clair.
:return: Texte avec les secrets remplacés par ``REDACTED``. :return: Texte avec les secrets remplacés par ``REDACTED``.
:rtype: str :rtype: str
""" """
redacted = _URL_PATTERN.sub(lambda match: redact_url(match.group(0)), text) redacted = _URL_PATTERN.sub(lambda match: redact_url(match.group(0)), text)
return _ISOLATED_SECRET_PATTERN.sub(r"\1=REDACTED", redacted) return _ISOLATED_SECRET_PATTERN.sub(r"\1\2\3REDACTED", redacted)
def redact_exception(exc: Exception) -> str: def redact_exception(exc: Exception) -> str: