feat(openviking): identify Hermes requests
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user