mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
69 lines
2.8 KiB
Python
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
|