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).
46 lines
1.9 KiB
Python
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"]
|