{"repo":"NousResearch/hermes-agent","public_id":"48464","display_label":"OpenViking integration updates and end-user isolation","summary":"These members cover adapting the memory provider to the current OpenViking API and adding configurable end-user isolation using the peer_id feature introduced in OpenViking v0.4.1.","subsystem":"agent","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":48206,"merged_at":"2026-06-18T16:23:43+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-06-18T16:23:43+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":48464,"pr_number":48206,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-18T16:23:43+00:00"}],"members":[{"kind":"issue","number":48464,"title":"feat(openviking): add configurable end-user isolation via OpenViking v0.4.1 peer_id","url":null,"state":"closed"},{"kind":"pr","number":48206,"title":"fix(openviking): adapt memory provider for current api","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["plugins/memory/openviking/README.md","plugins/memory/openviking/__init__.py","scripts/release.py","tests/openviking_plugin/test_openviking.py","tests/plugins/memory/test_openviking_provider.py","website/docs/user-guide/features/memory-providers.md"],"fix_hunks":[{"file":"plugins/memory/openviking/README.md","hunks":["@@ -31,10 +31,14 @@ All config via environment variables in `.env`:\n | Env Var | Default | Description |\n |---------|---------|-------------|\n | `OPENVIKING_ENDPOINT` | `http://127.0.0.1:1933` | Server URL |\n-| `OPENVIKING_API_KEY` | (none) | API key (optional) |\n-| `OPENVIKING_ACCOUNT` | (none) | Tenant account override |\n-| `OPENVIKING_USER` | (none) | Tenant user override |\n-| `OPENVIKING_AGENT` | `hermes` | Tenant agent namespace |\n+| `OPENVIKING_API_KEY` | (none) | User/admin API key for authenticated servers |\n+| `OPENVIKING_ACCOUNT` | `default` | Tenant account for local/trusted mode |\n+| `OPENVIKING_USER` | `default` | Tenant user for local/trusted mode |\n+| `OPENVIKING_AGENT` | `hermes` | Hermes peer ID in OpenViking, used for peer-scoped memories |\n+\n+When `OPENVIKING_API_KEY` is set, Hermes lets OpenViking derive account/user\n+identity from the key. In local or trusted deployments without an API key,\n+Hermes sends `OPENVIKING_ACCOUNT` and `OPENVIKING_USER` as identity headers.\n \n ## Tools\n "]},{"file":"plugins/memory/openviking/__init__.py","hunks":["@@ -11,9 +11,9 @@\n or a linked OpenViking CLI config:\n   OPENVIKING_ENDPOINT  — Server URL (default: http://127.0.0.1:1933)\n   OPENVIKING_API_KEY   — API key (required for authenticated servers)\n-  OPENVIKING_ACCOUNT   — Optional tenant account override\n-  OPENVIKING_USER      — Optional tenant user override\n-  OPENVIKING_AGENT     — Tenant agent (default: hermes)\n+  OPENVIKING_ACCOUNT   — Tenant account for local/trusted mode (default: default)\n+  OPENVIKING_USER      — Tenant user for local/trusted mode (default: default)\n+  OPENVIKING_AGENT     — Hermes peer ID in OpenViking (default: hermes)\n \n Capabilities:\n   - Automatic memory extraction on session commit (6 categories)","@@ -55,6 +55,7 @@\n _DEFAULT_ENDPOINT = \"http://127.0.0.1:1933\"\n _OPENVIKING_SERVICE_ENDPOINT = \"https://api.vikingdb.cn-beijing.volces.com/openviking\"\n _DEFAULT_AGENT = \"hermes\"\n+_AGENT_PROMPT_LABEL = \"Hermes peer ID in OpenViking\"\n _OVCLI_CONFIG_ENV = \"OPENVIKING_CLI_CONFIG_FILE\"\n _OVCLI_DEFAULT_RELATIVE_PATH = \".openviking/ovcli.conf\"\n _OVCLI_SAVED_PREFIX = \"ovcli.conf.\"","@@ -200,10 +201,9 @@ def __init__(self, endpoint: str, api_key: str = \"\",\n                  agent: Optional[str] = None):\n         self._endpoint = endpoint.rstrip(\"/\")\n         self._api_key = [redacted:secret-assignment]\n-        # Empty account/user fall back to \"default\" and the tenant headers are\n-        # always sent — ROOT API keys require them (preserves the merged\n-        # contract from #22414/#21232; an empty string must NOT omit the\n-        # header). Use `or` (not `is not None`) so \"\" also falls back.\n+        # Account/user are local/trusted-mode tenant identity. API-key requests\n+        # omit these headers by default; trusted-mode retry may send them only\n+        # after OpenViking explicitly asks for asserted tenant identity.\n         self._account = account or os.environ.get(\"OPENVIKING_ACCOUNT\", \"default\")\n         self._user = user or os.environ.get(\"OPENVIKING_USER\", \"default\")\n         self._agent = agent if agent is not None else os.environ.get(\"OPENVIKING_AGENT\", _DEFAULT_AGENT)","@@ -211,15 +211,18 @@ def __init__(self, endpoint: str, api_key: str = \"\",\n         if self._httpx is None:\n             raise ImportError(\"httpx is required for OpenViking: pip install httpx\")\n \n-    def _headers(self) -> dict:\n+    def _headers(self, *, include_tenant: bool | None = None) -> dict:\n+        if include_tenant is None:\n+            include_tenant = not bool(self._api_key)\n+\n         h = {\"Content-Type\": \"application/json\"}\n         if self._agent:\n             h[\"X-OpenViking-Actor-Peer\"] = self._agent\n-            h[\"X-OpenViking-Agent\"] = self._agent\n-        if self._account:\n-            h[\"X-OpenViking-Account\"] = self._account\n-        if self._user:\n-            h[\"X-OpenViking-User\"] = self._user\n+        if include_tenant:\n+            if self._account:\n+                h[\"X-OpenViking-Account\"] = self._account\n+            if self._user:\n+                h[\"X-OpenViking-User\"] = self._user\n         if self._api_key:\n             h[\"X-API-Key\"] = self._api_key\n             h[\"Authorization\"] = \"Bearer \" + self._api_key","@@ -228,11 +231,33 @@ def _headers(self) -> dict:\n     def _url(self, path: str) -> str:\n         return f\"{self._endpoint}{path}\"\n \n-    def _multipart_headers(self) -> dict:\n-        headers = self._headers()\n+    def _multipart_headers(self, *, include_tenant: bool | None = None) -> dict:\n+        headers = self._headers(include_tenant=include_tenant)\n         headers.pop(\"Content-Type\", None)\n         return headers\n \n+    @staticmethod\n+    def _needs_trusted_identity_retry(exc: Exception) -> bool:\n+        message = str(exc)\n+        return (\n+            \"Trusted mode requests must include X-OpenViking-Account\" in message\n+            or \"Trusted mode requests must include X-OpenViking-User\" in message\n+            or \"Trusted mode requests must include X-OpenViking-Account or explicit account_id\" in message\n+        )\n+\n+    def _send_with_trusted_identity_retry(self, send, *, multipart: bool = False) -> dict:\n+        try:\n+            headers = self._multipart_headers() if multipart else self._headers()\n+            return self._parse_response(send(headers))\n+        except Exception as exc:\n+            if not self._api_key or not self._needs_trusted_identity_retry(exc):\n+                raise\n+            headers = (\n+                self._multipart_headers(include_tenant=True)\n+                if multipart else self._headers(include_tenant=True)\n+            )\n+            return self._parse_response(send(headers))\n+\n     def _parse_response(self, resp) -> dict:\n         try:\n             data = resp.json()","@@ -267,28 +292,33 @@ def _parse_response(self, resp) -> dict:\n         return data\n \n     def get(self, path: str, **kwargs) -> dict:\n-        resp = self._httpx.get(\n-            self._url(path), headers=self._headers(), timeout=_TIMEOUT, **kwargs\n+        return self._send_with_trusted_identity_retry(\n+            lambda headers: self._httpx.get(\n+                self._url(path), headers=headers, timeout=_TIMEOUT, **kwargs\n+            )\n         )\n-        return self._parse_response(resp)\n \n     def post(self, path: str, payload: dict = None, **kwargs) -> dict:\n-        resp = self._httpx.post(\n-            self._url(path), json=payload or {}, headers=self._headers(),\n-            timeout=_TIMEOUT, **kwargs\n+        return self._send_with_trusted_identity_retry(\n+            lambda headers: self._httpx.post(\n+                self._url(path), json=payload or {}, headers=headers,\n+                timeout=_TIMEOUT, **kwargs\n+            )\n         )\n-        return self._parse_response(resp)\n \n     def upload_temp_file(self, file_path: Path) -> str:\n         mime_type = mimetypes.guess_type(file_path.name)[0] or \"application/octet-stream\"\n-        with file_path.open(\"rb\") as f:\n-            resp = self._httpx.post(\n-                self._url(\"/api/v1/resources/temp_upload\"),\n-                files={\"file\": (file_path.name, f, mime_type)},\n-                headers=self._multipart_headers(),\n-                timeout=_TIMEOUT,\n-            )\n-        data = self._parse_response(resp)\n+\n+        def _send(headers):\n+            with file_path.open(\"rb\") as f:\n+                return self._httpx.post(\n+                    self._url(\"/api/v1/resources/temp_upload\"),\n+                    files={\"file\": (file_path.name, f, mime_type)},\n+                    headers=headers,\n+                    timeout=_TIMEOUT,\n+                )\n+\n+        data = self._send_with_trusted_identity_retry(_send, multipart=True)\n         result = data.get(\"result\", {})\n         temp_file_id = result.get(\"temp_file_id\", \"\")\n         if not temp_file_id:","@@ -1219,7 +1249,7 @@ def _prompt_manual_connection_values(prompt, select, cancelled, *, service: bool\n                 return _SETUP_CANCELLED\n             if credential_choice == 0:\n                 values[\"agent\"] = _clean_config_value(\n-                    prompt(\"OpenViking agent\", default=_DEFAULT_AGENT)\n+                    prompt(_AGENT_PROMPT_LABEL, default=_DEFAULT_AGENT)\n                 ) or _DEFAULT_AGENT\n                 _print_validation_progress(\"Validating OpenViking local dev access...\")\n                 valid, message, _role = _validate_openviking_setup_values(values)","@@ -1339,7 +1369,7 @@ def _prompt_manual_connection_values(prompt, select, cancelled, *, service: bool\n             prefilled_agent = \"\"\n         else:\n             values[\"agent\"] = _clean_config_value(\n-                prompt(\"OpenViking agent\", default=_DEFAULT_AGENT)\n+                prompt(_AGENT_PROMPT_LABEL, default=_DEFAULT_AGENT)\n             ) or _DEFAULT_AGENT\n         _print_validation_progress(\"Validating OpenViking API access...\")\n         valid, message, role = _validate_openviking_setup_values(","@@ -1697,7 +1727,10 @@ def get_config_schema(self):\n             },\n             {\n                 \"key\": \"agent\",\n-                \"description\": \"OpenViking agent ID within the account ([hermes], useful in multi-agent mode)\",\n+                \"description\": (\n+                    \"Hermes peer ID in OpenViking, sent as the actor peer and \"\n+                    \"used for peer-scoped memories\"\n+                ),\n                 \"default\": \"hermes\",\n                 \"env_var\": \"OPENVIKING_AGENT\",\n             },","@@ -2129,18 +2162,22 @@ def _new_client(self) -> _VikingClient:\n     def _text_part(content: str) -> Dict[str, str]:\n         return {\"type\": \"text\", \"text\": content}\n \n-    @classmethod\n-    def _turn_batch_payload(cls, user_content: str, assistant_content: str) -> Dict[str, Any]:\n+    def _turn_batch_payload(self, user_content: str, assistant_content: str) -> Dict[str, Any]:\n+        assistant_message: Dict[str, Any] = {\n+            \"role\": \"assistant\",\n+            \"parts\": [self._text_part(assistant_content)],\n+        }\n+        if self._agent:\n+            assistant_message[\"peer_id\"] = self._agent\n         return {\n             \"messages\": [\n-                {\"role\": \"user\", \"parts\": [cls._text_part(user_content)]},\n-                {\"role\": \"assistant\", \"parts\": [cls._text_part(assistant_content)]},\n+                {\"role\": \"user\", \"parts\": [self._text_part(user_content)]},\n+                assistant_message,\n             ]\n         }\n \n-    @classmethod\n     def _post_session_turn(\n-        cls,\n+        self,\n         client: _VikingClient,\n         sid: str,\n         user_content: str,","@@ -2148,7 +2185,7 @@ def _post_session_turn(\n     ) -> None:\n         client.post(\n             f\"/api/v1/sessions/{sid}/messages/batch\",\n-            cls._turn_batch_payload(user_content, assistant_content),\n+            self._turn_batch_payload(user_content, assistant_content),\n         )\n \n     def _session_has_pending_tokens(self, sid: str) -> bool:","@@ -2402,9 +2439,9 @@ def on_session_switch(\n         )\n \n     def _build_memory_uri(self, subdir: str) -> str:\n-        \"\"\"Build a viking:// memory URI under the configured user/agent/subdir.\"\"\"\n+        \"\"\"Build a viking:// memory URI under the configured peer namespace.\"\"\"\n         slug = uuid.uuid4().hex[:12]\n-        return f\"viking://user/{self._user}/agent/{self._agent}/memories/{subdir}/mem_{slug}.md\"\n+        return f\"viking://user/peers/{self._agent}/memories/{subdir}/mem_{slug}.md\"\n \n     def on_memory_write(\n         self,","@@ -2535,14 +2572,16 @@ def _tool_search(self, args: dict) -> str:\n \n         payload: Dict[str, Any] = {\"query\": query}\n         mode = args.get(\"mode\", \"auto\")\n-        if mode != \"auto\":\n-            payload[\"mode\"] = mode\n         if args.get(\"scope\"):\n             payload[\"target_uri\"] = args[\"scope\"]\n         if args.get(\"limit\"):\n             payload[\"limit\"] = args[\"limit\"]\n \n-        resp = self._client.post(\"/api/v1/search/find\", payload)\n+        endpoint = \"/api/v1/search/search\" if mode == \"deep\" else \"/api/v1/search/find\"\n+        if endpoint == \"/api/v1/search/search\" and self._session_id:\n+            payload[\"session_id\"] = self._session_id\n+\n+        resp = self._client.post(endpoint, payload)\n         result = resp.get(\"result\", {})\n \n         # Format results for the model — keep it concise"]},{"file":"scripts/release.py","hunks":["@@ -91,6 +91,7 @@\n     \"[redacted:email]\": \"randomsnowflake\",\n     \"[redacted:email]\": \"zakame\",\n     \"152110621+jiangkoumo@users.noreply.github.com\": \"jiangkoumo\",\n+    \"[redacted:email]\": \"qin-ctx\",\n     \"[redacted:email]\": \"ViewWay\",\n     \"[redacted:email]\": \"m4dni5\",\n     \"[redacted:email]\": \"harjothkhara\","]},{"file":"tests/openviking_plugin/test_openviking.py","hunks":["@@ -239,6 +239,7 @@ def test_sync_turn_stores_only_slash_skill_user_instruction(self, monkeypatch):\n                         {\n                             \"role\": \"assistant\",\n                             \"parts\": [{\"type\": \"text\", \"text\": \"Done.\"}],\n+                            \"peer_id\": \"hermes\",\n                         },\n                     ]\n                 },","@@ -474,8 +475,8 @@ def test_list_browse_unwraps_and_normalizes_entry_shapes(self):\n class TestOpenVikingMemoryUriBuilder:\n     \"\"\"Regression tests for _build_memory_uri — fixes #36969.\n \n-    Before the fix the URI omitted /agent/{agent}/, causing all agents\n-    under the same user to share the same memory namespace.\n+    OpenViking's current memory layout stores peer-scoped memories under\n+    viking://user/peers/{peer_id}/...\n     \"\"\"\n \n     def _make_provider(self, user=\"alice\", agent=\"coder\"):","@@ -484,19 +485,19 @@ def _make_provider(self, user=\"alice\", agent=\"coder\"):\n         p._agent = agent\n         return p\n \n-    def test_uri_layout_includes_agent_segment(self):\n-        \"\"\"URI must contain /agent/{agent}/ between user and memories.\"\"\"\n+    def test_uri_layout_includes_peer_segment(self):\n+        \"\"\"URI must contain /peers/{peer_id}/ between user and memories.\"\"\"\n         p = self._make_provider(user=\"alice\", agent=\"coder\")\n         uri = p._build_memory_uri(\"preferences\")\n-        assert uri.startswith(\"viking://user/alice/agent/coder/memories/preferences/mem_\")\n+        assert uri.startswith(\"viking://user/peers/coder/memories/preferences/mem_\")\n         assert uri.endswith(\".md\")\n \n-    def test_uri_uses_configured_agent_not_default(self):\n-        \"\"\"_agent value must be interpolated — not hardcoded to 'hermes'.\"\"\"\n+    def test_uri_uses_configured_peer_not_default(self):\n+        \"\"\"_agent value is the OpenViking actor peer ID, not hardcoded to 'hermes'.\"\"\"\n         p = self._make_provider(user=\"alice\", agent=\"research-bot\")\n         uri = p._build_memory_uri(\"entities\")\n-        assert \"/agent/research-bot/\" in uri\n-        assert \"/agent/hermes/\" not in uri\n+        assert \"/peers/research-bot/\" in uri\n+        assert \"/peers/hermes/\" not in uri\n \n     def test_uri_slug_is_twelve_hex_chars_and_unique(self):\n         \"\"\"Slug must be 12 hex chars and differ between calls.\"\"\""]},{"file":"tests/plugins/memory/test_openviking_provider.py","hunks":["@@ -369,7 +369,7 @@ def test_post_setup_create_remote_user_profile_can_mirror_to_openviking_store(tm\n         _prompt_from_values({\n             \"OpenViking server URL\": \"https://openviking.example\",\n             \"OpenViking user API key\": \"[redacted:secret-assignment]\",\n-            \"OpenViking agent\": \"hermes\",\n+            \"Hermes peer ID in OpenViking\": \"hermes\",\n             \"OpenViking profile name\": \"VPS\",\n         }),\n     )","@@ -411,7 +411,7 @@ def test_post_setup_create_remote_user_can_keep_hermes_only(tmp_path, monkeypatc\n         _prompt_from_values({\n             \"OpenViking server URL\": \"https://openviking.example\",\n             \"OpenViking user API key\": \"[redacted:secret-assignment]\",\n-            \"OpenViking agent\": \"agent\",\n+            \"Hermes peer ID in OpenViking\": \"agent\",\n         }),\n     )\n     config = {\"memory\": {}}","@@ -455,7 +455,7 @@ def validate_values(values, *, require_api_key=[redacted:secret-assignment]\n         _prompt_from_values(\n             {\n                 \"OpenViking API key\": \"[redacted:secret-assignment]\",\n-                \"OpenViking agent\": \"agent\",\n+                \"Hermes peer ID in OpenViking\": \"agent\",\n             },\n             forbidden={\"OpenViking server URL\", \"OpenViking user API key\", \"OpenViking root API key\"},\n         ),","@@ -540,7 +540,7 @@ def fake_prompt(label, default=None, secret=[redacted:secret-assignment]\n             \"OpenViking user API key\": \"[redacted:secret-assignment]\",\n             \"OpenViking account\": \"acct\",\n             \"OpenViking user\": \"alice\",\n-            \"OpenViking agent\": \"agent\",\n+            \"Hermes peer ID in OpenViking\": \"agent\",\n         }\n         return values.get(label, default or \"\")\n ","@@ -549,7 +549,7 @@ def fake_prompt(label, default=None, secret=[redacted:secret-assignment]\n \n     OpenVikingMemoryProvider().post_setup(str(hermes_home), config)\n \n-    assert prompt_events.count(\"OpenViking agent\") == 1\n+    assert prompt_events.count(\"Hermes peer ID in OpenViking\") == 1\n     env_text = (hermes_home / \".env\").read_text(encoding=\"utf-8\")\n     assert \"OPENVIKING_API_KEY=[redacted:secret-assignment]\" in env_text\n     assert \"OPENVIKING_ACCOUNT=acct\" in env_text","@@ -580,7 +580,7 @@ def validate_values(values, *, require_api_key=[redacted:secret-assignment]\n             {\n                 \"OpenViking server URL\": \"https://openviking.example\",\n                 \"OpenViking root API key\": \"[redacted:secret-assignment]\",\n-                \"OpenViking agent\": \"agent\",\n+                \"Hermes peer ID in OpenViking\": \"agent\",\n             },\n             forbidden={\"OpenViking user API key\", \"OpenViking account\", \"OpenViking user\"},\n         ),","@@ -616,7 +616,7 @@ def validate_values(values, *, require_api_key=[redacted:secret-assignment]\n             \"OpenViking root API key\": \"[redacted:secret-assignment]\",\n             \"OpenViking account\": \"acct\",\n             \"OpenViking user\": \"alice\",\n-            \"OpenViking agent\": \"agent\",\n+            \"Hermes peer ID in OpenViking\": \"agent\",\n         }),\n         lambda *args, **kwargs: next(choices),\n         -1,","@@ -1091,7 +1091,7 @@ def validate_reachability(endpoint):\n         \"_prompt\",\n         _prompt_from_values({\n             \"OpenViking server URL\": \"localhost\",\n-            \"OpenViking agent\": \"agent\",\n+            \"Hermes peer ID in OpenViking\": \"agent\",\n         }),\n     )\n     config = {\"memory\": {}}","@@ -1126,7 +1126,7 @@ def test_post_setup_invalid_env_profile_can_create_new_config(tmp_path, monkeypa\n         _prompt_from_values({\n             \"OpenViking server URL\": \"https://openviking.example\",\n             \"OpenViking user API key\": \"[redacted:secret-assignment]\",\n-            \"OpenViking agent\": \"agent\",\n+            \"Hermes peer ID in OpenViking\": \"agent\",\n         }),\n     )\n     config = {\"memory\": {}}","@@ -1210,6 +1210,36 @@ def test_tool_search_sends_limit_not_legacy_top_k():\n     assert \"top_k\" not in payload\n \n \n+def test_tool_search_uses_find_for_normal_search():\n+    provider = OpenVikingMemoryProvider()\n+    provider._client = MagicMock()\n+    provider._client.post.return_value = {\n+        \"result\": {\"memories\": [], \"resources\": [], \"skills\": [], \"total\": 0}\n+    }\n+\n+    provider._tool_search({\"query\": \"simple lookup\", \"mode\": \"fast\"})\n+\n+    provider._client.post.assert_called_once_with(\"/api/v1/search/find\", {\n+        \"query\": \"simple lookup\",\n+    })\n+\n+\n+def test_tool_search_uses_session_search_for_deep_search():\n+    provider = OpenVikingMemoryProvider()\n+    provider._client = MagicMock()\n+    provider._session_id = \"session-123\"\n+    provider._client.post.return_value = {\n+        \"result\": {\"memories\": [], \"resources\": [], \"skills\": [], \"total\": 0}\n+    }\n+\n+    provider._tool_search({\"query\": \"connect facts\", \"mode\": \"deep\"})\n+\n+    provider._client.post.assert_called_once_with(\"/api/v1/search/search\", {\n+        \"query\": \"connect facts\",\n+        \"session_id\": \"session-123\",\n+    })\n+\n+\n def test_tool_add_resource_uploads_existing_local_file(tmp_path):\n     sample = tmp_path / \"sample.md\"\n     sample.write_text(\"# Local resource\\n\", encoding=\"utf-8\")","@@ -1457,10 +1487,10 @@ def capture_httpx_post(url, **kwargs):\n     assert \"files\" in captured_kwargs\n     assert \"json\" not in captured_kwargs\n     headers = captured_kwargs[\"headers\"]\n-    assert headers[\"X-OpenViking-Account\"] == \"test-account\"\n-    assert headers[\"X-OpenViking-User\"] == \"test-user\"\n+    assert \"X-OpenViking-Account\" not in headers\n+    assert \"X-OpenViking-User\" not in headers\n     assert headers[\"X-OpenViking-Actor-Peer\"] == \"test-agent\"\n-    assert headers[\"X-OpenViking-Agent\"] == \"test-agent\"\n+    assert \"X-OpenViking-Agent\" not in headers\n     assert headers[\"X-API-Key\"] == \"test-key\"\n     assert \"Content-Type\" not in headers\n ","@@ -1517,16 +1547,17 @@ def test_viking_client_headers_include_bearer_when_api_key_set():\n     headers = client._headers()\n     assert headers[\"X-API-Key\"] == \"test-key\"\n     assert headers[\"Authorization\"] == \"Bearer test-key\"\n+    assert headers[\"X-OpenViking-Actor-Peer\"] == \"hermes\"\n+    assert \"X-OpenViking-Agent\" not in headers\n+    assert \"X-OpenViking-Account\" not in headers\n+    assert \"X-OpenViking-User\" not in headers\n \n \n-def test_viking_client_headers_send_tenant_when_default():\n-    # account/user set to the literal string \"default\". OpenViking 0.3.x\n-    # requires X-OpenViking-Account and X-OpenViking-User for ROOT API key\n-    # requests to tenant-scoped APIs — omitting them causes\n-    # INVALID_ARGUMENT errors even when account=\"default\".\n+def test_viking_client_headers_send_tenant_in_local_mode():\n+    # Local/trusted mode needs explicit tenant identity headers.\n     client = _VikingClient(\n         \"https://example.com\",\n-        api_key=\"[redacted:secret-assignment]\",\n+        api_key=\"\",\n         account=\"default\",\n         user=\"default\",\n         agent=\"hermes\",","@@ -1535,14 +1566,13 @@ def test_viking_client_headers_send_tenant_when_default():\n     assert headers[\"X-OpenViking-Account\"] == \"default\"\n     assert headers[\"X-OpenViking-User\"] == \"default\"\n     assert headers[\"X-OpenViking-Actor-Peer\"] == \"hermes\"\n-    assert headers[\"X-OpenViking-Agent\"] == \"hermes\"\n-    assert headers[\"Authorization\"] == \"Bearer test-key\"\n+    assert \"X-OpenViking-Agent\" not in headers\n+    assert \"Authorization\" not in headers\n \n \n def test_viking_client_headers_send_tenant_when_empty_falls_back_to_default(monkeypatch):\n     _clear_openviking_tenant_env(monkeypatch)\n-    # Empty account/user strings fall back to \"default\" via the constructor.\n-    # Headers are sent even for the default value — ROOT API keys need them.\n+    # Empty account/user strings fall back to \"default\" in local mode.\n     client = _VikingClient(\n         \"https://example.com\",\n         api_key=\"\",","@@ -1553,11 +1583,13 @@ def test_viking_client_headers_send_tenant_when_empty_falls_back_to_default(monk\n     headers = client._headers()\n     assert headers[\"X-OpenViking-Account\"] == \"default\"\n     assert headers[\"X-OpenViking-User\"] == \"default\"\n+    assert headers[\"X-OpenViking-Actor-Peer\"] == \"hermes\"\n+    assert \"X-OpenViking-Agent\" not in headers\n     assert \"Authorization\" not in headers\n     assert \"X-API-Key\" not in headers\n \n \n-def test_viking_client_headers_sent_with_real_tenant_values():\n+def test_viking_client_headers_can_include_tenant_for_trusted_retry():\n     client = _VikingClient(\n         \"https://example.com\",\n         api_key=\"[redacted:secret-assignment]\",","@@ -1565,9 +1597,54 @@ def test_viking_client_headers_sent_with_real_tenant_values():\n         user=\"real-user\",\n         agent=\"hermes\",\n     )\n-    headers = client._headers()\n+    headers = client._headers(include_tenant=True)\n     assert headers[\"X-OpenViking-Account\"] == \"real-account\"\n     assert headers[\"X-OpenViking-User\"] == \"real-user\"\n+    assert headers[\"Authorization\"] == \"Bearer test-key\"\n+\n+\n+def test_viking_client_retries_with_tenant_headers_for_trusted_mode(monkeypatch):\n+    client = _VikingClient(\n+        \"https://example.com\",\n+        api_key=\"[redacted:secret-assignment]\",\n+        account=\"acct\",\n+        user=\"usr\",\n+        agent=\"hermes\",\n+    )\n+    captured_headers = []\n+\n+    def capture_get(url, **kwargs):\n+        captured_headers.append(kwargs.get(\"headers\") or {})\n+        if len(captured_headers) == 1:\n+            return SimpleNamespace(\n+                status_code=400,\n+                text=\"\",\n+                json=lambda: {\n+                    \"status\": \"error\",\n+                    \"error\": {\n+                        \"code\": \"INVALID_ARGUMENT\",\n+                        \"message\": \"Trusted mode requests must include X-OpenViking-Account.\",\n+                    },\n+                },\n+                raise_for_status=lambda: None,\n+            )\n+        return SimpleNamespace(\n+            status_code=200,\n+            text=\"\",\n+            json=lambda: {\"status\": \"ok\", \"result\": {\"ok\": True}},\n+            raise_for_status=lambda: None,\n+        )\n+\n+    monkeypatch.setattr(client._httpx, \"get\", capture_get)\n+\n+    assert client.get(\"/api/v1/system/status\") == {\n+        \"status\": \"ok\",\n+        \"result\": {\"ok\": True},\n+    }\n+    assert \"X-OpenViking-Account\" not in captured_headers[0]\n+    assert \"X-OpenViking-User\" not in captured_headers[0]\n+    assert captured_headers[1][\"X-OpenViking-Account\"] == \"acct\"\n+    assert captured_headers[1][\"X-OpenViking-User\"] == \"usr\"\n \n \n def test_viking_client_health_sends_auth_headers(monkeypatch):","@@ -1590,6 +1667,10 @@ def capture_get(url, **kwargs):\n     assert client.health() is True\n     assert captured[\"url\"] == \"https://example.com/health\"\n     assert captured[\"headers\"][\"Authorization\"] == \"Bearer test-key\"\n+    assert captured[\"headers\"][\"X-OpenViking-Actor-Peer\"] == \"hermes\"\n+    assert \"X-OpenViking-Agent\" not in captured[\"headers\"]\n+    assert \"X-OpenViking-Account\" not in captured[\"headers\"]\n+    assert \"X-OpenViking-User\" not in captured[\"headers\"]\n \n \n def test_viking_client_validate_auth_uses_authenticated_system_status(monkeypatch):","@@ -1620,8 +1701,9 @@ def capture_get(url, **kwargs):\n     }\n     assert captured[\"url\"] == \"https://example.com/api/v1/system/status\"\n     assert captured[\"headers\"][\"Authorization\"] == \"Bearer test-key\"\n-    assert captured[\"headers\"][\"X-OpenViking-Account\"] == \"acct\"\n-    assert captured[\"headers\"][\"X-OpenViking-User\"] == \"alice\"\n+    assert captured[\"headers\"][\"X-OpenViking-Actor-Peer\"] == \"hermes\"\n+    assert \"X-OpenViking-Account\" not in captured[\"headers\"]\n+    assert \"X-OpenViking-User\" not in captured[\"headers\"]\n \n \n def test_viking_client_validate_root_access_uses_admin_accounts(monkeypatch):","@@ -1650,10 +1732,9 @@ def capture_get(url, **kwargs):\n     assert client.validate_root_access() == {\"status\": \"ok\", \"result\": []}\n     assert captured[\"url\"] == \"https://example.com/api/v1/admin/accounts\"\n     assert captured[\"headers\"][\"Authorization\"] == \"Bearer root-key\"\n-    # Empty account/user fall back to \"default\" and the tenant headers are\n-    # always sent — ROOT API keys require them (#22414/#21232 contract).\n-    assert captured[\"headers\"][\"X-OpenViking-Account\"] == \"default\"\n-    assert captured[\"headers\"][\"X-OpenViking-User\"] == \"default\"\n+    assert captured[\"headers\"][\"X-OpenViking-Actor-Peer\"] == \"hermes\"\n+    assert \"X-OpenViking-Account\" not in captured[\"headers\"]\n+    assert \"X-OpenViking-User\" not in captured[\"headers\"]\n \n \n def test_validate_openviking_reachability_uses_health_only(monkeypatch):","@@ -2055,7 +2136,7 @@ def post(self, path, payload=None, **kwargs):\n     assert captured_payloads == [{\n         \"messages\": [\n             {\"role\": \"user\", \"parts\": [{\"type\": \"text\", \"text\": \"u\"}]},\n-            {\"role\": \"assistant\", \"parts\": [{\"type\": \"text\", \"text\": \"a\"}]},\n+            {\"role\": \"assistant\", \"parts\": [{\"type\": \"text\", \"text\": \"a\"}], \"peer_id\": \"hermes\"},\n         ]\n     }]\n ","@@ -2099,7 +2180,7 @@ def post(self, path, payload=None, **kwargs):\n         {\n             \"messages\": [\n                 {\"role\": \"user\", \"parts\": [{\"type\": \"text\", \"text\": \"u\"}]},\n-                {\"role\": \"assistant\", \"parts\": [{\"type\": \"text\", \"text\": \"a\"}]},\n+                {\"role\": \"assistant\", \"parts\": [{\"type\": \"text\", \"text\": \"a\"}], \"peer_id\": \"hermes\"},\n             ]\n         },\n     )]","@@ -2453,7 +2534,7 @@ def post(self, path, payload=None, **kwargs):\n     assert captured_payloads[0][\"content\"] == \"remember this\"\n     assert captured_payloads[0][\"mode\"] == \"create\"\n     assert captured_payloads[0][\"uri\"].startswith(\n-        \"viking://user/usr/agent/hermes/memories/preferences/mem_\"\n+        \"viking://user/peers/hermes/memories/preferences/mem_\"\n     )\n \n "]},{"file":"website/docs/user-guide/features/memory-providers.md","hunks":["@@ -299,6 +299,8 @@ hermes memory setup    # select \"openviking\"\n # Or manually:\n hermes config set memory.provider openviking\n echo \"OPENVIKING_ENDPOINT=http://localhost:1933\" >> ~/.hermes/.env\n+# Authenticated servers should use a user/admin API key:\n+echo \"OPENVIKING_API_KEY=...\" >> ~/.hermes/.env\n ```\n \n **Key features:**","@@ -306,6 +308,9 @@ echo \"OPENVIKING_ENDPOINT=http://localhost:1933\" >> ~/.hermes/.env\n - Automatic memory extraction on session commit (profile, preferences, entities, events, cases, patterns)\n - `viking://` URI scheme for hierarchical knowledge browsing\n \n+`OPENVIKING_ACCOUNT` and `OPENVIKING_USER` are used for local/trusted mode.\n+`OPENVIKING_AGENT` is Hermes' peer ID in OpenViking for peer-scoped memories.\n+\n ---\n \n ### Mem0\n"]}]},{"kind":"pr","number":48465,"title":"feat(openviking): add configurable end-user isolation via OpenViking v0.4.1 peer_id","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#48206","number":48206}],"category":"superseded_by_pr","implication":"superseded"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}