From a65a6f334c07de2245f28cdacbc902bf4e560acc Mon Sep 17 00:00:00 2001 From: Xubin Ren <52506698+Re-bin@users.noreply.github.com> Date: Mon, 13 Jul 2026 15:06:16 +0800 Subject: [PATCH] test(gateway): cover slow health clients --- docs/deployment.md | 4 +++- nanobot/cli/commands.py | 3 ++- tests/cli/test_commands.py | 11 +++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/deployment.md b/docs/deployment.md index c749cc53..f7565ade 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -56,7 +56,9 @@ Restart the deployed process after editing `config.json`. Long-running processes > When the WebSocket `host` is `0.0.0.0`, the channel refuses to start unless `token` or `tokenIssueSecret` is also configured. See [`webui.md#lan-access`](./webui.md#lan-access) for details. > The gateway health route itself is intentionally minimal and unauthenticated. When the > container binds it to `0.0.0.0`, publish port `18790` to host loopback only; place any -> remotely monitored health endpoint behind a firewall or reverse proxy. +> remotely monitored health endpoint behind a firewall or reverse proxy. If another host +> must probe it directly, replace `127.0.0.1` in the port mapping with a trusted host +> interface and restrict inbound traffic to the monitoring system. ### Docker Compose diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index aa018134..9c3609df 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -1022,7 +1022,8 @@ def _print_gateway_health_endpoint(host: str, port: int) -> None: return console.print( - "[yellow]Warning: the unauthenticated health endpoint is reachable beyond this device. " + "[yellow]Warning: the unauthenticated health endpoint is listening beyond loopback " + "and may be reachable from other devices. " f"Keep port {port} private or protect it with a firewall or reverse proxy.[/yellow]" ) diff --git a/tests/cli/test_commands.py b/tests/cli/test_commands.py index fc8757f9..dce93986 100644 --- a/tests/cli/test_commands.py +++ b/tests/cli/test_commands.py @@ -2889,6 +2889,7 @@ def test_gateway_health_endpoint_binds_and_serves_expected_responses( assert captured["port"] == 18791 assert f"Health endpoint: {display_url}" in result.stdout assert ("unauthenticated health endpoint" in result.stdout) is warns_about_public_bind + assert ("may be reachable from other devices" in result.stdout) is warns_about_public_bind assert ("listening on 0.0.0.0" in result.stdout) is warns_about_public_bind health_handler = captured["handler"] @@ -2954,6 +2955,16 @@ def test_gateway_health_endpoint_binds_and_serves_expected_responses( asyncio.run(_exercise_connection_limit()) + class _NeverRespondingReader: + async def read(self, _size: int) -> bytes: + await asyncio.Event().wait() + + monkeypatch.setattr(cli_commands, "_GATEWAY_HEALTH_READ_TIMEOUT_SECONDS", 0.01) + timed_out_writer = _FakeWriter() + asyncio.run(health_handler(_NeverRespondingReader(), timed_out_writer)) + assert timed_out_writer.closed is True + assert timed_out_writer.output == b"" + def test_gateway_shutdown_lets_agent_task_own_mcp_cleanup( monkeypatch,