fix(M11): propagate PipelineCriticalError, redact configured secrets, signal blog failures

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>
This commit is contained in:
2026-09-08 12:20:29 +02:00
parent 26b083561a
commit 28c695795a
7 changed files with 672 additions and 14 deletions

View File

@@ -89,7 +89,9 @@ def redact_secrets(text: str, extra_secrets: Iterable[SecretStr | str] = ()) ->
(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.
reconnue. Une valeur vide ou ``None`` est ignorée. Les secrets sont
appliqués du plus long au plus court afin qu'un secret qui est une
sous-chaîne d'un autre soit remplacé en premier, sans être corrompu.
:param text: Texte pouvant contenir des URLs ou des secrets en clair.
:param extra_secrets: Itérable de secrets bruts (``str`` ou
@@ -101,19 +103,25 @@ def redact_secrets(text: str, extra_secrets: Iterable[SecretStr | str] = ()) ->
redacted = _URL_PATTERN.sub(lambda match: redact_url(match.group(0)), text)
redacted = _AUTH_HEADER_PATTERN.sub(r"\1: REDACTED", redacted)
redacted = _ISOLATED_SECRET_PATTERN.sub(r"\1\2\3REDACTED", redacted)
values: list[str] = []
for secret in extra_secrets:
value: str | None = secret.get_secret_value() if isinstance(secret, SecretStr) else secret
if not value:
continue
values.append(value)
for value in sorted(values, key=len, reverse=True):
redacted = redacted.replace(value, _REDACTED)
return redacted
def redact_exception(exc: Exception) -> str:
def redact_exception(exc: Exception, extra_secrets: Iterable[SecretStr | str] = ()) -> str:
"""Masque les secrets dans la représentation textuelle d'une exception.
:param exc: Exception dont le message doit être rédigé.
:param extra_secrets: Itérable de secrets bruts (``str`` ou
:class:`pydantic.SecretStr`) à masquer, transmis à
:func:`redact_secrets`. Les valeurs vides ou ``None`` sont ignorées.
:return: Représentation textuelle de l'exception avec les secrets masqués.
:rtype: str
"""
return redact_secrets(str(exc))
return redact_secrets(str(exc), extra_secrets)