Files
hermes-agent/agent/monitoring/redaction.py
teknium1 226df89f74 refactor(redact): one secret-pattern source; a2a, gateway chat and monitoring egress scrub through redact_for_egress
plugins/platforms/a2a/security.py::redact_outbound shipped text to a REMOTE peer
through 8 private regexes (sk-, sk-ant-, ghp_ only, xox[bap] only, AKIA, JWT,
Bearer, email) and never called redact_sensitive_text, so every prefix added to
agent/redact.py (hf_, glpat-, xapp-, npm_, Telegram bot tokens, private keys,
DB URLs, env assignments, auth headers, plugin-registered patterns) was absent
on the A2A path. gateway/run.py::_GATEWAY_SECRET_PATTERNS and
agent/monitoring/redaction.py::_TOKEN_RE/_BEARER_RE were two more parallel
"fallback" lists to maintain.

Now agent/redact.py::redact_for_egress is the one egress scrub:
redact_sensitive_text(force=True) + a bearer sweep for prefix-less opaque
tokens, fail-closed ("[redaction-unavailable]"). Gateway user-facing text,
monitoring export and A2A outbound call it; A2A keeps only its e-mail pass.

Behavior changes: a2a egress now masks the full canonical set; the gateway
chat path returns the fail-closed sentinel instead of a raw string when the
redactor raises; honcho plugin registers hch-at-/hch-rt- with
register_redaction_patterns (masked on every surface; mask shape is the
shared head/tail form instead of "hch-at-[redacted]"); proxy_cli token
display uses mask_secret (4 visible prefix chars instead of 12).

Invariant test: redact_outbound masks a synthesized token for every
registered prefix pattern (fails when reverted to the private list).
2026-09-13 05:07:50 -07:00

46 lines
1.9 KiB
Python

"""Redaction applied to monitoring data before egress.
One unconditional scrub, no modes, no knobs. Every string that leaves the process passes
through ``redact_for_export``: secrets via ``agent/redact.py::redact_for_egress`` (the single
pattern source; fails CLOSED so a broken redactor never emits the raw string), then PII
(e-mail, phone, UUID-shaped ids -> ``[email]`` / ``[phone]`` / ``[id]``).
"""
from __future__ import annotations
import re
from typing import Any, Optional
from agent.redact import REDACTION_UNAVAILABLE as UNAVAILABLE, redact_for_egress
# ── PII shapes ───────────────────────────────────────────────────────────────
_EMAIL_RE = re.compile(r"[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}")
# E.164-ish and common separators; conservative to avoid nuking code/IDs.
_PHONE_RE = re.compile(
r"(?<!\w)(?:\+?\d{1,3}[\s.\-]?)?(?:\(\d{2,4}\)[\s.\-]?)?\d{3}[\s.\-]?\d{3,4}(?:[\s.\-]?\d{2,4})?(?!\w)"
)
_UUID_RE = re.compile(r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b")
def redact_for_export(text: Optional[str]) -> Optional[str]:
"""Scrub a string for egress: secrets, then PII. Unconditional."""
if text is None:
return None
out = redact_for_egress(str(text))
out = _EMAIL_RE.sub("[email]", out)
out = _UUID_RE.sub("[id]", out)
out = _PHONE_RE.sub("[phone]", out)
return out
def redact_bounded(raw: Any, *, limit: int = 500, empty: str = "[redacted]", unavailable: str = UNAVAILABLE) -> str:
"""Redact ``str(raw or "")`` and length-bound it; ``empty`` replaces an empty
result, ``unavailable`` is returned if redaction itself raises."""
try:
return (redact_for_export(str(raw or "")) or empty)[:limit]
except Exception:
return unavailable
__all__ = ["redact_for_export", "redact_bounded"]