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:
Shxiao 2026-09-27 12:18:03 +09:00 committed by GitHub
parent b63906ce28
commit 230d8f03d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 102 additions and 1 deletions

View File

@ -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`.

View File

@ -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.

View File

@ -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