"""Redact secrets out of a settings view before an agent ever sees it. The settings-meta read tool is always-on, so an always-on exfiltration risk: a prompt-injected agent that could read raw settings could mail your API keys out. So the read tool returns shape + state, never a secret VALUE. Keys are write-only from the agent's side: it can SET a new one, never SEE the old. The secret set is derived from field NAMES, not a hand-kept list that silently drifts the day someone adds a new credential. Rule: a field whose name ends in `_key`, `_token`, or `_secret` is a secret, plus installation_id (a stable machine fingerprint that isn't a credential but still shouldn't leak). A test asserts every field the settings PUT path already treats as secret is caught here, so the two can't diverge. """ from __future__ import annotations from typing import Any from backend.common.secret_scan import looks_secret P_SECRET_NAME_SUFFIXES = ("_key", "_token", "_secret") # Not a credential and doesn't match the suffix rule, but a stable hardware-ish fingerprint used for cohorting/abuse; keep it out of the agent's eyes too. P_SECRET_EXTRA_FIELDS = frozenset({"installation_id"}) def is_secret_field(name: str) -> bool: return name.endswith(P_SECRET_NAME_SUFFIXES) or name in P_SECRET_EXTRA_FIELDS def p_value_is_secret_shaped(value: Any) -> bool: """Fail-safe behind the name rule: a field the name rule misses (a future secret with an off-convention name) is still caught if its VALUE looks like a credential (sk-..., ghp_..., Bearer ...). So a leak needs BOTH a bad name AND a non-credential-shaped value, not just one.""" return isinstance(value, str) and looks_secret(value) def p_redact_value(value: Any) -> dict[str, Any]: """A secret rendered as state, never content: configured + last 4 only.""" if value is None or (isinstance(value, str) and value.strip() == ""): return {"configured": False} last4 = value[-4:] if isinstance(value, str) and len(value) >= 4 else None return {"configured": True, "last4": last4} def redact_settings(raw: dict[str, Any]) -> dict[str, Any]: """Return a copy of a settings dict with every secret value collapsed to {configured, last4}. Nested custom-provider api_keys are redacted too.""" out: dict[str, Any] = {} for key, value in raw.items(): if is_secret_field(key) or p_value_is_secret_shaped(value): out[key] = p_redact_value(value) elif key == "custom_providers" and isinstance(value, list): out[key] = [p_redact_custom_provider(cp) for cp in value] else: out[key] = value return out def p_redact_custom_provider(cp: Any) -> Any: if not isinstance(cp, dict): return cp out = dict(cp) if "api_key" in out: out["api_key"] = p_redact_value(out.get("api_key")) return out