diff --git a/hermes_cli/config.py b/hermes_cli/config.py index d2a7693a..086acfa2 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -670,6 +670,11 @@ OPTIONAL_ENV_VARS = { "password": True, "category": "tool", }, + "HONCHO_BASE_URL": { + "description": "Base URL for self-hosted Honcho instances (no API key needed)", + "prompt": "Honcho base URL (e.g. http://localhost:8000)", + "category": "tool", + }, # ── Messaging platforms ── "TELEGRAM_BOT_TOKEN": { diff --git a/honcho_integration/client.py b/honcho_integration/client.py index 759576ad..4411241a 100644 --- a/honcho_integration/client.py +++ b/honcho_integration/client.py @@ -117,11 +117,13 @@ class HonchoClientConfig: def from_env(cls, workspace_id: str = "hermes") -> HonchoClientConfig: """Create config from environment variables (fallback).""" api_key = os.environ.get("HONCHO_API_KEY") + base_url = os.environ.get("HONCHO_BASE_URL", "").strip() or None return cls( workspace_id=workspace_id, api_key=api_key, environment=os.environ.get("HONCHO_ENVIRONMENT", "production"), - enabled=bool(api_key), + base_url=base_url, + enabled=bool(api_key or base_url), ) @classmethod @@ -171,8 +173,14 @@ class HonchoClientConfig: or raw.get("environment", "production") ) - # Auto-enable when API key is present (unless explicitly disabled) - # Host-level enabled wins, then root-level, then auto-enable if key exists. + base_url = ( + raw.get("baseUrl") + or os.environ.get("HONCHO_BASE_URL", "").strip() + or None + ) + + # Auto-enable when API key or base_url is present (unless explicitly disabled) + # Host-level enabled wins, then root-level, then auto-enable if key/url exists. host_enabled = host_block.get("enabled") root_enabled = raw.get("enabled") if host_enabled is not None: @@ -180,8 +188,8 @@ class HonchoClientConfig: elif root_enabled is not None: enabled = root_enabled else: - # Not explicitly set anywhere -> auto-enable if API key exists - enabled = bool(api_key) + # Not explicitly set anywhere -> auto-enable if API key or base_url exists + enabled = bool(api_key or base_url) # write_frequency: accept int or string raw_wf = ( @@ -214,6 +222,7 @@ class HonchoClientConfig: workspace_id=workspace, api_key=api_key, environment=environment, + base_url=base_url, peer_name=host_block.get("peerName") or raw.get("peerName"), ai_peer=ai_peer, linked_hosts=linked_hosts, @@ -348,11 +357,12 @@ def get_honcho_client(config: HonchoClientConfig | None = None) -> Honcho: if config is None: config = HonchoClientConfig.from_global_config() - if not config.api_key: + if not config.api_key and not config.base_url: raise ValueError( "Honcho API key not found. " "Get your API key at https://app.honcho.dev, " - "then run 'hermes honcho setup' or set HONCHO_API_KEY." + "then run 'hermes honcho setup' or set HONCHO_API_KEY. " + "For local instances, set HONCHO_BASE_URL instead." ) try: diff --git a/tests/honcho_integration/test_client.py b/tests/honcho_integration/test_client.py index b1ae29c5..a9a837e6 100644 --- a/tests/honcho_integration/test_client.py +++ b/tests/honcho_integration/test_client.py @@ -60,6 +60,21 @@ class TestFromEnv: config = HonchoClientConfig.from_env(workspace_id="custom") assert config.workspace_id == "custom" + def test_reads_base_url_from_env(self): + with patch.dict(os.environ, {"HONCHO_BASE_URL": "http://localhost:8000"}, clear=False): + config = HonchoClientConfig.from_env() + assert config.base_url == "http://localhost:8000" + assert config.enabled is True + + def test_enabled_without_api_key_when_base_url_set(self): + """base_url alone (no API key) is sufficient to enable a local instance.""" + with patch.dict(os.environ, {"HONCHO_BASE_URL": "http://localhost:8000"}, clear=False): + os.environ.pop("HONCHO_API_KEY", None) + config = HonchoClientConfig.from_env() + assert config.api_key is None + assert config.base_url == "http://localhost:8000" + assert config.enabled is True + class TestFromGlobalConfig: def test_missing_config_falls_back_to_env(self, tmp_path): @@ -188,6 +203,36 @@ class TestFromGlobalConfig: config = HonchoClientConfig.from_global_config(config_path=config_file) assert config.api_key == "env-key" + def test_base_url_env_fallback(self, tmp_path): + """HONCHO_BASE_URL env var is used when no baseUrl in config JSON.""" + config_file = tmp_path / "config.json" + config_file.write_text(json.dumps({"workspace": "local"})) + + with patch.dict(os.environ, {"HONCHO_BASE_URL": "http://localhost:8000"}, clear=False): + config = HonchoClientConfig.from_global_config(config_path=config_file) + assert config.base_url == "http://localhost:8000" + assert config.enabled is True + + def test_base_url_from_config_root(self, tmp_path): + """baseUrl in config root is read and takes precedence over env var.""" + config_file = tmp_path / "config.json" + config_file.write_text(json.dumps({"baseUrl": "http://config-host:9000"})) + + with patch.dict(os.environ, {"HONCHO_BASE_URL": "http://localhost:8000"}, clear=False): + config = HonchoClientConfig.from_global_config(config_path=config_file) + assert config.base_url == "http://config-host:9000" + + def test_base_url_not_read_from_host_block(self, tmp_path): + """baseUrl is a root-level connection setting, not overridable per-host (consistent with apiKey).""" + config_file = tmp_path / "config.json" + config_file.write_text(json.dumps({ + "baseUrl": "http://root:9000", + "hosts": {"hermes": {"baseUrl": "http://host-block:9001"}}, + })) + + config = HonchoClientConfig.from_global_config(config_path=config_file) + assert config.base_url == "http://root:9000" + class TestResolveSessionName: def test_manual_override(self):