Skip to content

fix(memory): reject a Honcho base_url that can never resolve - #5607

Merged
WillemJiang merged 2 commits into
bytedance:mainfrom
Lesereingrape:fix/honcho-base-url-must-be-absolute
Sep 24, 2026
Merged

WillemJiang merged 2 commits into
bytedance:mainfrom
Lesereingrape:fix/honcho-base-url-must-be-absolute

Conversation

@Lesereingrape

@Lesereingrape Lesereingrape commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #5606

Why

backend_config is operator-supplied YAML, and base_url is the one Honcho knob that is read without any check: config.py:105 is str(cfg.get("base_url", ...)).rstrip("/"). The most likely wrong spelling is also the natural one when pointing DeerFlow at a compose service or an internal host (base_url: honcho:8000), and httpx does not treat that as relative — it parses the host as the scheme, so every request fails later:

'localhost:8000'        -> accepted  -> recall: Request URL has an unsupported protocol 'localhost://'
'honcho.internal'       -> accepted  -> get_context() returns '' and add() returns silently
'not a url' / '' / 'ftp://localhost:8000' / 12345 -> all accepted

The Gateway starts green and Honcho memory is permanently dead, with one WARNING per operation as the only trace. Two aggravating details measured on main:

  • the api_key-over-plain-HTTP guard at config.py:108 is base_url.startswith("http://"), so a scheme-less URL skips it as well: {'base_url': 'localhost:8000', 'api_key': 'sk-test'} is accepted while allow_insecure_http stays false.
  • the backend promises the opposite in two places: backends/honcho/README.md:36 — "Config errors (bad URL, insecure key combination) fail fast at Gateway startup" — and the HonchoMemoryManager.from_config docstring at honcho_manager.py:118, which is that startup path.

This continues the posture the file was already hardened to, the same way #5555 did: #4783 put parse-time ValueError guards in __post_init__ for the scalar knobs, #5555 added _mapping / _number for the nested and numeric ones, and #4782 / #4821 are the same "silently accepted now, wrong later" class in the sibling backends. base_url was the remaining unguarded field, and mem0 (config.py:134-136) plus OpenViking (config.py:112-114) already validate it.

What changed

  • HonchoConfig.__post_init__ now rejects a base_url whose scheme is not http/https, or whose netloc is empty: Honcho backend: base_url must be an absolute http:// or https:// URL. It goes in __post_init__ rather than only in from_backend_config so direct construction is covered too — that is where this class's other guards already live, and it keeps the check next to the field it protects.
  • The message follows each backend's existing phrasing convention, so the three backends word the same error slightly differently, as they already do for the numeric knobs.
  • Every URL that works today keeps working: urlsplit accepts http/https with any host, so http://host.docker.internal:8000 (the documented deployment form), https://api.honcho.dev, an internal http://internal:8000, and portless/IPv6 forms all pass; the trailing-slash strip stays where it is; and for a real http:// URL the existing insecure-key check still runs first, unchanged.
  • Tests added to TestHonchoConfig, in the parametrized style of the neighbouring test_rejects_invalid_timeouts: test_malformed_base_url_rejected_as_config_error (7 cases), test_absolute_base_urls_still_accepted (4 cases), test_manager_from_config_fails_fast_on_malformed_base_url.

Surface area

HonchoConfig construction only — backends/honcho/config.py (+9) and its test file (+13). No new dependency (urlsplit is stdlib and is already imported by mem0/config.py in this same package tree), no schema, prompt, I/O or network change, and no other backend, caller or route is touched. For a configuration that loads and works today, nothing behaves differently; the only outcomes that change belong to URLs that could never have resolved.

Bug fix verification

Reproduction is tests/test_honcho_memory_backend.py::TestHonchoConfig::test_malformed_base_url_rejected_as_config_error plus ::test_manager_from_config_fails_fast_on_malformed_base_url. Both were measured on this branch's base, main at 0b7cef2e, by reverting only config.py in place and keeping the new tests:

# main 0b7cef2e + new tests, fix reverted        (-k "base_url")
8 failed, 4 passed, 103 deselected in 8.64s
FAILED ...test_malformed_base_url_rejected_as_config_error[localhost:8000]
FAILED ...[honcho.internal]  ...[not a url]  ...[]  ...[ftp://localhost:8000]  ...[http://]  ...[12345]
FAILED TestHonchoConfig::test_manager_from_config_fails_fast_on_malformed_base_url
# the 4 "absolute base urls still accepted" cases pass before and after, which is the point

# this branch cd76fc56
255 passed in 112.96s

Validation

cd backend
pytest tests/test_honcho_memory_backend.py tests/test_mem0_memory_backend.py tests/test_openviking_memory_backend.py -q
  -> 255 passed in 112.96s      (243 on main before this PR + the 12 new cases)
ruff check <both changed files>            -> All checks passed!
ruff format --check <both changed files>   -> 2 files already formatted

I did not run make test (the whole backend suite): on this Windows machine it has pre-existing environmental failures unrelated to this diff, documented in a comment on #5553, so I ran the three memory-backend suites that cover the changed file instead. Frontend checks were not run — this diff cannot reach them. The diff is small enough to read in one pass: 22 added lines, 0 removed.

AI assistance

  • Tool(s) used: Claude Code (Anthropic), plus a code-search agent pass that surveyed the three memory backends' config guards.
  • How you used it: the candidate came out of that search. The reproduction, the decision to place the guard in __post_init__ (where this class's other guards already are) and the message wording were worked out and verified at file/line level by the agent, and every number above comes from commands run on this branch. The account owner has now read every line of this diff and ticked the box below (2026-09-24), so this is no longer unreviewed agent output.
  • I've read and understand every line of this change and take responsibility for it — it's not unreviewed AI output.

Review follow-up (68d232d)

Three findings from review, all addressed on top of the original commit:

  • Scheme-case bypass of the api_key-over-plain-HTTP guard (substantive). The check
    keyed off base_url.startswith("http://"), which is case-sensitive, while urlsplit
    lowercases the scheme — so HTTP://internal:8000 was accepted with a key set and
    allow_insecure_http left False, and httpx sent the key in the clear. It now reads
    urlsplit(base_url).scheme == "http", the way the mem0 sibling already does
    (mem0/config.py:137).
  • Host-less authority (the nit). __post_init__ checked netloc, which is truthy for
    ":8000", so http://:8000 was accepted into the same accepted-now/dead-later class
    this PR removes. It checks hostname now, which rejects http:// and http://:8000
    in one predicate. The mem0 and OpenViking siblings keep their netloc check — that is
    pre-existing and outside this diff unless you want it here.
  • Direct construction was untested even though the guard sits in __post_init__
    precisely for that path. test_direct_construction_hits_the_same_guard covers
    HonchoConfig(base_url=...) over localhost:8000, http://, http://:8000 and
    ftp://localhost:8000, and test_http_scheme_case_does_not_bypass_the_insecure_key_guard
    parametrizes HTTP://, HtTp:// and http:// with and without the opt-in.

Measured on this branch: the five new/extended cases run against the previous head
(cd76fc56) give 5 failed, 12 passed — the failures are DID NOT RAISE on the
scheme-case and host-less shapes, so they fail on behaviour; at 68d232df
tests/test_honcho_memory_backend.py gives 124 passed (115 at cd76fc56) and
tests/test_honcho_memory_backend.py + tests/blocking_io/test_honcho_memory_backend.py
together give 125 passed. ruff check and ruff format --check are clean on both
changed files.

HonchoConfig only did str(...).rstrip('/') on backend_config.base_url, so a
scheme-less or non-http value was accepted at Gateway startup and then made
every memory read return empty and every write drop silently, with only a
per-operation WARNING. httpx turns 'localhost:8000' into the bogus scheme
'localhost:'. The same check already exists in the mem0 and OpenViking
backends, and honcho/README.md plus HonchoMemoryManager.from_config both
promise a bad URL fails fast at startup.

@willem-bd willem-bd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at cd76fc5. Ran pytest tests/test_honcho_memory_backend.py at this SHA: 115 passed; ruff check and ruff format --check clean on both files. The guard itself is correct and matches the mem0/OpenViking phrasing and placement conventions, and the new parametrized tests cover the shapes claimed in the description. Three findings, one substantive: the PR fixes the scheme-less bypass of the api_key-over-plain-HTTP guard but leaves the case-variant bypass open, even though urlsplit here already computes the scheme it would need.

- drive the api_key-over-plain-HTTP check off urlsplit(...).scheme: urlsplit
  lowercases the scheme while startswith("http://") does not, so
  "HTTP://internal:8000" reached httpx over plain HTTP with the key set and
  allow_insecure_http still False
- read hostname instead of netloc in the base_url guard, so an authority
  without a host ("http://", "http://:8000") is rejected at config time like
  the scheme-less shape this PR already removes
- test direct HonchoConfig(base_url=...) construction, the path the guard's
  placement in __post_init__ exists to protect

@willem-bd willem-bd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the follow-up at 68d232d: all three findings from the cd76fc5 review are fixed.

  1. Case-variant insecure-key bypass — from_backend_config now reads urlsplit(base_url).scheme == "http"; the HTTP://internal:8000 + api_key repro now raises the allow_insecure_http error, pinned by test_http_scheme_case_does_not_bypass_the_insecure_key_guard.
  2. Host-less authorities — not parsed_base_url.hostname rejects both http:// and http://:8000 in one predicate.
  3. Direct-construction coverage — test_direct_construction_hits_the_same_guard builds HonchoConfig(base_url=...) directly over localhost:8000, http://, http://:8000 and ftp://localhost:8000.

Verified independently at this head: pytest tests/test_honcho_memory_backend.py → 124 passed (was 115 at cd76fc5), ruff check and ruff format --check clean on both files, the default http://localhost:8000 still constructs, and a repo-wide grep at this SHA shows the test file is the only direct HonchoConfig( construction site, so the stricter guard breaks no other caller. Nothing new found; ready to merge from my side.

@Lesereingrape

Lesereingrape commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for verifying independently at this head — nothing further from my side.

At the time of writing this the PR was kept a draft on purpose: the AI-assistance box in the body is unticked until the account owner has read the diff line by line, and #5588 was occupying this account's ready-PR slot here.

Superseded 2026-09-24: the account owner has since read the diff and ticked the box, and @WillemJiang marked this ready for review at 08:15:22Z, so it is in the review queue now. Marking it ready from the author side was refused by GitHub (markPullRequestReadyForReview → FORBIDDEN for this account on this PR), which is why the flip needed a maintainer.

@WillemJiang WillemJiang added this to the 2.2.0 milestone Sep 24, 2026
@WillemJiang
WillemJiang marked this pull request as ready for review September 24, 2026 08:15
@github-actions github-actions Bot added area:agents Agents, subagents, graph wiring, prompts, langgraph.json needs-validation Touches front/back contract surface; needs real-path validation risk:high High risk: backend API, agents, sandbox, auth, deps, CI size/S PR changes 20-100 lines labels Sep 24, 2026
@WillemJiang
WillemJiang merged commit f250d11 into bytedance:main Sep 24, 2026
30 of 34 checks passed
hawkli-1994 pushed a commit to rongxinzy/deer-flow that referenced this pull request Sep 27, 2026
…ce#5607)

* fix(memory): reject a Honcho base_url that can never resolve

HonchoConfig only did str(...).rstrip('/') on backend_config.base_url, so a
scheme-less or non-http value was accepted at Gateway startup and then made
every memory read return empty and every write drop silently, with only a
per-operation WARNING. httpx turns 'localhost:8000' into the bogus scheme
'localhost:'. The same check already exists in the mem0 and OpenViking
backends, and honcho/README.md plus HonchoMemoryManager.from_config both
promise a bad URL fails fast at startup.

* fix(memory): cover the scheme-case and host-less variants of the guard

- drive the api_key-over-plain-HTTP check off urlsplit(...).scheme: urlsplit
  lowercases the scheme while startswith("http://") does not, so
  "HTTP://internal:8000" reached httpx over plain HTTP with the key set and
  allow_insecure_http still False
- read hostname instead of netloc in the base_url guard, so an authority
  without a host ("http://", "http://:8000") is rejected at config time like
  the scheme-less shape this PR already removes
- test direct HonchoConfig(base_url=...) construction, the path the guard's
  placement in __post_init__ exists to protect
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:agents Agents, subagents, graph wiring, prompts, langgraph.json needs-validation Touches front/back contract surface; needs real-path validation risk:high High risk: backend API, agents, sandbox, auth, deps, CI size/S PR changes 20-100 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Honcho backend accepts a base_url that can never resolve: memory dies silently while startup stays green

3 participants