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