feat(openviking): identify Hermes requests

This commit is contained in:
ehz0ah
2026-08-25 18:07:32 +08:00
committed by kshitij
parent d1627d5133
commit 3db5267008
5 changed files with 93 additions and 10 deletions
+3
View File
@@ -78,6 +78,9 @@ profile's `.env`:
When `OPENVIKING_API_KEY` is set, Hermes lets OpenViking derive account/user
identity from the key. In local or trusted deployments without an API key,
Hermes sends `OPENVIKING_ACCOUNT` and `OPENVIKING_USER` as identity headers.
Hermes also sends `User-Agent: openviking-memory-hermes/<version>` on
OpenViking requests. This standard harness identifier contains the Hermes
version, but no per-user identifier, and does not add a separate request.
## Tools
+12 -2
View File
@@ -52,6 +52,7 @@ from urllib.request import url2pathname
from agent.message_content import flatten_message_text
from agent.memory_provider import MemoryProvider
from agent.skill_commands import extract_user_instruction_from_skill_message
from hermes_cli import __version__ as _HERMES_VERSION
from tools.registry import tool_error
from utils import atomic_json_write, env_var_enabled
@@ -65,6 +66,7 @@ logger = logging.getLogger(__name__)
_DEFAULT_ENDPOINT = "http://127.0.0.1:1933"
_OPENVIKING_SERVICE_ENDPOINT = "https://api.vikingdb.cn-beijing.volces.com/openviking"
_DEFAULT_AGENT = "hermes"
_OPENVIKING_USER_AGENT = f"openviking-memory-hermes/{_HERMES_VERSION}"
_AGENT_PROMPT_LABEL = "Hermes peer ID in OpenViking"
_OVCLI_CONFIG_ENV = "OPENVIKING_CLI_CONFIG_FILE"
_OVCLI_DEFAULT_RELATIVE_PATH = ".openviking/ovcli.conf"
@@ -340,7 +342,10 @@ class _VikingClient:
if include_tenant is None:
include_tenant = not bool(self._api_key)
h = {"Content-Type": "application/json"}
h = {
"Content-Type": "application/json",
"User-Agent": _OPENVIKING_USER_AGENT,
}
if self._agent:
h["X-OpenViking-Actor-Peer"] = self._agent
if include_tenant:
@@ -481,7 +486,12 @@ class _VikingClient:
def _anonymous_json(self, path: str) -> dict:
"""Probe server identity without disclosing credentials or tenant IDs."""
resp = self._httpx.get(
self._url(path), headers={"Accept": "application/json"}, timeout=3.0
self._url(path),
headers={
"Accept": "application/json",
"User-Agent": _OPENVIKING_USER_AGENT,
},
timeout=3.0,
)
return self._parse_response(resp)
@@ -9,6 +9,7 @@ from typing import Any, cast
from urllib.parse import parse_qs, urlparse
import plugins.memory.openviking as openviking_plugin
from hermes_cli import __version__ as _HERMES_VERSION
from plugins.memory.openviking import OpenVikingMemoryProvider
@@ -781,6 +782,10 @@ class TestOpenVikingAutoRecallPrefetch:
for headers in records["headers"]
]
assert all(headers.get("x-openviking-actor-peer") == "hermes" for headers in normalized_headers)
assert all(
headers.get("user-agent") == f"openviking-memory-hermes/{_HERMES_VERSION}"
for headers in normalized_headers
)
assert all(headers.get("x-openviking-account") == "acct" for headers in normalized_headers)
assert all(headers.get("x-openviking-user") == "user" for headers in normalized_headers)
@@ -11,12 +11,15 @@ from unittest.mock import MagicMock
import pytest
import plugins.memory.openviking as openviking_module
from hermes_cli import __version__ as _HERMES_VERSION
from plugins.memory.openviking import (
OpenVikingMemoryProvider,
_DEFERRED_COMMIT_TIMEOUT,
_VikingClient,
)
_EXPECTED_USER_AGENT = f"openviking-memory-hermes/{_HERMES_VERSION}"
def _clear_openviking_tenant_env(monkeypatch):
for name in ("OPENVIKING_ACCOUNT", "OPENVIKING_USER", "OPENVIKING_AGENT"):
@@ -767,6 +770,40 @@ def test_viking_client_delete_uses_identity_headers(monkeypatch):
assert captured["kwargs"]["params"] == {"uri": "viking://~/memories/x.md"}
assert captured["kwargs"]["headers"]["Authorization"] == "Bearer test-key"
assert captured["kwargs"]["headers"]["X-OpenViking-Actor-Peer"] == "hermes"
assert captured["kwargs"]["headers"]["User-Agent"] == _EXPECTED_USER_AGENT
def test_viking_client_upload_uses_user_agent_without_json_content_type(
tmp_path,
monkeypatch,
):
client = _VikingClient(
"https://example.com",
api_key="test-key",
account="acct",
user="alice",
agent="hermes",
)
upload = tmp_path / "notes.txt"
upload.write_text("notes", encoding="utf-8")
captured = {}
def capture_post(url, **kwargs):
captured["url"] = url
captured["kwargs"] = kwargs
return SimpleNamespace(
status_code=200,
text="",
json=lambda: {"result": {"temp_file_id": "temp-1"}},
)
monkeypatch.setattr(client._httpx, "post", capture_post)
assert client.upload_temp_file(upload) == "temp-1"
assert captured["url"] == "https://example.com/api/v1/resources/temp_upload"
headers = captured["kwargs"]["headers"]
assert headers["User-Agent"] == _EXPECTED_USER_AGENT
assert "Content-Type" not in headers
def test_openviking_identity_probes_are_anonymous_before_authenticated_requests(monkeypatch):
@@ -808,14 +845,20 @@ def test_openviking_identity_probes_are_anonymous_before_authenticated_requests(
"/api/v1/system/status",
"/api/v1/admin/accounts",
]
assert calls[0][1] == {"Accept": "application/json"}
assert calls[1][1] == {"Accept": "application/json"}
expected_anonymous_headers = {
"Accept": "application/json",
"User-Agent": _EXPECTED_USER_AGENT,
}
assert calls[0][1] == expected_anonymous_headers
assert calls[1][1] == expected_anonymous_headers
for _url, headers in calls[2:]:
assert headers["X-API-Key"] == "secret-key"
assert headers["Authorization"] == "Bearer secret-key"
def test_repeated_openviking_health_probes_never_send_identity_headers(monkeypatch):
def test_repeated_openviking_health_probes_never_send_credentials_or_tenant_headers(
monkeypatch,
):
captured_headers = []
client = _VikingClient(
"https://openviking.example",
@@ -838,8 +881,14 @@ def test_repeated_openviking_health_probes_never_send_identity_headers(monkeypat
assert client.health() is True
assert client.health() is True
assert captured_headers == [
{"Accept": "application/json"},
{"Accept": "application/json"},
{
"Accept": "application/json",
"User-Agent": _EXPECTED_USER_AGENT,
},
{
"Accept": "application/json",
"User-Agent": _EXPECTED_USER_AGENT,
},
]
@@ -874,7 +923,10 @@ def test_cloud_health_retries_with_api_key_after_anonymous_auth_error(monkeypatc
payload = client.health_payload()
assert payload == modern
assert client.health() is True
assert calls[0] == {"Accept": "application/json"}
assert calls[0] == {
"Accept": "application/json",
"User-Agent": _EXPECTED_USER_AGENT,
}
assert "Authorization" in calls[1]
assert calls[1]["Authorization"].startswith("Bearer account.user.")
assert "X-API-Key" in calls[1]
@@ -908,7 +960,12 @@ def test_cloud_health_does_not_send_key_without_api_key(monkeypatch):
with pytest.raises(openviking_module._OpenVikingHTTPError):
client.health_payload()
assert calls == [{"Accept": "application/json"}]
assert calls == [
{
"Accept": "application/json",
"User-Agent": _EXPECTED_USER_AGENT,
}
]
def test_health_non_auth_errors_do_not_retry_with_credentials(monkeypatch):
@@ -931,7 +988,12 @@ def test_health_non_auth_errors_do_not_retry_with_credentials(monkeypatch):
with pytest.raises(openviking_module._OpenVikingHTTPError):
client.health_payload()
assert calls == [{"Accept": "application/json"}]
assert calls == [
{
"Accept": "application/json",
"User-Agent": _EXPECTED_USER_AGENT,
}
]
def test_modern_openviking_identity_does_not_probe_openapi():
@@ -330,6 +330,9 @@ live in `ovcli.conf` (`OPENVIKING_CLI_CONFIG_FILE` or
`OPENVIKING_ACCOUNT` and `OPENVIKING_USER` are used for local/trusted mode.
`OPENVIKING_AGENT` is Hermes' peer ID in OpenViking for peer-scoped memories.
Hermes sends `User-Agent: openviking-memory-hermes/<version>` on OpenViking
requests. This standard harness identifier contains no per-user identifier and
does not add a separate request.
---