From 5cc47c994beb243407bb4c8ba47d2ab421cda9cf Mon Sep 17 00:00:00 2001 From: Victor Kyriazakos Date: Fri, 28 Aug 2026 01:31:10 +0000 Subject: [PATCH] fix(cron): resolve api_server host for the manual-run forward (bind parity) The forward dialed a hardcoded 127.0.0.1. The api_server adapter binds extra.host -> API_SERVER_HOST -> 127.0.0.1, so mirror that chain when dialing. Wildcard binds (0.0.0.0/::) listen on loopback, so keep dialing loopback for those; bracket bare IPv6 literals. --- tests/cron/test_cron_relay_run_forward.py | 31 +++++++++++++++++++++++ tools/cronjob_tools.py | 24 +++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/tests/cron/test_cron_relay_run_forward.py b/tests/cron/test_cron_relay_run_forward.py index a6831d6ff3..f0f3d4a3cb 100644 --- a/tests/cron/test_cron_relay_run_forward.py +++ b/tests/cron/test_cron_relay_run_forward.py @@ -86,4 +86,35 @@ class TestForwardRelayFrontedRun: ): cronjob_tools._forward_relay_fronted_run({"id": "abc123"}) assert sent["url"].endswith("/api/jobs/abc123/run") + assert sent["url"].startswith("http://127.0.0.1:") assert sent["headers"]["Authorization"] == "Bearer secret-key-16chars" + + def test_honors_api_server_host_env(self, monkeypatch): + """API_SERVER_HOST must reach the forward URL (adapter bind parity).""" + monkeypatch.setenv("API_SERVER_HOST", "10.9.8.7") + sent = {} + + def fake_post(url, headers=None, timeout=None): + sent["url"] = url + return _Resp(200) + + with patch.object( + cronjob_tools, "_relay_fronted_delivery_platforms", return_value={"discord"} + ), patch("httpx.post", side_effect=fake_post): + cronjob_tools._forward_relay_fronted_run({"id": "j1"}) + assert sent["url"].startswith("http://10.9.8.7:") + + def test_wildcard_bind_dials_loopback(self, monkeypatch): + """0.0.0.0 is a bind address, not a dial address — use loopback.""" + monkeypatch.setenv("API_SERVER_HOST", "0.0.0.0") + sent = {} + + def fake_post(url, headers=None, timeout=None): + sent["url"] = url + return _Resp(200) + + with patch.object( + cronjob_tools, "_relay_fronted_delivery_platforms", return_value={"discord"} + ), patch("httpx.post", side_effect=fake_post): + cronjob_tools._forward_relay_fronted_run({"id": "j1"}) + assert sent["url"].startswith("http://127.0.0.1:") diff --git a/tools/cronjob_tools.py b/tools/cronjob_tools.py index 43c4e874ac..894c1ef148 100644 --- a/tools/cronjob_tools.py +++ b/tools/cronjob_tools.py @@ -849,7 +849,29 @@ def _forward_relay_fronted_run(job: Dict[str, Any]) -> Optional[str]: port = int(port_raw) if port_raw else 8642 except ValueError: port = 8642 - url = f"http://127.0.0.1:{port}/api/jobs/{job_id}/run" + # Mirror the api_server's own bind resolution (adapter reads + # extra.host -> API_SERVER_HOST -> 127.0.0.1). A wildcard bind + # (0.0.0.0/::) listens on loopback too, so dial loopback for those. + host = "" + try: + from hermes_cli.config import cfg_get, load_config_readonly + + host = str( + cfg_get( + load_config_readonly(), "platforms", "api_server", "extra", "host", + default="", + ) + or "" + ).strip() + except Exception: + host = "" + if not host: + host = os.getenv("API_SERVER_HOST", "").strip() + if not host or host in ("0.0.0.0", "::", "*"): + host = "127.0.0.1" + if ":" in host and not host.startswith("["): + host = f"[{host}]" # bare IPv6 literal + url = f"http://{host}:{port}/api/jobs/{job_id}/run" from agent.secret_scope import get_secret