diff --git a/plugins/memory/honcho/cli.py b/plugins/memory/honcho/cli.py index 9581df17e..c8f396022 100644 --- a/plugins/memory/honcho/cli.py +++ b/plugins/memory/honcho/cli.py @@ -277,14 +277,23 @@ def _resolve_api_key(cfg: dict) -> str: For self-hosted instances configured with ``baseUrl`` instead of an API key, returns ``"local"`` so that credential guards throughout the CLI - don't reject a valid configuration. + don't reject a valid configuration. The ``baseUrl`` is scheme-validated + (http/https only) so that a typo like ``baseUrl: true`` can't silently + pass the guard. """ host_key = ((cfg.get("hosts") or {}).get(_host_key()) or {}).get("apiKey") key = host_key or cfg.get("apiKey", "") or os.environ.get("HONCHO_API_KEY", "") if not key: base_url = cfg.get("baseUrl") or cfg.get("base_url") or os.environ.get("HONCHO_BASE_URL", "") - if base_url.strip(): - return "local" + base_url = (base_url or "").strip() + if base_url: + from urllib.parse import urlparse + try: + parsed = urlparse(base_url) + except (TypeError, ValueError): + parsed = None + if parsed and parsed.scheme in ("http", "https") and parsed.netloc: + return "local" return key diff --git a/plugins/memory/honcho/client.py b/plugins/memory/honcho/client.py index a13b89c25..63e45b462 100644 --- a/plugins/memory/honcho/client.py +++ b/plugins/memory/honcho/client.py @@ -16,6 +16,7 @@ from __future__ import annotations import json import os import logging +import hashlib from dataclasses import dataclass, field from pathlib import Path @@ -571,8 +572,6 @@ class HonchoClientConfig: if len(sanitized) <= max_len: return sanitized - import hashlib - hash_len = cls._HONCHO_SESSION_ID_HASH_LEN digest = hashlib.sha256(original.encode("utf-8")).hexdigest()[:hash_len] # max_len - hash_len - 1 (for the '-' separator) chars of the sanitized diff --git a/tests/honcho_plugin/test_cli.py b/tests/honcho_plugin/test_cli.py index 70b015d18..229e0a6a7 100644 --- a/tests/honcho_plugin/test_cli.py +++ b/tests/honcho_plugin/test_cli.py @@ -41,6 +41,33 @@ class TestResolveApiKey: monkeypatch.delenv("HONCHO_BASE_URL", raising=False) assert honcho_cli._resolve_api_key({}) == "" + def test_rejects_garbage_base_url_without_scheme(self, monkeypatch): + """A non-URL string in baseUrl (typo) must not pass the guard.""" + import plugins.memory.honcho.cli as honcho_cli + monkeypatch.setattr(honcho_cli, "_host_key", lambda: "hermes") + monkeypatch.delenv("HONCHO_API_KEY", raising=False) + monkeypatch.delenv("HONCHO_BASE_URL", raising=False) + for garbage in ("true", "yes", "1", "localhost", "10.0.0.5"): + assert honcho_cli._resolve_api_key({"baseUrl": garbage}) == "", \ + f"expected empty for garbage {garbage!r}" + + def test_rejects_non_http_scheme_base_url(self, monkeypatch): + """Only http/https schemes are accepted; file:// / ftp:// are not.""" + import plugins.memory.honcho.cli as honcho_cli + monkeypatch.setattr(honcho_cli, "_host_key", lambda: "hermes") + monkeypatch.delenv("HONCHO_API_KEY", raising=False) + monkeypatch.delenv("HONCHO_BASE_URL", raising=False) + for bad in ("file:///etc/passwd", "ftp://host/", "ws://host/"): + assert honcho_cli._resolve_api_key({"baseUrl": bad}) == "", \ + f"expected empty for scheme of {bad!r}" + + def test_accepts_https_base_url(self, monkeypatch): + import plugins.memory.honcho.cli as honcho_cli + monkeypatch.setattr(honcho_cli, "_host_key", lambda: "hermes") + monkeypatch.delenv("HONCHO_API_KEY", raising=False) + monkeypatch.delenv("HONCHO_BASE_URL", raising=False) + assert honcho_cli._resolve_api_key({"baseUrl": "https://honcho.example.com"}) == "local" + class TestCmdStatus: def test_reports_connection_failure_when_session_setup_fails(self, monkeypatch, capsys, tmp_path):