Ajoute le mode d'authentification PRONOTE_AUTH_MODE=qr_token comme alternative
au mode password pour les instances Pronote utilisant HubEduConnect/EduConnect
où l'authentification par mot de passe échoue (CAPTCHA, MFA, flux SAML).
Nouveaux éléments :
- PronoteSettings : auth_mode, qr_code_file, qr_pin (SecretStr)
- PronoteAuthState : persistance du token rotatif dans .pronote_auth_state.json
(écriture atomique, permissions 0600, symlink-safe via O_EXCL|O_NOFOLLOW)
- PronoteClient._connect_qr_token() : token_login avec creds persistés,
qrcode_login pour l'enrôlement initial, export_credentials persisté après
chaque login réussi
- PronoteAuthRotationError : levée en cas d'échec de rotation du token,
propagée sans wrapping à travers PronoteFetcher et fetch_step jusqu'à
PipelineRunner.run() qui notifie via XMPP (si canal disponible et dry_run inactif)
- _is_pronotepy_configured() mode-aware : qr_token ne requiert que PRONOTE_URL
- _collect_auth_secrets() : redaction des secrets explicites (token, PIN, jeton QR)
dans tous les logs du chemin d'authentification
Documentation :
- .env.example : PRONOTE_AUTH_MODE, PRONOTE_QR_CODE_FILE, PRONOTE_QR_PIN
- AGENTS.md : contrat d'authentification QR code / token
- Wiki GuidePronote : section enrôlement, exécutions suivantes, ré-enrôlement
Tests (686 passés, couverture 94.87%) :
- 5 tests config QR, 9 tests auth_state, 10 tests client QR, 3 tests propagation,
4 tests intégration rotation end-to-end, 4 tests fallback mode-aware
- Tests de non-fuite : sentinelles distinctes pour token, PIN, jeton QR
Co-authored-by: coder/litellm/coder <coder@agents.invalid>
Correct 4 findings from the independent M11 review:
#1 (Critical) — PipelineCriticalError was downgraded to PipelineWarning:
- Add except PipelineCriticalError: raise before each except Exception
in all 5 non-blocking steps (fetch_blog, compare, caldav_sync, synthesis, send)
- Critical errors now propagate to the outer handler and stop the pipeline
#2 (Critical) — redact_exception() did not use configured secrets:
- Extend redact_exception() with extra_secrets parameter (upward compatible)
- Harden redact_secrets(): sort extra_secrets by length descending
- Add Settings.redaction_secrets() collecting all 6 SecretStr fields
- Add PipelineRunner._redact(exc) using self._redaction_secrets
- All except blocks in run() now use self._redact(exc)
- CalDAV FAILED-status path uses full redaction_secrets collection
#3 (Medium) — BlogRSSClient silently swallowed failures:
- Add error field to BlogRSSFetchResult
- rss.py sets error on failure paths (except Exception, bozo/invalid feed)
- fetch_blog_step raises RuntimeError when result.error is set
- PipelineRunner now produces PipelineWarning for blog failures
#4 (Medium) — Test coverage at 80%, now 91%:
- 11 new integration tests covering blog failure/success, compare failure,
CalDAV failure (exception + FAILED status), send False/exception,
PipelineCriticalError propagation, secret redaction with sentinel,
empty agenda/homework, iCal cache cleanup
- Secret redaction test uses mock (no network) and proves configured-secret
propagation via non-URL sentinel in RuntimeError
Validation: 619 tests pass, ruff/mypy/bandit/pre-commit green, coverage 91%.
Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Fix all 8 findings from the independent review (FIXME_M10.md):
#1 Transport compatible with slixmpp 1.17.0 (D5):
- Use real ClientXMPP type (remove Any), JID with resource
- connect(host, port) explicit, no use_tls kwarg
- enable_direct_tls/enable_starttls configured before connect
- Single timeout via asyncio.Future for session_start/failed_auth/disconnected
- Remove premature 'starttls' in features check, remove auto_reconnect
- try/finally guarantees disconnect on all paths (#4)
#2 Factory dry_run no longer bypassed (D6):
- Single send() entry point in SyncXmppChannel
- dry_run check before any ClientXMPP creation
- Remove XmppChannel.send() dual implementation
#3 Thread daemon removed — single asyncio.run(), documented limitation
#5 Richer message format:
- Target date header, change type [Ajouté/Supprimé/Modifié]
- Lesson times, homework due date, message author
- No pronote_messages duplication (external_info = blog + other_info only)
#6 Error contract unified (D6):
- Channel.send() -> bool never raises PipelineWarning
- Errors logged with redaction, returns False
- PipelineWarning(step='xmpp') will be created by pipeline M11
#7 Tests faithful to slixmpp 1.17.0 API:
- FakeClientXMPP with real connect(host,port)/disconnect() signatures
- Assertions on host, port, resource, mtype='chat'
- No RuntimeWarning from unawaited coroutines
#8 .secrets.baseline restored from main
Coverage: 96.44% on channels/, 600 tests pass, pre-commit all-files green.
Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Corrige les 5 constats de l'audit FIXME_M7 :
#1 (Bloquant) — Protection des événements non marqués :
- upsert_event() vérifie le marqueur X-PRONOTE-SYNC-MANAGED avant
modification ; lève PronoteSyncError en cas de collision avec un
événement non géré (aucune écriture)
- delete_event() vérifie le marqueur ; no-op avec warning si non géré
- Méthode privée _is_managed_event() factorisant le contrôle
#2 (Bloquant) — Fenêtre de synchronisation :
- Calcul en journées entières (minuit à minuit exclusif)
- Filtrage des données locales (lessons, homeworks, school_events) avant
passage au planner
- Paramètre now injectable pour les tests
#3 (Bloquant) — UID canonique vs brut :
- list_managed_events() retourne (raw_uid, canonical_uid, vevent)
- compute_plan() matche par UID canonique, route les raw UID vers
*_to_remove, retourne le mapping remote_raw_by_canonical
- executor.execute() utilise le raw UID pour les mises à jour (pas de
doublon)
- Pas de migration destructive des UID distants existants
#4 (Correction) — Normalisation temporelle UTC :
- normalize_datetime_to_utc() dans utils/uid.py : naïve → Europe/Paris →
UTC ; consciente → UTC
- Utilisée par generate_deterministic_uid() et component_to_signature()
- Deux représentations du même instant → même UID et même signature
#5 (Compatibilité) — date_search déprécié :
- Remplacement par calendar.search(start, end, event=True, expand=True)
Documentation :
- GUIDE_DEV_PYTHON.md : suppression des références obsolètes à
sync/state.py et état SQLite/JSON ; mise à jour de l'API CalDAV
(search au lieu de date_search, upsert par UID)
- TODO.md : M7 décoché (corrections en cours de validation)
Tests : 390 passés, couverture 95.61%
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>