From 6ced760a3db3bc8c5d57d35e482202f62da0fbf8 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 23 Jul 2026 08:22:25 -0700 Subject: [PATCH] test(gateway): cover gateway.api_server YAML discovery + extra bridge; docs: document config.yaml support Follow-up for salvaged PR #66633 (fixes #66630): - 5 tests: nested gateway.api_server discovery, port/key/host/model_name bridged into extra, explicit extra wins over top-level key, non-platform gateway keys (streaming/timeout) not misparsed, gateway.platforms.api_server path regression-guarded. - docs: api-server.md (en + zh-Hans) now documents the gateway.api_server YAML section instead of 'not yet supported'. --- tests/gateway/test_config.py | 134 ++++++++++++++++++ .../docs/user-guide/features/api-server.md | 14 +- .../current/user-guide/features/api-server.md | 14 +- 3 files changed, 158 insertions(+), 4 deletions(-) diff --git a/tests/gateway/test_config.py b/tests/gateway/test_config.py index 53c79871be65..3d1cfea9641c 100644 --- a/tests/gateway/test_config.py +++ b/tests/gateway/test_config.py @@ -728,6 +728,140 @@ class TestLoadGatewayConfig: assert config.stt_echo_transcripts is False + @staticmethod + def _clear_api_server_env(monkeypatch): + """Keep _apply_env_overrides from masking the YAML path under test.""" + for key in ( + "API_SERVER_ENABLED", + "API_SERVER_KEY", + "API_SERVER_PORT", + "API_SERVER_HOST", + "API_SERVER_CORS_ORIGINS", + "API_SERVER_MODEL_NAME", + ): + monkeypatch.delenv(key, raising=False) + + def test_api_server_from_nested_gateway_section(self, tmp_path, monkeypatch): + """``gateway.api_server:`` (nested YAML form) must be discovered and + enable the platform. + + Regression for #66630: load_gateway_config handled gateway.streaming + and gateway.platforms.* but silently dropped nested gateway. + blocks, so ``gateway.api_server.enabled: true`` never started the API + server unless API_SERVER_* env vars were also set. + """ + self._clear_api_server_env(monkeypatch) + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + (hermes_home / "config.yaml").write_text( + "gateway:\n api_server:\n enabled: true\n", + encoding="utf-8", + ) + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + + config = load_gateway_config() + + assert Platform.API_SERVER in config.platforms + assert config.platforms[Platform.API_SERVER].enabled is True + + def test_api_server_port_bridged_into_extra(self, tmp_path, monkeypatch): + """``gateway.api_server.port`` must land in PlatformConfig.extra — + the adapter reads port/key/host/cors_origins/model_name from extra + (gateway/platforms/api_server.py), and from_dict discards unknown + top-level keys, so without the bridge the port is silently lost.""" + self._clear_api_server_env(monkeypatch) + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + (hermes_home / "config.yaml").write_text( + "gateway:\n" + " api_server:\n" + " enabled: true\n" + " port: 8642\n" + " host: 0.0.0.0\n" + " key: sekrit\n" + " model_name: my-hermes\n", + encoding="utf-8", + ) + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + + config = load_gateway_config() + + extra = config.platforms[Platform.API_SERVER].extra + assert extra["port"] == 8642 + assert extra["host"] == "0.0.0.0" + assert extra["key"] == "sekrit" + assert extra["model_name"] == "my-hermes" + + def test_api_server_explicit_extra_wins_over_toplevel_key(self, tmp_path, monkeypatch): + """An explicit ``extra: {port: X}`` must beat a sibling top-level + ``port:`` — the bridge's ``not in _api_extra`` guard must never + clobber a value the user placed in extra deliberately.""" + self._clear_api_server_env(monkeypatch) + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + (hermes_home / "config.yaml").write_text( + "gateway:\n" + " api_server:\n" + " enabled: true\n" + " port: 9999\n" + " extra:\n" + " port: 8642\n", + encoding="utf-8", + ) + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + + config = load_gateway_config() + + assert config.platforms[Platform.API_SERVER].extra["port"] == 8642 + + def test_non_platform_gateway_keys_not_misparsed_as_platforms(self, tmp_path, monkeypatch): + """Nested-platform discovery must only pick up keys matching the + Platform enum: ``gateway.streaming`` / ``gateway.timeout`` must not + be turned into phantom platform entries or break loading.""" + self._clear_api_server_env(monkeypatch) + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + (hermes_home / "config.yaml").write_text( + "gateway:\n" + " streaming:\n" + " enabled: false\n" + " timeout: 120\n" + " api_server:\n" + " enabled: true\n", + encoding="utf-8", + ) + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + + config = load_gateway_config() + + # streaming still parsed as the streaming subsection, not a platform + assert config.streaming.enabled is False + # only real platforms present; nothing named streaming/timeout leaked in + assert all(isinstance(p, Platform) for p in config.platforms) + assert config.platforms[Platform.API_SERVER].enabled is True + + def test_api_server_via_gateway_platforms_block_still_works(self, tmp_path, monkeypatch): + """No regression: the pre-existing ``gateway.platforms.api_server`` + path must keep working alongside the new nested discovery, and its + api_server keys get the same extra bridge.""" + self._clear_api_server_env(monkeypatch) + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + (hermes_home / "config.yaml").write_text( + "gateway:\n" + " platforms:\n" + " api_server:\n" + " enabled: true\n" + " port: 8643\n", + encoding="utf-8", + ) + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + + config = load_gateway_config() + + assert config.platforms[Platform.API_SERVER].enabled is True + assert config.platforms[Platform.API_SERVER].extra["port"] == 8643 + def test_group_sessions_per_user_from_nested_gateway_section(self, tmp_path, monkeypatch): hermes_home = tmp_path / ".hermes" hermes_home.mkdir() diff --git a/website/docs/user-guide/features/api-server.md b/website/docs/user-guide/features/api-server.md index cbcb1f954d5b..b67a896dd837 100644 --- a/website/docs/user-guide/features/api-server.md +++ b/website/docs/user-guide/features/api-server.md @@ -426,11 +426,21 @@ The API server gives full access to hermes-agent's toolset, **including terminal ### config.yaml +The same settings can live in `~/.hermes/config.yaml` under a nested `gateway.api_server:` section: + ```yaml -# Not yet supported — use environment variables. -# config.yaml support coming in a future release. +gateway: + api_server: + enabled: true + port: 8642 + host: 127.0.0.1 + key: your-secret-key + cors_origins: http://localhost:3000 + model_name: my-hermes ``` +`port`, `key`, `host`, `cors_origins`, and `model_name` are automatically bridged into the platform's `extra` settings, so they behave exactly like their `API_SERVER_*` environment-variable counterparts. Environment variables take precedence over `config.yaml` values. The block is also accepted under `gateway.platforms.api_server:` or a top-level `platforms.api_server:` section. + ## Security Headers All responses include security headers: diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/features/api-server.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/features/api-server.md index d2e3ef148143..490e957bf8e5 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/features/api-server.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/features/api-server.md @@ -352,11 +352,21 @@ API 服务器提供对 hermes-agent 工具集的完整访问权限,**包括终 ### config.yaml +相同的设置也可以写在 `~/.hermes/config.yaml` 中嵌套的 `gateway.api_server:` 小节下: + ```yaml -# 暂不支持——请使用环境变量。 -# config.yaml 支持将在未来版本中推出。 +gateway: + api_server: + enabled: true + port: 8642 + host: 127.0.0.1 + key: your-secret-key + cors_origins: http://localhost:3000 + model_name: my-hermes ``` +`port`、`key`、`host`、`cors_origins` 和 `model_name` 会自动桥接到该平台的 `extra` 设置中,行为与对应的 `API_SERVER_*` 环境变量完全一致。环境变量优先于 `config.yaml` 中的值。该配置块同样可以放在 `gateway.platforms.api_server:` 或顶层 `platforms.api_server:` 小节下。 + ## 安全响应头 所有响应均包含安全响应头: