mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-08 20:10:34 +00:00
* fix(memory): mask backend secrets in the memory config responses GET /api/memory/config and /api/memory/status returned memory.backend_config as loaded. Config loading has already resolved $VAR references by then, so a DeerMem extraction model api_key or a Honcho api_key reached every caller with memory:read in plaintext, which with authorization disabled (the default) is every signed-in user. MemoryConfigResponse now masks credential-named values (api_key, *_token, password, authorization, ...) at any depth as "***". The key is matched on its end, so knobs such as token_counting and guaranteed_token_budget and the api_key_env variable name stay readable, and unset values stay null or empty. The loaded config the backend uses is not changed. * docs(changelog): reference #7665 for the memory config secret masking fix * refactor(gateway): share config secret masking between MCP and memory The memory config masking forked the MCP router's key normalization and recursive masker. Both now use app/gateway/secret_masking.py, and each router keeps its own key rule: MCP matches credential terms anywhere in the key, memory matches them at the end and keeps unset values. MCP masking is unchanged. The memory rule now also covers plural and cookie keys (api_keys, secrets, passwords, cookies). Plural tokens stays out because max_injection_tokens is a DeerMem knob. A secret-named container is still masked whole, since its keys can be arbitrary names; the helper docstring and a test now say so. * docs(changelog): move the #7665 entry to Unreleased
31 lines
1.4 KiB
Python
31 lines
1.4 KiB
Python
"""Shared masking for credential values that Gateway routes echo back from config."""
|
|
|
|
import re
|
|
from collections.abc import Callable
|
|
from typing import Any
|
|
|
|
MASKED_VALUE = "***"
|
|
|
|
|
|
def normalize_config_key(key: str) -> str:
|
|
"""Snake-case a config key so ``apiKey``, ``API-Key`` and ``api_key`` compare equal."""
|
|
with_boundaries = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", key)
|
|
with_boundaries = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", with_boundaries)
|
|
return re.sub(r"[^a-z0-9]+", "_", with_boundaries.lower()).strip("_")
|
|
|
|
|
|
def mask_secret_values(value: Any, is_secret_key: Callable[[str], bool], *, keep_unset: bool = False) -> Any:
|
|
"""Return a copy of ``value`` with every value under a secret-named key masked, at any depth.
|
|
|
|
A secret-named key masks its whole value, containers included: a
|
|
``credentials`` mapping can hold keys under arbitrary names, so masking only
|
|
its secret-named leaves could pass one through. With ``keep_unset``, a
|
|
``None`` or ``""`` value stays as it is, so a response still shows that no
|
|
secret is configured.
|
|
"""
|
|
if isinstance(value, dict):
|
|
return {key: MASKED_VALUE if is_secret_key(str(key)) and not (keep_unset and nested in (None, "")) else mask_secret_values(nested, is_secret_key, keep_unset=keep_unset) for key, nested in value.items()}
|
|
if isinstance(value, list):
|
|
return [mask_secret_values(item, is_secret_key, keep_unset=keep_unset) for item in value]
|
|
return value
|