mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-20 10:11:58 +00:00
Merge pull request #48206 from ehz0ah/fix/openviking-current-api-rebased
fix(openviking): adapt memory provider for current api
This commit is contained in:
commit
4af16b5da2
6 changed files with 222 additions and 91 deletions
|
|
@ -31,10 +31,14 @@ All config via environment variables in `.env`:
|
|||
| Env Var | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `OPENVIKING_ENDPOINT` | `http://127.0.0.1:1933` | Server URL |
|
||||
| `OPENVIKING_API_KEY` | (none) | API key (optional) |
|
||||
| `OPENVIKING_ACCOUNT` | (none) | Tenant account override |
|
||||
| `OPENVIKING_USER` | (none) | Tenant user override |
|
||||
| `OPENVIKING_AGENT` | `hermes` | Tenant agent namespace |
|
||||
| `OPENVIKING_API_KEY` | (none) | User/admin API key for authenticated servers |
|
||||
| `OPENVIKING_ACCOUNT` | `default` | Tenant account for local/trusted mode |
|
||||
| `OPENVIKING_USER` | `default` | Tenant user for local/trusted mode |
|
||||
| `OPENVIKING_AGENT` | `hermes` | Hermes peer ID in OpenViking, used for peer-scoped memories |
|
||||
|
||||
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.
|
||||
|
||||
## Tools
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ Config via environment variables (profile-scoped via each profile's .env)
|
|||
or a linked OpenViking CLI config:
|
||||
OPENVIKING_ENDPOINT — Server URL (default: http://127.0.0.1:1933)
|
||||
OPENVIKING_API_KEY — API key (required for authenticated servers)
|
||||
OPENVIKING_ACCOUNT — Optional tenant account override
|
||||
OPENVIKING_USER — Optional tenant user override
|
||||
OPENVIKING_AGENT — Tenant agent (default: hermes)
|
||||
OPENVIKING_ACCOUNT — Tenant account for local/trusted mode (default: default)
|
||||
OPENVIKING_USER — Tenant user for local/trusted mode (default: default)
|
||||
OPENVIKING_AGENT — Hermes peer ID in OpenViking (default: hermes)
|
||||
|
||||
Capabilities:
|
||||
- Automatic memory extraction on session commit (6 categories)
|
||||
|
|
@ -55,6 +55,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"
|
||||
_AGENT_PROMPT_LABEL = "Hermes peer ID in OpenViking"
|
||||
_OVCLI_CONFIG_ENV = "OPENVIKING_CLI_CONFIG_FILE"
|
||||
_OVCLI_DEFAULT_RELATIVE_PATH = ".openviking/ovcli.conf"
|
||||
_OVCLI_SAVED_PREFIX = "ovcli.conf."
|
||||
|
|
@ -200,10 +201,9 @@ class _VikingClient:
|
|||
agent: Optional[str] = None):
|
||||
self._endpoint = endpoint.rstrip("/")
|
||||
self._api_key = api_key
|
||||
# Empty account/user fall back to "default" and the tenant headers are
|
||||
# always sent — ROOT API keys require them (preserves the merged
|
||||
# contract from #22414/#21232; an empty string must NOT omit the
|
||||
# header). Use `or` (not `is not None`) so "" also falls back.
|
||||
# Account/user are local/trusted-mode tenant identity. API-key requests
|
||||
# omit these headers by default; trusted-mode retry may send them only
|
||||
# after OpenViking explicitly asks for asserted tenant identity.
|
||||
self._account = account or os.environ.get("OPENVIKING_ACCOUNT", "default")
|
||||
self._user = user or os.environ.get("OPENVIKING_USER", "default")
|
||||
self._agent = agent if agent is not None else os.environ.get("OPENVIKING_AGENT", _DEFAULT_AGENT)
|
||||
|
|
@ -211,15 +211,18 @@ class _VikingClient:
|
|||
if self._httpx is None:
|
||||
raise ImportError("httpx is required for OpenViking: pip install httpx")
|
||||
|
||||
def _headers(self) -> dict:
|
||||
def _headers(self, *, include_tenant: bool | None = None) -> dict:
|
||||
if include_tenant is None:
|
||||
include_tenant = not bool(self._api_key)
|
||||
|
||||
h = {"Content-Type": "application/json"}
|
||||
if self._agent:
|
||||
h["X-OpenViking-Actor-Peer"] = self._agent
|
||||
h["X-OpenViking-Agent"] = self._agent
|
||||
if self._account:
|
||||
h["X-OpenViking-Account"] = self._account
|
||||
if self._user:
|
||||
h["X-OpenViking-User"] = self._user
|
||||
if include_tenant:
|
||||
if self._account:
|
||||
h["X-OpenViking-Account"] = self._account
|
||||
if self._user:
|
||||
h["X-OpenViking-User"] = self._user
|
||||
if self._api_key:
|
||||
h["X-API-Key"] = self._api_key
|
||||
h["Authorization"] = "Bearer " + self._api_key
|
||||
|
|
@ -228,11 +231,33 @@ class _VikingClient:
|
|||
def _url(self, path: str) -> str:
|
||||
return f"{self._endpoint}{path}"
|
||||
|
||||
def _multipart_headers(self) -> dict:
|
||||
headers = self._headers()
|
||||
def _multipart_headers(self, *, include_tenant: bool | None = None) -> dict:
|
||||
headers = self._headers(include_tenant=include_tenant)
|
||||
headers.pop("Content-Type", None)
|
||||
return headers
|
||||
|
||||
@staticmethod
|
||||
def _needs_trusted_identity_retry(exc: Exception) -> bool:
|
||||
message = str(exc)
|
||||
return (
|
||||
"Trusted mode requests must include X-OpenViking-Account" in message
|
||||
or "Trusted mode requests must include X-OpenViking-User" in message
|
||||
or "Trusted mode requests must include X-OpenViking-Account or explicit account_id" in message
|
||||
)
|
||||
|
||||
def _send_with_trusted_identity_retry(self, send, *, multipart: bool = False) -> dict:
|
||||
try:
|
||||
headers = self._multipart_headers() if multipart else self._headers()
|
||||
return self._parse_response(send(headers))
|
||||
except Exception as exc:
|
||||
if not self._api_key or not self._needs_trusted_identity_retry(exc):
|
||||
raise
|
||||
headers = (
|
||||
self._multipart_headers(include_tenant=True)
|
||||
if multipart else self._headers(include_tenant=True)
|
||||
)
|
||||
return self._parse_response(send(headers))
|
||||
|
||||
def _parse_response(self, resp) -> dict:
|
||||
try:
|
||||
data = resp.json()
|
||||
|
|
@ -267,28 +292,33 @@ class _VikingClient:
|
|||
return data
|
||||
|
||||
def get(self, path: str, **kwargs) -> dict:
|
||||
resp = self._httpx.get(
|
||||
self._url(path), headers=self._headers(), timeout=_TIMEOUT, **kwargs
|
||||
return self._send_with_trusted_identity_retry(
|
||||
lambda headers: self._httpx.get(
|
||||
self._url(path), headers=headers, timeout=_TIMEOUT, **kwargs
|
||||
)
|
||||
)
|
||||
return self._parse_response(resp)
|
||||
|
||||
def post(self, path: str, payload: dict = None, **kwargs) -> dict:
|
||||
resp = self._httpx.post(
|
||||
self._url(path), json=payload or {}, headers=self._headers(),
|
||||
timeout=_TIMEOUT, **kwargs
|
||||
return self._send_with_trusted_identity_retry(
|
||||
lambda headers: self._httpx.post(
|
||||
self._url(path), json=payload or {}, headers=headers,
|
||||
timeout=_TIMEOUT, **kwargs
|
||||
)
|
||||
)
|
||||
return self._parse_response(resp)
|
||||
|
||||
def upload_temp_file(self, file_path: Path) -> str:
|
||||
mime_type = mimetypes.guess_type(file_path.name)[0] or "application/octet-stream"
|
||||
with file_path.open("rb") as f:
|
||||
resp = self._httpx.post(
|
||||
self._url("/api/v1/resources/temp_upload"),
|
||||
files={"file": (file_path.name, f, mime_type)},
|
||||
headers=self._multipart_headers(),
|
||||
timeout=_TIMEOUT,
|
||||
)
|
||||
data = self._parse_response(resp)
|
||||
|
||||
def _send(headers):
|
||||
with file_path.open("rb") as f:
|
||||
return self._httpx.post(
|
||||
self._url("/api/v1/resources/temp_upload"),
|
||||
files={"file": (file_path.name, f, mime_type)},
|
||||
headers=headers,
|
||||
timeout=_TIMEOUT,
|
||||
)
|
||||
|
||||
data = self._send_with_trusted_identity_retry(_send, multipart=True)
|
||||
result = data.get("result", {})
|
||||
temp_file_id = result.get("temp_file_id", "")
|
||||
if not temp_file_id:
|
||||
|
|
@ -1219,7 +1249,7 @@ def _prompt_manual_connection_values(prompt, select, cancelled, *, service: bool
|
|||
return _SETUP_CANCELLED
|
||||
if credential_choice == 0:
|
||||
values["agent"] = _clean_config_value(
|
||||
prompt("OpenViking agent", default=_DEFAULT_AGENT)
|
||||
prompt(_AGENT_PROMPT_LABEL, default=_DEFAULT_AGENT)
|
||||
) or _DEFAULT_AGENT
|
||||
_print_validation_progress("Validating OpenViking local dev access...")
|
||||
valid, message, _role = _validate_openviking_setup_values(values)
|
||||
|
|
@ -1339,7 +1369,7 @@ def _prompt_manual_connection_values(prompt, select, cancelled, *, service: bool
|
|||
prefilled_agent = ""
|
||||
else:
|
||||
values["agent"] = _clean_config_value(
|
||||
prompt("OpenViking agent", default=_DEFAULT_AGENT)
|
||||
prompt(_AGENT_PROMPT_LABEL, default=_DEFAULT_AGENT)
|
||||
) or _DEFAULT_AGENT
|
||||
_print_validation_progress("Validating OpenViking API access...")
|
||||
valid, message, role = _validate_openviking_setup_values(
|
||||
|
|
@ -1697,7 +1727,10 @@ class OpenVikingMemoryProvider(MemoryProvider):
|
|||
},
|
||||
{
|
||||
"key": "agent",
|
||||
"description": "OpenViking agent ID within the account ([hermes], useful in multi-agent mode)",
|
||||
"description": (
|
||||
"Hermes peer ID in OpenViking, sent as the actor peer and "
|
||||
"used for peer-scoped memories"
|
||||
),
|
||||
"default": "hermes",
|
||||
"env_var": "OPENVIKING_AGENT",
|
||||
},
|
||||
|
|
@ -2129,18 +2162,22 @@ class OpenVikingMemoryProvider(MemoryProvider):
|
|||
def _text_part(content: str) -> Dict[str, str]:
|
||||
return {"type": "text", "text": content}
|
||||
|
||||
@classmethod
|
||||
def _turn_batch_payload(cls, user_content: str, assistant_content: str) -> Dict[str, Any]:
|
||||
def _turn_batch_payload(self, user_content: str, assistant_content: str) -> Dict[str, Any]:
|
||||
assistant_message: Dict[str, Any] = {
|
||||
"role": "assistant",
|
||||
"parts": [self._text_part(assistant_content)],
|
||||
}
|
||||
if self._agent:
|
||||
assistant_message["peer_id"] = self._agent
|
||||
return {
|
||||
"messages": [
|
||||
{"role": "user", "parts": [cls._text_part(user_content)]},
|
||||
{"role": "assistant", "parts": [cls._text_part(assistant_content)]},
|
||||
{"role": "user", "parts": [self._text_part(user_content)]},
|
||||
assistant_message,
|
||||
]
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _post_session_turn(
|
||||
cls,
|
||||
self,
|
||||
client: _VikingClient,
|
||||
sid: str,
|
||||
user_content: str,
|
||||
|
|
@ -2148,7 +2185,7 @@ class OpenVikingMemoryProvider(MemoryProvider):
|
|||
) -> None:
|
||||
client.post(
|
||||
f"/api/v1/sessions/{sid}/messages/batch",
|
||||
cls._turn_batch_payload(user_content, assistant_content),
|
||||
self._turn_batch_payload(user_content, assistant_content),
|
||||
)
|
||||
|
||||
def _session_has_pending_tokens(self, sid: str) -> bool:
|
||||
|
|
@ -2402,9 +2439,9 @@ class OpenVikingMemoryProvider(MemoryProvider):
|
|||
)
|
||||
|
||||
def _build_memory_uri(self, subdir: str) -> str:
|
||||
"""Build a viking:// memory URI under the configured user/agent/subdir."""
|
||||
"""Build a viking:// memory URI under the configured peer namespace."""
|
||||
slug = uuid.uuid4().hex[:12]
|
||||
return f"viking://user/{self._user}/agent/{self._agent}/memories/{subdir}/mem_{slug}.md"
|
||||
return f"viking://user/peers/{self._agent}/memories/{subdir}/mem_{slug}.md"
|
||||
|
||||
def on_memory_write(
|
||||
self,
|
||||
|
|
@ -2535,14 +2572,16 @@ class OpenVikingMemoryProvider(MemoryProvider):
|
|||
|
||||
payload: Dict[str, Any] = {"query": query}
|
||||
mode = args.get("mode", "auto")
|
||||
if mode != "auto":
|
||||
payload["mode"] = mode
|
||||
if args.get("scope"):
|
||||
payload["target_uri"] = args["scope"]
|
||||
if args.get("limit"):
|
||||
payload["limit"] = args["limit"]
|
||||
|
||||
resp = self._client.post("/api/v1/search/find", payload)
|
||||
endpoint = "/api/v1/search/search" if mode == "deep" else "/api/v1/search/find"
|
||||
if endpoint == "/api/v1/search/search" and self._session_id:
|
||||
payload["session_id"] = self._session_id
|
||||
|
||||
resp = self._client.post(endpoint, payload)
|
||||
result = resp.get("result", {})
|
||||
|
||||
# Format results for the model — keep it concise
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ AUTHOR_MAP = {
|
|||
"al@randomsnowflake.me": "randomsnowflake",
|
||||
"zakame@zakame.net": "zakame",
|
||||
"152110621+jiangkoumo@users.noreply.github.com": "jiangkoumo",
|
||||
"qinhaojie.exe@bytedance.com": "qin-ctx",
|
||||
"834740219@qq.com": "ViewWay",
|
||||
"matt@vestigial.dev": "m4dni5",
|
||||
"harjoth.khara@gmail.com": "harjothkhara",
|
||||
|
|
|
|||
|
|
@ -239,6 +239,7 @@ class TestOpenVikingSkillQuerySafety:
|
|||
{
|
||||
"role": "assistant",
|
||||
"parts": [{"type": "text", "text": "Done."}],
|
||||
"peer_id": "hermes",
|
||||
},
|
||||
]
|
||||
},
|
||||
|
|
@ -474,8 +475,8 @@ class TestOpenVikingBrowse:
|
|||
class TestOpenVikingMemoryUriBuilder:
|
||||
"""Regression tests for _build_memory_uri — fixes #36969.
|
||||
|
||||
Before the fix the URI omitted /agent/{agent}/, causing all agents
|
||||
under the same user to share the same memory namespace.
|
||||
OpenViking's current memory layout stores peer-scoped memories under
|
||||
viking://user/peers/{peer_id}/...
|
||||
"""
|
||||
|
||||
def _make_provider(self, user="alice", agent="coder"):
|
||||
|
|
@ -484,19 +485,19 @@ class TestOpenVikingMemoryUriBuilder:
|
|||
p._agent = agent
|
||||
return p
|
||||
|
||||
def test_uri_layout_includes_agent_segment(self):
|
||||
"""URI must contain /agent/{agent}/ between user and memories."""
|
||||
def test_uri_layout_includes_peer_segment(self):
|
||||
"""URI must contain /peers/{peer_id}/ between user and memories."""
|
||||
p = self._make_provider(user="alice", agent="coder")
|
||||
uri = p._build_memory_uri("preferences")
|
||||
assert uri.startswith("viking://user/alice/agent/coder/memories/preferences/mem_")
|
||||
assert uri.startswith("viking://user/peers/coder/memories/preferences/mem_")
|
||||
assert uri.endswith(".md")
|
||||
|
||||
def test_uri_uses_configured_agent_not_default(self):
|
||||
"""_agent value must be interpolated — not hardcoded to 'hermes'."""
|
||||
def test_uri_uses_configured_peer_not_default(self):
|
||||
"""_agent value is the OpenViking actor peer ID, not hardcoded to 'hermes'."""
|
||||
p = self._make_provider(user="alice", agent="research-bot")
|
||||
uri = p._build_memory_uri("entities")
|
||||
assert "/agent/research-bot/" in uri
|
||||
assert "/agent/hermes/" not in uri
|
||||
assert "/peers/research-bot/" in uri
|
||||
assert "/peers/hermes/" not in uri
|
||||
|
||||
def test_uri_slug_is_twelve_hex_chars_and_unique(self):
|
||||
"""Slug must be 12 hex chars and differ between calls."""
|
||||
|
|
|
|||
|
|
@ -369,7 +369,7 @@ def test_post_setup_create_remote_user_profile_can_mirror_to_openviking_store(tm
|
|||
_prompt_from_values({
|
||||
"OpenViking server URL": "https://openviking.example",
|
||||
"OpenViking user API key": "user-secret",
|
||||
"OpenViking agent": "hermes",
|
||||
"Hermes peer ID in OpenViking": "hermes",
|
||||
"OpenViking profile name": "VPS",
|
||||
}),
|
||||
)
|
||||
|
|
@ -411,7 +411,7 @@ def test_post_setup_create_remote_user_can_keep_hermes_only(tmp_path, monkeypatc
|
|||
_prompt_from_values({
|
||||
"OpenViking server URL": "https://openviking.example",
|
||||
"OpenViking user API key": "user-secret",
|
||||
"OpenViking agent": "agent",
|
||||
"Hermes peer ID in OpenViking": "agent",
|
||||
}),
|
||||
)
|
||||
config = {"memory": {}}
|
||||
|
|
@ -455,7 +455,7 @@ def test_post_setup_create_openviking_service_validates_after_api_key(tmp_path,
|
|||
_prompt_from_values(
|
||||
{
|
||||
"OpenViking API key": "service-secret",
|
||||
"OpenViking agent": "agent",
|
||||
"Hermes peer ID in OpenViking": "agent",
|
||||
},
|
||||
forbidden={"OpenViking server URL", "OpenViking user API key", "OpenViking root API key"},
|
||||
),
|
||||
|
|
@ -540,7 +540,7 @@ def test_post_setup_user_key_path_can_route_detected_root_key_to_root_setup(tmp_
|
|||
"OpenViking user API key": "root-secret",
|
||||
"OpenViking account": "acct",
|
||||
"OpenViking user": "alice",
|
||||
"OpenViking agent": "agent",
|
||||
"Hermes peer ID in OpenViking": "agent",
|
||||
}
|
||||
return values.get(label, default or "")
|
||||
|
||||
|
|
@ -549,7 +549,7 @@ def test_post_setup_user_key_path_can_route_detected_root_key_to_root_setup(tmp_
|
|||
|
||||
OpenVikingMemoryProvider().post_setup(str(hermes_home), config)
|
||||
|
||||
assert prompt_events.count("OpenViking agent") == 1
|
||||
assert prompt_events.count("Hermes peer ID in OpenViking") == 1
|
||||
env_text = (hermes_home / ".env").read_text(encoding="utf-8")
|
||||
assert "OPENVIKING_API_KEY=root-secret" in env_text
|
||||
assert "OPENVIKING_ACCOUNT=acct" in env_text
|
||||
|
|
@ -580,7 +580,7 @@ def test_post_setup_root_key_path_can_route_detected_user_key_to_user_setup(tmp_
|
|||
{
|
||||
"OpenViking server URL": "https://openviking.example",
|
||||
"OpenViking root API key": "user-secret",
|
||||
"OpenViking agent": "agent",
|
||||
"Hermes peer ID in OpenViking": "agent",
|
||||
},
|
||||
forbidden={"OpenViking user API key", "OpenViking account", "OpenViking user"},
|
||||
),
|
||||
|
|
@ -616,7 +616,7 @@ def test_manual_root_key_flow_prints_validation_progress(monkeypatch, capsys):
|
|||
"OpenViking root API key": "root-secret",
|
||||
"OpenViking account": "acct",
|
||||
"OpenViking user": "alice",
|
||||
"OpenViking agent": "agent",
|
||||
"Hermes peer ID in OpenViking": "agent",
|
||||
}),
|
||||
lambda *args, **kwargs: next(choices),
|
||||
-1,
|
||||
|
|
@ -1091,7 +1091,7 @@ def test_post_setup_local_server_down_can_offer_autostart(tmp_path, monkeypatch)
|
|||
"_prompt",
|
||||
_prompt_from_values({
|
||||
"OpenViking server URL": "localhost",
|
||||
"OpenViking agent": "agent",
|
||||
"Hermes peer ID in OpenViking": "agent",
|
||||
}),
|
||||
)
|
||||
config = {"memory": {}}
|
||||
|
|
@ -1126,7 +1126,7 @@ def test_post_setup_invalid_env_profile_can_create_new_config(tmp_path, monkeypa
|
|||
_prompt_from_values({
|
||||
"OpenViking server URL": "https://openviking.example",
|
||||
"OpenViking user API key": "user-secret",
|
||||
"OpenViking agent": "agent",
|
||||
"Hermes peer ID in OpenViking": "agent",
|
||||
}),
|
||||
)
|
||||
config = {"memory": {}}
|
||||
|
|
@ -1210,6 +1210,36 @@ def test_tool_search_sends_limit_not_legacy_top_k():
|
|||
assert "top_k" not in payload
|
||||
|
||||
|
||||
def test_tool_search_uses_find_for_normal_search():
|
||||
provider = OpenVikingMemoryProvider()
|
||||
provider._client = MagicMock()
|
||||
provider._client.post.return_value = {
|
||||
"result": {"memories": [], "resources": [], "skills": [], "total": 0}
|
||||
}
|
||||
|
||||
provider._tool_search({"query": "simple lookup", "mode": "fast"})
|
||||
|
||||
provider._client.post.assert_called_once_with("/api/v1/search/find", {
|
||||
"query": "simple lookup",
|
||||
})
|
||||
|
||||
|
||||
def test_tool_search_uses_session_search_for_deep_search():
|
||||
provider = OpenVikingMemoryProvider()
|
||||
provider._client = MagicMock()
|
||||
provider._session_id = "session-123"
|
||||
provider._client.post.return_value = {
|
||||
"result": {"memories": [], "resources": [], "skills": [], "total": 0}
|
||||
}
|
||||
|
||||
provider._tool_search({"query": "connect facts", "mode": "deep"})
|
||||
|
||||
provider._client.post.assert_called_once_with("/api/v1/search/search", {
|
||||
"query": "connect facts",
|
||||
"session_id": "session-123",
|
||||
})
|
||||
|
||||
|
||||
def test_tool_add_resource_uploads_existing_local_file(tmp_path):
|
||||
sample = tmp_path / "sample.md"
|
||||
sample.write_text("# Local resource\n", encoding="utf-8")
|
||||
|
|
@ -1457,10 +1487,10 @@ def test_viking_client_upload_temp_file_uses_multipart_identity_headers(tmp_path
|
|||
assert "files" in captured_kwargs
|
||||
assert "json" not in captured_kwargs
|
||||
headers = captured_kwargs["headers"]
|
||||
assert headers["X-OpenViking-Account"] == "test-account"
|
||||
assert headers["X-OpenViking-User"] == "test-user"
|
||||
assert "X-OpenViking-Account" not in headers
|
||||
assert "X-OpenViking-User" not in headers
|
||||
assert headers["X-OpenViking-Actor-Peer"] == "test-agent"
|
||||
assert headers["X-OpenViking-Agent"] == "test-agent"
|
||||
assert "X-OpenViking-Agent" not in headers
|
||||
assert headers["X-API-Key"] == "test-key"
|
||||
assert "Content-Type" not in headers
|
||||
|
||||
|
|
@ -1517,16 +1547,17 @@ def test_viking_client_headers_include_bearer_when_api_key_set():
|
|||
headers = client._headers()
|
||||
assert headers["X-API-Key"] == "test-key"
|
||||
assert headers["Authorization"] == "Bearer test-key"
|
||||
assert headers["X-OpenViking-Actor-Peer"] == "hermes"
|
||||
assert "X-OpenViking-Agent" not in headers
|
||||
assert "X-OpenViking-Account" not in headers
|
||||
assert "X-OpenViking-User" not in headers
|
||||
|
||||
|
||||
def test_viking_client_headers_send_tenant_when_default():
|
||||
# account/user set to the literal string "default". OpenViking 0.3.x
|
||||
# requires X-OpenViking-Account and X-OpenViking-User for ROOT API key
|
||||
# requests to tenant-scoped APIs — omitting them causes
|
||||
# INVALID_ARGUMENT errors even when account="default".
|
||||
def test_viking_client_headers_send_tenant_in_local_mode():
|
||||
# Local/trusted mode needs explicit tenant identity headers.
|
||||
client = _VikingClient(
|
||||
"https://example.com",
|
||||
api_key="test-key",
|
||||
api_key="",
|
||||
account="default",
|
||||
user="default",
|
||||
agent="hermes",
|
||||
|
|
@ -1535,14 +1566,13 @@ def test_viking_client_headers_send_tenant_when_default():
|
|||
assert headers["X-OpenViking-Account"] == "default"
|
||||
assert headers["X-OpenViking-User"] == "default"
|
||||
assert headers["X-OpenViking-Actor-Peer"] == "hermes"
|
||||
assert headers["X-OpenViking-Agent"] == "hermes"
|
||||
assert headers["Authorization"] == "Bearer test-key"
|
||||
assert "X-OpenViking-Agent" not in headers
|
||||
assert "Authorization" not in headers
|
||||
|
||||
|
||||
def test_viking_client_headers_send_tenant_when_empty_falls_back_to_default(monkeypatch):
|
||||
_clear_openviking_tenant_env(monkeypatch)
|
||||
# Empty account/user strings fall back to "default" via the constructor.
|
||||
# Headers are sent even for the default value — ROOT API keys need them.
|
||||
# Empty account/user strings fall back to "default" in local mode.
|
||||
client = _VikingClient(
|
||||
"https://example.com",
|
||||
api_key="",
|
||||
|
|
@ -1553,11 +1583,13 @@ def test_viking_client_headers_send_tenant_when_empty_falls_back_to_default(monk
|
|||
headers = client._headers()
|
||||
assert headers["X-OpenViking-Account"] == "default"
|
||||
assert headers["X-OpenViking-User"] == "default"
|
||||
assert headers["X-OpenViking-Actor-Peer"] == "hermes"
|
||||
assert "X-OpenViking-Agent" 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():
|
||||
def test_viking_client_headers_can_include_tenant_for_trusted_retry():
|
||||
client = _VikingClient(
|
||||
"https://example.com",
|
||||
api_key="test-key",
|
||||
|
|
@ -1565,9 +1597,54 @@ def test_viking_client_headers_sent_with_real_tenant_values():
|
|||
user="real-user",
|
||||
agent="hermes",
|
||||
)
|
||||
headers = client._headers()
|
||||
headers = client._headers(include_tenant=True)
|
||||
assert headers["X-OpenViking-Account"] == "real-account"
|
||||
assert headers["X-OpenViking-User"] == "real-user"
|
||||
assert headers["Authorization"] == "Bearer test-key"
|
||||
|
||||
|
||||
def test_viking_client_retries_with_tenant_headers_for_trusted_mode(monkeypatch):
|
||||
client = _VikingClient(
|
||||
"https://example.com",
|
||||
api_key="test-key",
|
||||
account="acct",
|
||||
user="usr",
|
||||
agent="hermes",
|
||||
)
|
||||
captured_headers = []
|
||||
|
||||
def capture_get(url, **kwargs):
|
||||
captured_headers.append(kwargs.get("headers") or {})
|
||||
if len(captured_headers) == 1:
|
||||
return SimpleNamespace(
|
||||
status_code=400,
|
||||
text="",
|
||||
json=lambda: {
|
||||
"status": "error",
|
||||
"error": {
|
||||
"code": "INVALID_ARGUMENT",
|
||||
"message": "Trusted mode requests must include X-OpenViking-Account.",
|
||||
},
|
||||
},
|
||||
raise_for_status=lambda: None,
|
||||
)
|
||||
return SimpleNamespace(
|
||||
status_code=200,
|
||||
text="",
|
||||
json=lambda: {"status": "ok", "result": {"ok": True}},
|
||||
raise_for_status=lambda: None,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(client._httpx, "get", capture_get)
|
||||
|
||||
assert client.get("/api/v1/system/status") == {
|
||||
"status": "ok",
|
||||
"result": {"ok": True},
|
||||
}
|
||||
assert "X-OpenViking-Account" not in captured_headers[0]
|
||||
assert "X-OpenViking-User" not in captured_headers[0]
|
||||
assert captured_headers[1]["X-OpenViking-Account"] == "acct"
|
||||
assert captured_headers[1]["X-OpenViking-User"] == "usr"
|
||||
|
||||
|
||||
def test_viking_client_health_sends_auth_headers(monkeypatch):
|
||||
|
|
@ -1590,6 +1667,10 @@ def test_viking_client_health_sends_auth_headers(monkeypatch):
|
|||
assert client.health() is True
|
||||
assert captured["url"] == "https://example.com/health"
|
||||
assert captured["headers"]["Authorization"] == "Bearer test-key"
|
||||
assert captured["headers"]["X-OpenViking-Actor-Peer"] == "hermes"
|
||||
assert "X-OpenViking-Agent" not in captured["headers"]
|
||||
assert "X-OpenViking-Account" not in captured["headers"]
|
||||
assert "X-OpenViking-User" not in captured["headers"]
|
||||
|
||||
|
||||
def test_viking_client_validate_auth_uses_authenticated_system_status(monkeypatch):
|
||||
|
|
@ -1620,8 +1701,9 @@ def test_viking_client_validate_auth_uses_authenticated_system_status(monkeypatc
|
|||
}
|
||||
assert captured["url"] == "https://example.com/api/v1/system/status"
|
||||
assert captured["headers"]["Authorization"] == "Bearer test-key"
|
||||
assert captured["headers"]["X-OpenViking-Account"] == "acct"
|
||||
assert captured["headers"]["X-OpenViking-User"] == "alice"
|
||||
assert captured["headers"]["X-OpenViking-Actor-Peer"] == "hermes"
|
||||
assert "X-OpenViking-Account" not in captured["headers"]
|
||||
assert "X-OpenViking-User" not in captured["headers"]
|
||||
|
||||
|
||||
def test_viking_client_validate_root_access_uses_admin_accounts(monkeypatch):
|
||||
|
|
@ -1650,10 +1732,9 @@ def test_viking_client_validate_root_access_uses_admin_accounts(monkeypatch):
|
|||
assert client.validate_root_access() == {"status": "ok", "result": []}
|
||||
assert captured["url"] == "https://example.com/api/v1/admin/accounts"
|
||||
assert captured["headers"]["Authorization"] == "Bearer root-key"
|
||||
# Empty account/user fall back to "default" and the tenant headers are
|
||||
# always sent — ROOT API keys require them (#22414/#21232 contract).
|
||||
assert captured["headers"]["X-OpenViking-Account"] == "default"
|
||||
assert captured["headers"]["X-OpenViking-User"] == "default"
|
||||
assert captured["headers"]["X-OpenViking-Actor-Peer"] == "hermes"
|
||||
assert "X-OpenViking-Account" not in captured["headers"]
|
||||
assert "X-OpenViking-User" not in captured["headers"]
|
||||
|
||||
|
||||
def test_validate_openviking_reachability_uses_health_only(monkeypatch):
|
||||
|
|
@ -2055,7 +2136,7 @@ def test_sync_turn_captures_session_id_before_worker_runs():
|
|||
assert captured_payloads == [{
|
||||
"messages": [
|
||||
{"role": "user", "parts": [{"type": "text", "text": "u"}]},
|
||||
{"role": "assistant", "parts": [{"type": "text", "text": "a"}]},
|
||||
{"role": "assistant", "parts": [{"type": "text", "text": "a"}], "peer_id": "hermes"},
|
||||
]
|
||||
}]
|
||||
|
||||
|
|
@ -2099,7 +2180,7 @@ def test_sync_turn_retries_batch_write_with_fresh_client():
|
|||
{
|
||||
"messages": [
|
||||
{"role": "user", "parts": [{"type": "text", "text": "u"}]},
|
||||
{"role": "assistant", "parts": [{"type": "text", "text": "a"}]},
|
||||
{"role": "assistant", "parts": [{"type": "text", "text": "a"}], "peer_id": "hermes"},
|
||||
]
|
||||
},
|
||||
)]
|
||||
|
|
@ -2453,7 +2534,7 @@ def test_on_memory_write_uses_content_write_independent_of_session_rotation():
|
|||
assert captured_payloads[0]["content"] == "remember this"
|
||||
assert captured_payloads[0]["mode"] == "create"
|
||||
assert captured_payloads[0]["uri"].startswith(
|
||||
"viking://user/usr/agent/hermes/memories/preferences/mem_"
|
||||
"viking://user/peers/hermes/memories/preferences/mem_"
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -299,6 +299,8 @@ hermes memory setup # select "openviking"
|
|||
# Or manually:
|
||||
hermes config set memory.provider openviking
|
||||
echo "OPENVIKING_ENDPOINT=http://localhost:1933" >> ~/.hermes/.env
|
||||
# Authenticated servers should use a user/admin API key:
|
||||
echo "OPENVIKING_API_KEY=..." >> ~/.hermes/.env
|
||||
```
|
||||
|
||||
**Key features:**
|
||||
|
|
@ -306,6 +308,9 @@ echo "OPENVIKING_ENDPOINT=http://localhost:1933" >> ~/.hermes/.env
|
|||
- Automatic memory extraction on session commit (profile, preferences, entities, events, cases, patterns)
|
||||
- `viking://` URI scheme for hierarchical knowledge browsing
|
||||
|
||||
`OPENVIKING_ACCOUNT` and `OPENVIKING_USER` are used for local/trusted mode.
|
||||
`OPENVIKING_AGENT` is Hermes' peer ID in OpenViking for peer-scoped memories.
|
||||
|
||||
---
|
||||
|
||||
### Mem0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue