fix(memory): authenticate OpenViking cloud /health when anonymous probe fails
Hosted OpenViking (Volcengine) rejects anonymous GET /health with AuthenticationError, which made the provider look unhealthy and silently disabled automatic memory mirroring. Keep the anonymous probe first for identity safety, then retry once with the configured API key only when the server demands credentials. Fixes #78410
This commit is contained in:
parent
ccce6976e3
commit
d976670081
|
|
@ -451,8 +451,60 @@ class _VikingClient:
|
|||
)
|
||||
return self._parse_response(resp)
|
||||
|
||||
def _authenticated_json(self, path: str) -> dict:
|
||||
"""JSON GET with the configured API key (no tenant headers).
|
||||
|
||||
Used only after an anonymous probe is rejected for missing auth, so we
|
||||
still avoid disclosing credentials to a server that answers health
|
||||
anonymously.
|
||||
"""
|
||||
headers = {"Accept": "application/json"}
|
||||
# Reuse the same key headers as authenticated API calls, but omit
|
||||
# tenant identity — health is not a tenant-scoped resource.
|
||||
if self._api_key:
|
||||
headers["X-API-Key"] = self._api_key
|
||||
headers["Authorization"] = "Bearer " + self._api_key
|
||||
if self._agent:
|
||||
headers["X-OpenViking-Actor-Peer"] = self._agent
|
||||
resp = self._httpx.get(
|
||||
self._url(path), headers=headers, timeout=3.0
|
||||
)
|
||||
return self._parse_response(resp)
|
||||
|
||||
@staticmethod
|
||||
def _health_requires_credentials(exc: Exception) -> bool:
|
||||
"""True when /health rejected the anonymous probe for auth reasons."""
|
||||
status = getattr(exc, "status_code", None)
|
||||
if status in {401, 403}:
|
||||
return True
|
||||
message = str(exc).lower()
|
||||
return any(
|
||||
token in message
|
||||
for token in (
|
||||
"authenticationerror",
|
||||
"unauthorized",
|
||||
"api key",
|
||||
"apikey",
|
||||
"invalid authentication",
|
||||
"missing or invalid",
|
||||
)
|
||||
)
|
||||
|
||||
def health_payload(self) -> dict:
|
||||
return self._anonymous_json("/health")
|
||||
"""Fetch ``GET /health``.
|
||||
|
||||
Prefer an anonymous probe so credentials are never sent to an unknown
|
||||
host during identity checks. Hosted OpenViking (e.g. Volcengine cloud)
|
||||
requires authentication on ``/health``; when an API key is configured
|
||||
and the anonymous call is rejected for auth, retry once with that key
|
||||
so automatic memory mirroring is not silently disabled (#78410).
|
||||
"""
|
||||
try:
|
||||
return self._anonymous_json("/health")
|
||||
except _OpenVikingHTTPError as exc:
|
||||
if not self._api_key or not self._health_requires_credentials(exc):
|
||||
raise
|
||||
return self._authenticated_json("/health")
|
||||
|
||||
def openapi_payload(self) -> dict:
|
||||
return self._anonymous_json("/openapi.json")
|
||||
|
|
|
|||
|
|
@ -814,6 +814,96 @@ def test_repeated_openviking_health_probes_never_send_identity_headers(monkeypat
|
|||
]
|
||||
|
||||
|
||||
def test_cloud_health_retries_with_api_key_after_anonymous_auth_error(monkeypatch):
|
||||
"""Hosted OpenViking may require auth on GET /health (#78410)."""
|
||||
calls = []
|
||||
client = _VikingClient(
|
||||
"https://api.vikingdb.cn-beijing.volces.com/openviking",
|
||||
api_key="account.user.0123456789abcdef0123456789abcdef",
|
||||
agent="hermes",
|
||||
)
|
||||
modern = {"status": "ok", "healthy": True, "version": "0.3.0"}
|
||||
|
||||
def fake_get(url, **kwargs):
|
||||
headers = kwargs["headers"]
|
||||
calls.append(dict(headers))
|
||||
if "Authorization" not in headers:
|
||||
return SimpleNamespace(
|
||||
status_code=401,
|
||||
text='{"error":{"code":"AuthenticationError","message":"The API key in the request is missing or invalid."}}',
|
||||
json=lambda: {
|
||||
"error": {
|
||||
"code": "AuthenticationError",
|
||||
"message": "The API key in the request is missing or invalid.",
|
||||
}
|
||||
},
|
||||
)
|
||||
return SimpleNamespace(status_code=200, text="", json=lambda: modern)
|
||||
|
||||
monkeypatch.setattr(client._httpx, "get", fake_get)
|
||||
|
||||
payload = client.health_payload()
|
||||
assert payload == modern
|
||||
assert client.health() is True
|
||||
assert calls[0] == {"Accept": "application/json"}
|
||||
assert calls[1]["Authorization"].startswith("Bearer account.user.")
|
||||
assert calls[1]["X-API-Key"].startswith("account.user.")
|
||||
# No tenant headers on health.
|
||||
assert "X-OpenViking-Account" not in calls[1]
|
||||
assert "X-OpenViking-User" not in calls[1]
|
||||
|
||||
|
||||
def test_cloud_health_does_not_send_key_without_api_key(monkeypatch):
|
||||
client = _VikingClient(
|
||||
"https://api.vikingdb.cn-beijing.volces.com/openviking",
|
||||
api_key="",
|
||||
agent="hermes",
|
||||
)
|
||||
calls = []
|
||||
|
||||
def fake_get(url, **kwargs):
|
||||
calls.append(kwargs["headers"])
|
||||
return SimpleNamespace(
|
||||
status_code=401,
|
||||
text="AuthenticationError",
|
||||
json=lambda: {
|
||||
"error": {
|
||||
"code": "AuthenticationError",
|
||||
"message": "The API key in the request is missing or invalid.",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
monkeypatch.setattr(client._httpx, "get", fake_get)
|
||||
|
||||
with pytest.raises(openviking_module._OpenVikingHTTPError):
|
||||
client.health_payload()
|
||||
assert calls == [{"Accept": "application/json"}]
|
||||
|
||||
|
||||
def test_health_non_auth_errors_do_not_retry_with_credentials(monkeypatch):
|
||||
client = _VikingClient(
|
||||
"https://openviking.example",
|
||||
api_key="secret-key",
|
||||
agent="hermes",
|
||||
)
|
||||
calls = []
|
||||
|
||||
def fake_get(url, **kwargs):
|
||||
calls.append(kwargs["headers"])
|
||||
return SimpleNamespace(
|
||||
status_code=503,
|
||||
text="unavailable",
|
||||
json=lambda: {"error": {"code": "UNAVAILABLE", "message": "down"}},
|
||||
)
|
||||
|
||||
monkeypatch.setattr(client._httpx, "get", fake_get)
|
||||
|
||||
with pytest.raises(openviking_module._OpenVikingHTTPError):
|
||||
client.health_payload()
|
||||
assert calls == [{"Accept": "application/json"}]
|
||||
|
||||
|
||||
def test_modern_openviking_identity_does_not_probe_openapi():
|
||||
client = MagicMock()
|
||||
client.health_payload.return_value = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue