diff --git a/gateway/platforms/api_server.py b/gateway/platforms/api_server.py index 13e97f4bd36..fb23664f017 100644 --- a/gateway/platforms/api_server.py +++ b/gateway/platforms/api_server.py @@ -61,6 +61,29 @@ from gateway.platforms.base import ( logger = logging.getLogger(__name__) + +def _hermes_version() -> str: + """Return the hermes-agent version string, or "dev" if it can't be resolved. + + Tries the installed package metadata first (authoritative for a pip/uv + install), then the in-tree ``hermes_cli.__version__`` (covers editable / + source checkouts where metadata may be stale or absent). Never raises — + a version probe must not be able to break the health endpoint. + """ + try: + from importlib.metadata import version + + return version("hermes-agent") + except Exception: + pass + try: + from hermes_cli import __version__ + + return __version__ + except Exception: + return "dev" + + # Default settings DEFAULT_HOST = "127.0.0.1" DEFAULT_PORT = 8642 @@ -1047,7 +1070,9 @@ class APIServerAdapter(BasePlatformAdapter): async def _handle_health(self, request: "web.Request") -> "web.Response": """GET /health — simple health check.""" - return web.json_response({"status": "ok", "platform": "hermes-agent"}) + return web.json_response( + {"status": "ok", "platform": "hermes-agent", "version": _hermes_version()} + ) async def _handle_health_detailed(self, request: "web.Request") -> "web.Response": """GET /health/detailed — rich status for cross-container dashboard probing. @@ -1062,6 +1087,7 @@ class APIServerAdapter(BasePlatformAdapter): return web.json_response({ "status": "ok", "platform": "hermes-agent", + "version": _hermes_version(), "gateway_state": runtime.get("gateway_state"), "platforms": runtime.get("platforms", {}), "active_agents": runtime.get("active_agents", 0), diff --git a/tests/gateway/test_api_server.py b/tests/gateway/test_api_server.py index c042fd556c6..95d49d8b4f1 100644 --- a/tests/gateway/test_api_server.py +++ b/tests/gateway/test_api_server.py @@ -497,6 +497,20 @@ class TestHealthEndpoint: assert data["status"] == "ok" assert data["platform"] == "hermes-agent" + @pytest.mark.asyncio + async def test_health_reports_version(self, adapter): + """GET /health must expose a non-empty version so orchestrators (e.g. + AgentOS) can read the gateway version without scraping. Regression + guard for the missing-version gap.""" + app = _create_app(adapter) + async with TestClient(TestServer(app)) as cli: + resp = await cli.get("/health") + assert resp.status == 200 + data = await resp.json() + assert "version" in data + assert isinstance(data["version"], str) + assert data["version"] != "" + @pytest.mark.asyncio async def test_v1_health_alias_returns_ok(self, adapter): """GET /v1/health should return the same response as /health.""" @@ -507,6 +521,7 @@ class TestHealthEndpoint: data = await resp.json() assert data["status"] == "ok" assert data["platform"] == "hermes-agent" + assert data.get("version") # ---------------------------------------------------------------------------