fix: verify dashboard sessions via config API

This commit is contained in:
Hermes Agent
2026-07-25 01:12:10 +00:00
parent 511baa60f3
commit a0afd8d076
2 changed files with 17 additions and 19 deletions
@@ -29,24 +29,25 @@ class GatewayHttpProbe(
bearerToken: String, bearerToken: String,
sessionCookie: String sessionCookie: String
): GatewayProbeResult { ): GatewayProbeResult {
// Hermes dashboard auth gates every protected route, including /health. // Hermes Dashboard and Hermes API Server are distinct upstream surfaces.
// A successful password login therefore must send its session cookie to // Dashboard /health and /v1/models can legitimately serve the SPA shell,
// the health probe as well as /v1/models; otherwise the 302-to-login // so they are not a valid dashboard authentication probe. A password
// response is incorrectly presented as a generic offline failure. // session is verified against the documented JSON dashboard config route.
val healthCookie = if (authMode == ConnectionAuthMode.PasswordLogin) sessionCookie else "" if (authMode == ConnectionAuthMode.PasswordLogin) {
val health = getJson("$baseUrl/health", sessionCookie = healthCookie) return when (val dashboard = getJson("$baseUrl/api/config", sessionCookie = sessionCookie)) {
HttpProbeResult.Success -> GatewayProbeResult.Healthy("Dashboard session authentication passed.")
else -> dashboard.toGatewayResult("Dashboard session check")
}
}
val health = getJson("$baseUrl/health")
if (health != HttpProbeResult.Success) return health.toGatewayResult("Health probe") if (health != HttpProbeResult.Success) return health.toGatewayResult("Health probe")
if (authMode == ConnectionAuthMode.None) { if (authMode == ConnectionAuthMode.None) {
return GatewayProbeResult.Healthy("Gateway health check passed.") return GatewayProbeResult.Healthy("Gateway health check passed.")
} }
val compatibility = when (authMode) { return when (val models = getJson("$baseUrl/v1/models", bearerToken = bearerToken)) {
ConnectionAuthMode.None -> error("Anonymous probes return after the health check.") HttpProbeResult.Success -> GatewayProbeResult.Healthy("Gateway health and bearer authentication checks passed.")
ConnectionAuthMode.BearerToken -> getJson("$baseUrl/v1/models", bearerToken = bearerToken) else -> models.toGatewayResult("Authenticated API check")
ConnectionAuthMode.PasswordLogin -> getJson("$baseUrl/v1/models", sessionCookie = sessionCookie)
}
return when (compatibility) {
HttpProbeResult.Success -> GatewayProbeResult.Healthy("Gateway health and authentication checks passed.")
else -> compatibility.toGatewayResult("Authenticated compatibility check")
} }
} }
@@ -56,7 +56,7 @@ class GatewayHttpProbeTest {
} }
@Test @Test
fun passwordSessionProbeUsesCookieOnlyForAuthenticatedCompatibilityCheck() = runBlocking { fun passwordSessionProbeUsesCookieOnlyForDashboardConfigCheck() = runBlocking {
val requests = mutableListOf<Triple<String, String?, String?>>() val requests = mutableListOf<Triple<String, String?, String?>>()
val probe = GatewayHttpProbe(client { chain -> val probe = GatewayHttpProbe(client { chain ->
requests += Triple( requests += Triple(
@@ -71,10 +71,7 @@ class GatewayHttpProbeTest {
assertTrue(result is GatewayProbeResult.Healthy) assertTrue(result is GatewayProbeResult.Healthy)
assertEquals( assertEquals(
listOf( listOf(Triple("/api/config", null, SESSION_COOKIE)),
Triple("/health", null, SESSION_COOKIE),
Triple("/v1/models", null, SESSION_COOKIE)
),
requests requests
) )
} }