mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-15 04:12:25 +00:00
fix(openviking): add Bearer auth header and omit empty/legacy tenant headers (#21232)
Authenticated remote OpenViking servers derive tenancy from the Bearer key, but the client was always sending X-OpenViking-Account and X-OpenViking-User — defaulted to the literal string "default" — which overrode the key-derived tenant and broke auth. - _headers(): skip X-OpenViking-Account/-User when blank or "default" (treats the legacy default value as unset, so existing installs don't need to touch their .env) - _headers(): send Authorization: Bearer <key> alongside X-API-Key for standard HTTP auth compatibility - health(): include auth headers so /health works against servers that require authentication Tests cover bearer emission, legacy "default" suppression, empty suppression, real tenant passthrough, and authenticated health checks. Fixes the same user report as #20695 (from @ZaynJarvis); that PR could not be merged because its branch was stale against main and would have reverted recent OpenViking work (#15696, local resource uploads, summary URI normalization, fs-stat pre-check).
This commit is contained in:
parent
b12a5a72b0
commit
6e250a55de
2 changed files with 91 additions and 3 deletions
|
|
@ -100,14 +100,22 @@ class _VikingClient:
|
||||||
raise ImportError("httpx is required for OpenViking: pip install httpx")
|
raise ImportError("httpx is required for OpenViking: pip install httpx")
|
||||||
|
|
||||||
def _headers(self) -> dict:
|
def _headers(self) -> dict:
|
||||||
|
# Only send tenant headers when the user actually configured them.
|
||||||
|
# Legacy installs had account/user defaulted to the literal string
|
||||||
|
# "default" — treat that as unset so authenticated remote servers
|
||||||
|
# that derive tenancy from the Bearer key aren't overridden by a
|
||||||
|
# bogus tenant value.
|
||||||
h = {
|
h = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-OpenViking-Account": self._account,
|
|
||||||
"X-OpenViking-User": self._user,
|
|
||||||
"X-OpenViking-Agent": self._agent,
|
"X-OpenViking-Agent": self._agent,
|
||||||
}
|
}
|
||||||
|
if self._account and self._account != "default":
|
||||||
|
h["X-OpenViking-Account"] = self._account
|
||||||
|
if self._user and self._user != "default":
|
||||||
|
h["X-OpenViking-User"] = self._user
|
||||||
if self._api_key:
|
if self._api_key:
|
||||||
h["X-API-Key"] = self._api_key
|
h["X-API-Key"] = self._api_key
|
||||||
|
h["Authorization"] = "Bearer " + self._api_key
|
||||||
return h
|
return h
|
||||||
|
|
||||||
def _url(self, path: str) -> str:
|
def _url(self, path: str) -> str:
|
||||||
|
|
@ -179,7 +187,7 @@ class _VikingClient:
|
||||||
def health(self) -> bool:
|
def health(self) -> bool:
|
||||||
try:
|
try:
|
||||||
resp = self._httpx.get(
|
resp = self._httpx.get(
|
||||||
self._url("/health"), timeout=3.0
|
self._url("/health"), headers=self._headers(), timeout=3.0
|
||||||
)
|
)
|
||||||
return resp.status_code == 200
|
return resp.status_code == 200
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
|
||||||
|
|
@ -299,3 +299,83 @@ def test_viking_client_raises_structured_server_error():
|
||||||
|
|
||||||
with pytest.raises(RuntimeError, match="PERMISSION_DENIED"):
|
with pytest.raises(RuntimeError, match="PERMISSION_DENIED"):
|
||||||
client._parse_response(response)
|
client._parse_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def test_viking_client_headers_include_bearer_when_api_key_set():
|
||||||
|
client = _VikingClient(
|
||||||
|
"https://example.com",
|
||||||
|
api_key="test-key",
|
||||||
|
account="acct",
|
||||||
|
user="usr",
|
||||||
|
agent="hermes",
|
||||||
|
)
|
||||||
|
headers = client._headers()
|
||||||
|
assert headers["X-API-Key"] == "test-key"
|
||||||
|
assert headers["Authorization"] == "Bearer test-key"
|
||||||
|
|
||||||
|
|
||||||
|
def test_viking_client_headers_omit_tenant_when_legacy_default():
|
||||||
|
# Existing installs have account/user set to the literal string "default".
|
||||||
|
# Those should NOT be sent as headers — the server would interpret that
|
||||||
|
# as a real tenant override and reject/misroute requests.
|
||||||
|
client = _VikingClient(
|
||||||
|
"https://example.com",
|
||||||
|
api_key="test-key",
|
||||||
|
account="default",
|
||||||
|
user="default",
|
||||||
|
agent="hermes",
|
||||||
|
)
|
||||||
|
headers = client._headers()
|
||||||
|
assert "X-OpenViking-Account" not in headers
|
||||||
|
assert "X-OpenViking-User" not in headers
|
||||||
|
assert headers["X-OpenViking-Agent"] == "hermes"
|
||||||
|
assert headers["Authorization"] == "Bearer test-key"
|
||||||
|
|
||||||
|
|
||||||
|
def test_viking_client_headers_omit_tenant_when_empty():
|
||||||
|
client = _VikingClient(
|
||||||
|
"https://example.com",
|
||||||
|
api_key="",
|
||||||
|
account="",
|
||||||
|
user="",
|
||||||
|
agent="hermes",
|
||||||
|
)
|
||||||
|
headers = client._headers()
|
||||||
|
assert "X-OpenViking-Account" not in headers
|
||||||
|
assert "X-OpenViking-User" not in headers
|
||||||
|
assert "Authorization" not in headers
|
||||||
|
assert "X-API-Key" not in headers
|
||||||
|
|
||||||
|
|
||||||
|
def test_viking_client_headers_sent_with_real_tenant_values():
|
||||||
|
client = _VikingClient(
|
||||||
|
"https://example.com",
|
||||||
|
api_key="test-key",
|
||||||
|
account="real-account",
|
||||||
|
user="real-user",
|
||||||
|
agent="hermes",
|
||||||
|
)
|
||||||
|
headers = client._headers()
|
||||||
|
assert headers["X-OpenViking-Account"] == "real-account"
|
||||||
|
assert headers["X-OpenViking-User"] == "real-user"
|
||||||
|
|
||||||
|
|
||||||
|
def test_viking_client_health_sends_auth_headers(monkeypatch):
|
||||||
|
client = _VikingClient(
|
||||||
|
"https://example.com",
|
||||||
|
api_key="test-key",
|
||||||
|
account="",
|
||||||
|
user="",
|
||||||
|
agent="hermes",
|
||||||
|
)
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def capture_get(url, **kwargs):
|
||||||
|
captured["url"] = url
|
||||||
|
captured["headers"] = kwargs.get("headers") or {}
|
||||||
|
return SimpleNamespace(status_code=200)
|
||||||
|
|
||||||
|
monkeypatch.setattr(client._httpx, "get", capture_get)
|
||||||
|
assert client.health() is True
|
||||||
|
assert captured["url"] == "https://example.com/health"
|
||||||
|
assert captured["headers"]["Authorization"] == "Bearer test-key"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue