Files
openswarm/backend/apps/settings/redaction.py
T

69 lines
2.8 KiB
Python

"""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
_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.
_SECRET_EXTRA_FIELDS = frozenset({"installation_id"})
def is_secret_field(name: str) -> bool:
return name.endswith(_SECRET_NAME_SUFFIXES) or name in _SECRET_EXTRA_FIELDS
def _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 _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 _value_is_secret_shaped(value):
out[key] = _redact_value(value)
elif key == "custom_providers" and isinstance(value, list):
out[key] = [_redact_custom_provider(cp) for cp in value]
else:
out[key] = value
return out
def _redact_custom_provider(cp: Any) -> Any:
if not isinstance(cp, dict):
return cp
out = dict(cp)
if "api_key" in out:
out["api_key"] = _redact_value(out.get("api_key"))
return out