OpenViking plugin auth, headers, and API fixes

NousResearch/hermes-agent · cluster 4740 · agent

Data as of

Nothing open on GitHub. Fix merged in #4936. Check it covers your case.

The short answer

0open issues
0open PRs
19items grouped
Known?
Yes. 19 related items grouped here, 8 issues already closed.
Fix event
A fix was merged to main in PR #4936. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #4936. That is separate from the fix event.
Still open
Nothing open on GitHub.

These issues and PRs address multiple interrelated problems in the OpenViking plugin: incorrect API endpoint paths, broken authentication header behavior with API key auth, tenant-scoping header mismatches causing memory search failures, and compatibility issues with OpenViking v0.4.1+.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #4936 fix(openviking): send tenant-scoping headers for multi-tenant servers (salvage #4863)

    merged to main

    files: plugins/memory/openviking/__init__.py

    Show the diff
    • plugins/memory/openviking/__init__.py
      @@ -10,6 +10,8 @@
       Config via environment variables (profile-scoped via each profile's .env):
         OPENVIKING_ENDPOINT  — Server URL (default: http://127.0.0.1:1933)
         OPENVIKING_API_KEY   — API key (required for authenticated servers)
      +  OPENVIKING_ACCOUNT   — Tenant account (default: root)
      +  OPENVIKING_USER      — Tenant user (default: default)
       
       Capabilities:
         - Automatic memory extraction on session commit (6 categories)
      @@ -51,15 +53,22 @@ def _get_httpx():
       class _VikingClient:
           """Thin HTTP client for the OpenViking REST API."""
       
      -    def __init__(self, endpoint: str, api_key: str = ""):
      +    def __init__(self, endpoint: str, api_key: str = "",
      +                 account: str = "", user: str = ""):
               self._endpoint = endpoint.rstrip("/")
               self._api_key = [redacted:secret-assignment]
      +        self._account = account or os.environ.get("OPENVIKING_ACCOUNT", "root")
      +        self._user = user or os.environ.get("OPENVIKING_USER", "default")
               self._httpx = _get_httpx()
               if self._httpx is None:
                   raise ImportError("httpx is required for OpenViking: pip install httpx")
       
           def _headers(self) -> dict:
      -        h = {"Content-Type": "application/json"}
      +        h = {
      +            "Content-Type": "application/json",
      +            "X-OpenViking-Account": self._account,
      +            "X-OpenViking-User": self._user,
      +        }
               if self._api_key:
                   h["X-API-Key"] = self._api_key
               return h
  • #5078 fix(openviking): correct API endpoint paths and response parsing

    merged to main

    files: plugins/memory/openviking/__init__.py

  • #10463 fix(openviking): correct account default + commit session on /new and compress

    merged to main

    files: cli.py, plugins/memory/openviking/__init__.py, run_agent.py, scripts/release.py, tests/agent/test_memory_provider.py

    Show the diff
    • cli.py
      @@ -4100,6 +4100,8 @@ def new_session(self, silent=False):
                       self.agent.flush_memories(self.conversation_history)
                   except (Exception, KeyboardInterrupt):
                       pass
      +            # Trigger memory extraction on the old session before session_id rotates.
      +            self.agent.commit_memory_session(self.conversation_history)
                   self._notify_session_boundary("on_session_finalize")
               elif self.agent:
                   # First session or empty history — still finalize the old session
    • plugins/memory/openviking/__init__.py
      @@ -10,8 +10,9 @@
       Config via environment variables (profile-scoped via each profile's .env):
         OPENVIKING_ENDPOINT  — Server URL (default: http://127.0.0.1:1933)
         OPENVIKING_API_KEY   — API key (required for authenticated servers)
      -  OPENVIKING_ACCOUNT   — Tenant account (default: root)
      +  OPENVIKING_ACCOUNT   — Tenant account (default: default)
         OPENVIKING_USER      — Tenant user (default: default)
      +  OPENVIKING_AGENT   — Tenant agent (default: hermes)
       
       Capabilities:
         - Automatic memory extraction on session commit (6 categories)
      @@ -80,11 +81,12 @@ class _VikingClient:
           """Thin HTTP client for the OpenViking REST API."""
       
           def __init__(self, endpoint: str, api_key: str = "",
      -                 account: str = "", user: str = ""):
      +                 account: str = "", user: str = "", agent: str = ""):
               self._endpoint = endpoint.rstrip("/")
               self._api_key = [redacted:secret-assignment]
      -        self._account = account or os.environ.get("OPENVIKING_ACCOUNT", "root")
      +        self._account = account or os.environ.get("OPENVIKING_ACCOUNT", "default")
               self._user = user or os.environ.get("OPENVIKING_USER", "default")
      +        self._agent = agent or os.environ.get("OPENVIKING_AGENT", "hermes")
               self._httpx = _get_httpx()
               if self._httpx is None:
                   raise ImportError("httpx is required for OpenViking: pip install httpx")
      @@ -94,6 +96,7 @@ def _headers(self) -> dict:
                   "Content-Type": "application/json",
                   "X-OpenViking-Account": self._account,
                   "X-OpenViking-User": self._user,
      +            "X-OpenViking-Agent": self._agent,
               }
               if self._api_key:
                   h["X-API-Key"] = self._api_key
      @@ -282,20 +285,44 @@ def get_config_schema(self):
                   },
                   {
                       "key": "api_key",
      -                "description": "OpenViking API key",
      +                "description": "OpenViking API key (leave blank for local dev mode)",
                       "secret": True,
                       "env_var": "OPENVIKING_API_KEY",
                   },
      +            {
      +                "key": "account",
      +                "description": "OpenViking tenant account ID ([default], used when local mode, OPENVIKING_API_KEY is empty)",
      +                "default": "default",
      +                "env_var": "OPENVIKING_ACCOUNT",
      +            },
      +            {
      +                "key": "user",
      +                "description": "OpenViking user ID within the account ([default], used when local mode, OPENVIKING_API_KEY is empty)",
      +                "default": "default",
      +                "env_var": "OPENVIKING_USER",
      +            },
      +            {
      +                "key": "agent",
      +                "description": "OpenViking agent ID within the account ([hermes], useful in multi-agent mode)",
      +                "default": "hermes",
      +                "env_var": "OPENVIKING_AGENT",
      +            },
               ]
       
           def initialize(self, session_id: str, **kwargs) -> None:
               self._endpoint = os.environ.get("OPENVIKING_ENDPOINT", _DEFAULT_ENDPOINT)
               self._api_key = [redacted:secret-assignment]"OPENVIKING_API_KEY", "")
      +        self._account = os.environ.get("OPENVIKING_ACCOUNT", "default")
      +        self._user = os.environ.get("OPENVIKING_USER", "default")
      +        self._agent = os.environ.get("OPENVIKING_AGENT", "hermes")
               self._session_id = session_id
               self._turn_count = 0
       
               try:
      -            self._client = _VikingClient(self._endpoint, self._api_key)
      +            self._client = _VikingClient(
      +                self._endpoint, self._api_key,
      +                account=self._account, user=self._user, agent=self._agent,
      +            )
                   if not self._client.health():
                       logger.warning("OpenViking server at %s is not reachable", self._endpoint)
                       self._client = None
      @@ -325,7 +352,8 @@ def system_prompt_block(self) -> str:
                       "(abstract/overview/full), viking_browse to explore.\n"
                       "Use viking_remember to store facts, viking_add_resource to index URLs/docs."
                   )
      -        except Exception:
      +        except Exception as e:
      +            logger.warning("OpenViking system_prompt_block failed: %s", e)
                   return (
                       "# OpenViking Knowledge Base\n"
                       f"Active. Endpoint: {self._endpoint}\n"
      @@ -351,7 +379,10 @@ def queue_prefetch(self, query: str, *, session_id: str = "") -> None:
       
               def _run():
                   try:
      -                client = _VikingClient(self._endpoint, self._api_key)
      +                client = _VikingClient(
      +                    self._endpoint, self._api_key,
      +                    account=self._account, user=self._user, agent=self._agent,
      +                )
                       resp = client.post("/api/v1/search/find", {
                           "query": query,
                           "top_k": 5,
      @@ -386,7 +417,10 @@ def sync_turn(self, user_content: str, assistant_content: str, *, session_id: st
       
               def _sync():
                   try:
      -                client = _VikingClient(self._endpoint, self._api_key)
      +                client = _VikingClient(
      +                    self._endpoint, self._api_key,
      +                    account=self._account, user=self._user, agent=self._agent,
      +                )
                       sid = self._session_id
       
                       # Add user message
      @@ -442,7 +476,10 @@ def on_memory_write(self, action: str, target: str, content: str) -> None:
       
               def _write():
                   try:
      -                client = _VikingClient(self._endpoint, self._api_key)
      +                client = _VikingClient(
      +                    self._endpoint, self._api_key,
      +                    account=self._account, user=self._user, agent=self._agent,
      +                )
                       # Add as a user message with memory context so the commit
                       # picks it up as an explicit memory during extraction
                       client.post(f"/api/v1/sessions/{self._session_id}/messages", {
    • run_agent.py
      @@ -3040,6 +3040,18 @@ def shutdown_memory_provider(self, messages: list = None) -> None:
                   except Exception:
                       pass
           
      +    def commit_memory_session(self, messages: list = None) -> None:
      +        """Trigger end-of-session extraction without tearing providers down.
      +        Called when session_id rotates (e.g. /new, context compression);
      +        providers keep their state and continue running under the old
      +        session_id — they just flush pending extraction now."""
      +        if not self._memory_manager:
      +            return
      +        try:
      +            self._memory_manager.on_session_end(messages or [])
      +        except Exception:
      +            pass
      +
           def close(self) -> None:
               """Release all resources held by this agent instance.
       
      @@ -6826,6 +6838,8 @@ def _compress_context(self, messages: list, system_message: str, *, approx_token
                   try:
                       # Propagate title to the new session with auto-numbering
                       old_title = self._session_db.get_session_title(self.session_id)
      +                # Trigger memory extraction on the old session before it rotates.
      +                self.commit_memory_session(messages)
                       self._session_db.end_session(self.session_id, "compression")
                       old_session_id = self.session_id
                       self.session_id = f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:6]}"
    • scripts/release.py
      @@ -197,6 +197,8 @@
           "[redacted:email]": "zhouboli",
           "[redacted:email]": "tomqiaozc",
           "[redacted:email]": "xinbenlv",
      +    "[redacted:email]": "ZaynJarvis",
      +    "[redacted:email]": "ZaynJarvis",
       }
       
       
    • tests/agent/test_memory_provider.py
      @@ -695,3 +695,44 @@ def test_fenced_block_separates_user_from_recall(self):
               fence_end = combined.index("</memory-context>")
               assert "Alice" in combined[fence_start:fence_end]
               assert combined.index("weather") < fence_start
      +
      +
      +# ---------------------------------------------------------------------------
      +# AIAgent.commit_memory_session — routes to MemoryManager.on_session_end
      +# ---------------------------------------------------------------------------
      +
      +
      +class _CommitRecorder(FakeMemoryProvider):
      +    """Provider that records on_session_end calls for assertions."""
      +
      +    def __init__(self, name="recorder"):
      +        super().__init__(name)
      +        self.end_calls = []
      +
      +    def on_session_end(self, messages):
      +        self.end_calls.append(list(messages or []))
      +
      +
      +class TestCommitMemorySessionRouting:
      +    def test_on_session_end_fans_out(self):
      +        mgr = MemoryManager()
      +        builtin = _CommitRecorder("builtin")
      +        external = _CommitRecorder("openviking")
      +        mgr.add_provider(builtin)
      +        mgr.add_provider(external)
      +
      +        msgs = [{"role": "user", "content": "hi"}]
      +        mgr.on_session_end(msgs)
      +
      +        assert builtin.end_calls == [msgs]
      +        assert external.end_calls == [msgs]
      +
      +    def test_on_session_end_tolerates_failure(self):
      +        mgr = MemoryManager()
      +        builtin = FakeMemoryProvider("builtin")
      +        bad = _CommitRecorder("bad-provider")
      +        bad.on_session_end = lambda m: (_ for _ in ()).throw(RuntimeError("boom"))
      +        mgr.add_provider(builtin)
      +        mgr.add_provider(bad)
      +
      +        mgr.on_session_end([])  # must not raise

8 PRs closed without merging

From the close comments: superseded by another PR 3 · merged via another change 2 · done elsewhere 1 · duplicate 1 · won't fix / by design 1

  • #4742 fix(openviking): correct API endpoint paths, response parsing, and auth headers · superseded by another PR → #4743
  • #4770 fix(openviking): correct API endpoints validated against live server OpenAPI spec · superseded by another PR → #5078, #4742
  • #4863 fix(openviking): send tenant-scoping headers for multi-tenant servers · merged via another change → #4936
  • #9103 fix(memory): align OpenViking default account with server extraction default · superseded by another PR → #10463, #9807
  • #12937 fix(openviking): viking_read fails on file URIs — route to /content/read · merged via another change → 5d253e6, #17869, #5886, #12757
Show 3 more
  • #21136 fix(openviking): resolve 403 with API key auth and late .env loading · done elsewhere → #51952, #21130
  • #47355 fix(openviking): skip tenant headers when api_key is set · duplicate → 4af16b5, #48206
  • #47528 fix(openviking): v0.4.1 compatibility - auth headers and search limit · won't fix / by design → #47527, #46532, #47528
Closed issues · 8
  • Closed#4740fix(openviking): plugin uses non-existent API endpoints, browse/read tools broken
  • Closed#4825fix(openviking memory): send tenant headers for multi-tenant OpenViking
  • Closed#8739Memory search returns 0 results: OpenViking _tenant_filter account_id mismatch
  • Closed#8742Memory search returns 0 results: OpenViking _tenant_filter account_id mismatch
  • Closed#8743Memory search returns 0 results: OpenViking _tenant_filter account_id mismatch
  • Closed#21130OpenViking plugin: 403 with API key auth + env vars not reloaded after /reload
  • Closed#47344OpenViking plugin always sends X-OpenViking-Account header, broken on OV v0.4.1+ with api_key auth
  • Closed#47527fix(openviking): v0.4.1 compatibility - auth headers and search limit
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.08: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#4936, #5078, #10463
Closed issues
8
Attempts
8 PRs closed without merging
Fix link checked
15 Apr 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.

For your agent

The same facts, typed.