fix(openviking): harden tool gating, config auth, and runtime shutdown - #51952
fix(openviking): harden tool gating, config auth, and runtime shutdown#51952ehz0ah wants to merge 15 commits into
Conversation
tonydwb
left a comment
There was a problem hiding this comment.
Hermes Agent Review
Verdict: Approved — Clean, well-scoped change with appropriate tests.
Reviewed as part of batch review session 2026-06-24d.
Reviewed by Hermes Agent
ef61ba4 to
441c4f6
Compare
53e0fab to
4e649d5
Compare
c8f66c7 to
c60f60c
Compare
`_write_env_vars` in the OpenViking memory provider interpolates each secret straight into a `KEY=VALUE` line, but the values only ever pass through `_clean_config_value`, whose `value.strip()` trims surrounding whitespace and leaves internal CR/LF intact. Because the file is strictly line-oriented and is re-read via `read_text().splitlines()`, a value that carries an embedded newline spills onto a second physical line, and the tail is re-parsed as an independent `KEY=VALUE` entry on the next round trip. A secret pasted with a trailing record (e.g. an `OPENVIKING_API_KEY` copied with an extra line) therefore injects an arbitrary additional variable into the persisted credentials file and silently corrupts it. The fix neutralizes the line terminators at the single chokepoint where values reach the file. A small `_env_line_safe` helper strips `\r`, `\n`, and the NUL byte from each value, and both write sites in `_write_env_vars` (the existing-key update branch and the appended-key branch) route through it, so a value can only ever occupy the single line it is written on. ## What does this PR do? Hardens the OpenViking memory provider's `.env` writer so a malformed or pasted secret value can no longer break out of its `KEY=VALUE` line and inject a rogue variable into the profile-scoped credentials file. ## Related Issue N/A ## Type of Change - [x] 🐛 Bug fix (non-breaking change that fixes an issue) ## Changes Made - `plugins/memory/openviking/__init__.py`: add `_env_line_safe()` which removes `\r`, `\n`, and `\x00` from a value, and apply it to both the updated-key and appended-key write branches in `_write_env_vars()`. - `tests/plugins/memory/test_openviking_provider.py`: add two regression tests covering a fresh write and an in-place key update with embedded CR/LF, asserting no injected line survives the read-back. ## How to Test 1. Run the targeted tests: `pytest tests/plugins/memory/test_openviking_provider.py -k env_writer -q` 2. Reverting the `_env_line_safe` sanitization makes `test_openviking_env_writer_strips_embedded_newlines_in_values` and `test_openviking_env_writer_strips_newlines_when_updating_existing_key` fail with a rogue `INJECTED_KEY=`/`ROGUE=1` line appearing in the file, confirming the tests pin the bug. 3. `ruff check plugins/memory/openviking/__init__.py` and `python scripts/check-windows-footguns.py plugins/memory/openviking/__init__.py` both pass. ## Checklist ### Code - [x] I've read the Contributing Guide - [x] My commit messages follow Conventional Commits - [x] I searched for existing PRs to make sure this isn't a duplicate - [x] My PR contains only changes related to this fix - [x] I've run the relevant tests and they pass - [x] I've added tests for my changes (required for bug fixes) - [x] I've tested on my platform: macOS 15 (Darwin 25.5) ### Documentation & Housekeeping - [x] I've updated relevant documentation (docstrings) — or N/A - [x] I've updated `cli-config.yaml.example` if I added/changed config keys — N/A - [x] I've updated `CONTRIBUTING.md` or `AGENTS.md` if I changed architecture or workflows — N/A - [x] I've considered cross-platform impact (strips CR as well as LF) — done - [x] I've updated tool descriptions/schemas if I changed tool behavior — N/A (cherry picked from commit f29dd2d)
initialize() snapshots OPENVIKING_* into the provider once, so /reload (which only updates os.environ) leaves viking_* tools running against stale auth — users have to restart hermes to pick up keys added to ~/.hermes/.env after startup. Add _ensure_client(), which re-resolves the connection settings via the same _resolve_connection_settings/_load_hermes_openviking_config path initialize() uses and rebuilds + health-checks the client only when an OPENVIKING_* value actually changed; otherwise it reuses the cached client so the hot path stays at one dict comparison with no network calls. Every `if not self._client:` guard in system_prompt_block, queue_prefetch, sync_turn, on_session_end, on_memory_write and handle_tool_call now goes through it. Refreshing is gated behind a flag set at the end of initialize() so the baseline is established before any env re-resolution happens — callers that wire up a client directly keep the existing client untouched. Refs NousResearch#21130 (cherry picked from commit b694d21b7c4ff0330df6051e12dc8991f7ea10a6)
…t-exit) `OpenVikingMemoryProvider.shutdown()` joins in-flight writers, deferred-commit threads, and prefetch threads, but not `_runtime_start_thread` — the tracked `daemon=True` waiter that runs `_finish_runtime_openviking_start`, which blocks on network health probes (`_wait_for_openviking_health` polling + a `_VikingClient.health()` request). If the local OpenViking runtime is slow or unreachable, that waiter can still be blocked in network I/O at interpreter exit. CPython then forcibly kills it during `Py_FinalizeEx` (`PyThread_exit_thread` -> `__pthread_unwind` -> `abort()`), producing SIGABRT (exit 134) with no traceback — the same daemon- thread-at-exit failure class fixed for the Honcho provider. Fix: - `shutdown()` now joins `_runtime_start_thread` (timeout-bounded) alongside the other tracked threads. - `_wait_for_openviking_health()` gains a `should_stop` callback; the waiter passes `lambda: self._shutting_down` so the poll loop bails out promptly once `shutdown()` flips the flag, instead of lingering up to the 60s autostart timeout and timing out the join (which would leave the thread alive). - Add tests/plugins/memory/test_openviking_shutdown.py covering the short-circuit and the shutdown-joins-runtime-thread behaviour. (cherry picked from commit 5471ec70210b35462450aa52f0cd483439fffba7)
Add the AUTHOR_MAP entry required for the salvaged NousResearch#49832 OpenViking shutdown fix so contributor attribution CI can resolve the original author.
…ard-coding strings The _needs_trusted_identity_retry method was hard-coding specific server-side error strings to detect when a request failed due to missing X-OpenViking-Account / X-OpenViking-User headers. Each new server-side error variant required another string added to the client. Replace the string enumeration with a structural match: the error message mentions one of the tenant headers AND the HTTP status is 400. This covers all current error variants: - "Trusted mode requests must include X-OpenViking-Account and User" - "ROOT requests to tenant-scoped APIs must include X-OpenViking-Account" - "Trusted mode requests must include X-OpenViking-Account." - "Trusted mode requests must include X-OpenViking-User." The 400 status guard avoids false-positives on 403 errors such as "USER API keys cannot override X-OpenViking-User", which must not trigger a retry. All 176 existing tests pass. (cherry picked from commit 5a24d67)
Route refreshed unreachable local OpenViking configs through the existing runtime recovery path so /reload can attach to a locally starting server instead of disabling memory until restart. (cherry picked from commit 040e18a)
Avoid spawning multiple local OpenViking server processes while a runtime autostart waiter is already active. Remote endpoints still retry on later accesses because they do not install a local waiter.
c60f60c to
6eb8676
Compare
|
Thanks for consolidating the OpenViking follow-ups. Static review confirms the reported premises on the current checkout: raw The shutdown path also drains three worker families but not Automated hermes-sweeper review. |
What does this PR do?
Hardens the existing OpenViking memory provider by consolidating focused upstream PRs and related current-main fixes on top of current
main.This keeps the provider aligned with the current OpenViking source contract while addressing these independent bug classes:
.envline and corrupting persisted config;/reloadso updatedOPENVIKING_*settings are picked up without restarting Hermes;/reload, including starting an unavailable local OpenViking server and attaching it in the background;memorytoolset is explicitly disabled, including after MCP tool-snapshot refreshes;The branch preserves contributor authorship and release attribution for the original work:
Related Issue
Refs #21130
Supersedes / safe to close after this lands:
.envwriter hardening commit..envand auth-refresh value is covered by the current_ensure_client()implementation here.mainalready resolves enabled composite toolsets correctly. This PR adapts the remaining disabled-toolset and reconnect behavior to the current shared memory-provider injection, MCP refresh, and OpenViking runtime-health paths.Type of Change
Changes Made
agent/memory_manager.pydisabled_toolsets: ["memory"]takes precedence over default, direct,all, and composite enablement.tools/mcp_tool.py/reload-mcpor late MCP discovery.plugins/memory/openviking/__init__.py_env_line_safe()and uses it at both.envwrite sites.splitlines()plus NUL so persisted.envvalues remain single-line._ensure_client()and wires all live OpenViking access paths through it, including current-query recall, tool calls, sync, session end/switch, and memory mirroring./reloadresolves to an unavailable local endpoint, while unavailable remote endpoints continue retrying on later accesses._clientare published as one coherent state._wait_for_openviking_health(..., should_stop=...)and joins_runtime_start_threadwhile preserving existing memory-write worker draining.tests/agent/test_memory_provider.pyall, and composite enabled-toolset configurations.tests/tools/test_refresh_agent_mcp_tools.pytests/openviking_plugin/test_openviking.pyreload_env()path with an isolated temporaryHERMES_HOME.tests/plugins/memory/test_openviking_provider.py.envnewline, splitline, and NUL sanitization regressions.tests/plugins/memory/test_openviking_shutdown.pyscripts/release.pyHow to Test
scripts/run_tests.sh tests/agent/test_memory_provider.py tests/tools/test_refresh_agent_mcp_tools.py tests/openviking_plugin/test_openviking.py tests/plugins/memory/test_openviking_provider.py tests/plugins/memory/test_openviking_shutdown.py -- -o addopts=.venv/bin/python -m py_compile agent/memory_manager.py tools/mcp_tool.py plugins/memory/openviking/__init__.py tests/agent/test_memory_provider.py tests/tools/test_refresh_agent_mcp_tools.py tests/openviking_plugin/test_openviking.py tests/plugins/memory/test_openviking_provider.py tests/plugins/memory/test_openviking_shutdown.py scripts/release.py.venv/bin/ruff check agent/memory_manager.py tools/mcp_tool.py plugins/memory/openviking/__init__.py tests/agent/test_memory_provider.py tests/tools/test_refresh_agent_mcp_tools.py tests/openviking_plugin/test_openviking.py tests/plugins/memory/test_openviking_provider.py tests/plugins/memory/test_openviking_shutdown.py scripts/release.py.venv/bin/python scripts/check-windows-footguns.py agent/memory_manager.py tools/mcp_tool.py plugins/memory/openviking/__init__.py tests/agent/test_memory_provider.py tests/tools/test_refresh_agent_mcp_tools.py tests/openviking_plugin/test_openviking.py tests/plugins/memory/test_openviking_provider.py tests/plugins/memory/test_openviking_shutdown.py scripts/release.pygit diff --check upstream/main...HEADLocal result:
Checklist
Code
pytest tests/ -qand all tests passDocumentation & Housekeeping
cli-config.yaml.exampleif I added/changed config keys — N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/A