fix(api_server): report hermes version on /health and /health/detailed (#40620)

Salvaged from #40479; re-verified on main, tightened, tested.

Co-authored-by: tfournet <tfournet@users.noreply.github.com>
This commit is contained in:
Teknium 2026-06-07 18:38:54 -07:00 committed by GitHub
parent d3b670e63e
commit 30c7913617
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 42 additions and 1 deletions

View file

@ -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),

View file

@ -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")
# ---------------------------------------------------------------------------