mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
fix(memory): validate Honcho's allow_insecure_http as a boolean (#5887)
* fix(memory): validate Honcho's allow_insecure_http as a boolean * docs(memory): list the full Pydantic boolean vocabulary for allow_insecure_http
This commit is contained in:
parent
b63906ce28
commit
230d8f03d7
@ -33,6 +33,13 @@ A configured `api_key` over a plain-`http://` base URL is rejected at startup
|
||||
unless you explicitly set `allow_insecure_http: true` (local-development opt-in,
|
||||
same posture as the mem0 backend). Use HTTPS for any non-local deployment.
|
||||
|
||||
`allow_insecure_http` also accepts an environment reference such as
|
||||
`allow_insecure_http: $HONCHO_ALLOW_INSECURE`. The value is validated with
|
||||
Pydantic's boolean vocabulary, case-insensitively: `true`, `t`, `y`, `yes`,
|
||||
`on` and `1` enable the opt-in, while `false`, `f`, `n`, `no`, `off` and `0`
|
||||
keep it off. Any other value is rejected when the backend loads its
|
||||
configuration, so a typo cannot silently enable the opt-in.
|
||||
|
||||
Config errors (bad URL, insecure key combination) fail fast at Gateway startup;
|
||||
connectivity is deliberately not probed, so a temporarily unreachable Honcho
|
||||
does not block startup — reads then degrade per `failure_policy.read`.
|
||||
|
||||
@ -15,6 +15,8 @@ from math import isfinite
|
||||
from typing import Any
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from pydantic import TypeAdapter, ValidationError
|
||||
|
||||
_ID_RE = re.compile(r"[^a-zA-Z0-9_-]+")
|
||||
|
||||
|
||||
@ -58,6 +60,29 @@ def _number[T](cfg: dict[str, Any], key: str, default: T, cast: Callable[[Any],
|
||||
raise ValueError(f"Honcho backend: {key} must be a number, got {type(value).__name__}") from None
|
||||
|
||||
|
||||
def _bool(cfg: dict[str, Any], key: str, default: bool) -> bool:
|
||||
"""Narrow an operator-supplied boolean knob.
|
||||
|
||||
``AppConfig.resolve_env_variables`` substitutes a ``$VAR`` reference with
|
||||
the raw environment string, and a quoted YAML scalar stays a string, so a
|
||||
configured ``false`` can arrive as ``"false"``. Truthiness would read that
|
||||
as ``True`` -- for ``allow_insecure_http`` it silently opts into sending the
|
||||
API key over plaintext HTTP. ``TypeAdapter(bool)`` reads the literal with
|
||||
Pydantic's boolean vocabulary instead (``true``/``t``/``y``/``yes``/``on``/
|
||||
``1`` and their false counterparts, case-insensitive), the same vocabulary
|
||||
the sandbox providers accept (see the tenki ``sticky`` knob), and the knob
|
||||
is named in the error, so a typo fails at config load instead of degrading
|
||||
to ``True``.
|
||||
"""
|
||||
value = cfg.get(key, default)
|
||||
if value is None:
|
||||
return default
|
||||
try:
|
||||
return TypeAdapter(bool).validate_python(value)
|
||||
except ValidationError:
|
||||
raise ValueError(f"Honcho backend: {key} must be a boolean, got {type(value).__name__}") from None
|
||||
|
||||
|
||||
def _parse_override_map(cfg: dict[str, Any], key: str) -> dict[str, str]:
|
||||
"""Overrides map raw user ids to explicit workspace/peer ids; an empty or
|
||||
null VALUE is always a config mistake (empty string is falsy and would
|
||||
@ -115,7 +140,7 @@ class HonchoConfig:
|
||||
failure_policy = _mapping(cfg.get("failure_policy"), "failure_policy")
|
||||
base_url = str(cfg.get("base_url", "http://localhost:8000")).rstrip("/")
|
||||
api_key = cfg.get("api_key") or None
|
||||
allow_insecure = bool(cfg.get("allow_insecure_http", False))
|
||||
allow_insecure = _bool(cfg, "allow_insecure_http", False)
|
||||
# The parsed scheme, not startswith("http://"): a caller can write
|
||||
# "HTTP://internal:8000" and urlsplit/httpx both treat that as plain
|
||||
# HTTP, so a case-sensitive prefix test leaks the key unencrypted.
|
||||
|
||||
@ -7,11 +7,13 @@ from types import SimpleNamespace
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from deerflow.agents.memory.backends.honcho.client import HonchoClient, HonchoRequestError
|
||||
from deerflow.agents.memory.backends.honcho.config import HonchoConfig, sanitize_id
|
||||
from deerflow.agents.memory.backends.honcho.honcho_manager import HonchoMemoryManager, _stable_id
|
||||
from deerflow.agents.memory.manager import MemoryManagerError, MemoryReadError
|
||||
from deerflow.config.app_config import AppConfig
|
||||
|
||||
|
||||
class TestHonchoConfig:
|
||||
@ -55,6 +57,73 @@ class TestHonchoConfig:
|
||||
cfg = HonchoConfig.from_backend_config({"base_url": "http://internal:8000", "api_key": "sk-x", "allow_insecure_http": True})
|
||||
assert cfg.api_key == "sk-x"
|
||||
|
||||
def test_allow_insecure_http_false_keeps_the_guard(self):
|
||||
"""An explicit ``False`` must not read as an opt-in."""
|
||||
with pytest.raises(ValueError, match="allow_insecure_http"):
|
||||
HonchoConfig.from_backend_config({"base_url": "http://internal:8000", "api_key": "sk-x", "allow_insecure_http": False})
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
("false", False),
|
||||
("f", False),
|
||||
("F", False),
|
||||
("n", False),
|
||||
("no", False),
|
||||
("off", False),
|
||||
("0", False),
|
||||
("FALSE", False),
|
||||
("true", True),
|
||||
("t", True),
|
||||
("T", True),
|
||||
("y", True),
|
||||
("Y", True),
|
||||
("yes", True),
|
||||
("on", True),
|
||||
("1", True),
|
||||
],
|
||||
)
|
||||
def test_allow_insecure_http_from_environment(self, monkeypatch, tmp_path, value, expected):
|
||||
"""``$VAR`` substitution yields the raw environment string, so the knob
|
||||
must be validated as a boolean: string truthiness turns ``"false"`` into
|
||||
``True`` and silently sends the API key over plaintext HTTP. The
|
||||
accepted literals are Pydantic's boolean vocabulary, so the documented
|
||||
list is pinned here rather than in prose only."""
|
||||
monkeypatch.setenv("TEST_HONCHO_ALLOW_INSECURE", value)
|
||||
config_path = tmp_path / "config.yaml"
|
||||
config_path.write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"models": [],
|
||||
"sandbox": {"use": "deerflow.sandbox.local:LocalSandboxProvider"},
|
||||
"memory": {
|
||||
"backend_config": {
|
||||
"base_url": "http://internal:8000",
|
||||
"api_key": "sk-x",
|
||||
"allow_insecure_http": "$TEST_HONCHO_ALLOW_INSECURE",
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
config = AppConfig.from_file(config_path)
|
||||
backend_config = config.memory.backend_config
|
||||
assert backend_config["allow_insecure_http"] == value
|
||||
|
||||
if expected:
|
||||
assert HonchoConfig.from_backend_config(backend_config).allow_insecure_http is True
|
||||
else:
|
||||
# The opt-in stays off, so the plaintext-http key guard still fires.
|
||||
with pytest.raises(ValueError, match="allow_insecure_http"):
|
||||
HonchoConfig.from_backend_config(backend_config)
|
||||
|
||||
@pytest.mark.parametrize("value", ["not-a-boolean", "2", "", [], "tru"])
|
||||
def test_allow_insecure_http_rejects_a_non_boolean(self, value):
|
||||
with pytest.raises(ValueError, match="allow_insecure_http must be a boolean"):
|
||||
HonchoConfig.from_backend_config({"base_url": "http://internal:8000", "allow_insecure_http": value})
|
||||
|
||||
def test_http_without_api_key_is_fine(self):
|
||||
cfg = HonchoConfig.from_backend_config({"base_url": "http://host.docker.internal:8000"})
|
||||
assert cfg.api_key is None
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user