diff --git a/plugins/memory/openviking/README.md b/plugins/memory/openviking/README.md index 0b6be37c0a7..17f658d350d 100644 --- a/plugins/memory/openviking/README.md +++ b/plugins/memory/openviking/README.md @@ -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 diff --git a/plugins/memory/openviking/__init__.py b/plugins/memory/openviking/__init__.py index 2e0df40a727..7ebe6869a46 100644 --- a/plugins/memory/openviking/__init__.py +++ b/plugins/memory/openviking/__init__.py @@ -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 diff --git a/scripts/release.py b/scripts/release.py index cc0aedf8dea..79ecf36382a 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -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", diff --git a/tests/openviking_plugin/test_openviking.py b/tests/openviking_plugin/test_openviking.py index c37a15c0cda..f10fc502000 100644 --- a/tests/openviking_plugin/test_openviking.py +++ b/tests/openviking_plugin/test_openviking.py @@ -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.""" diff --git a/tests/plugins/memory/test_openviking_provider.py b/tests/plugins/memory/test_openviking_provider.py index b751da36b1f..954385fa54e 100644 --- a/tests/plugins/memory/test_openviking_provider.py +++ b/tests/plugins/memory/test_openviking_provider.py @@ -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_" ) diff --git a/website/docs/user-guide/features/memory-providers.md b/website/docs/user-guide/features/memory-providers.md index 476bd46696d..e3054cf236a 100644 --- a/website/docs/user-guide/features/memory-providers.md +++ b/website/docs/user-guide/features/memory-providers.md @@ -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