Skip to content

security(gateway): re-resolve hooks directory per call to fix profile isolation - #428

Open
hashbender wants to merge 1 commit into
mainfrom
mirror/pr-56508
Open

security(gateway): re-resolve hooks directory per call to fix profile isolation#428
hashbender wants to merge 1 commit into
mainfrom
mirror/pr-56508

Conversation

@hashbender

Copy link
Copy Markdown
Owner

Summary

gateway/hooks.py::HOOKS_DIR is resolved once at import time via get_hermes_home(), which reads a context-local ContextVar (_HERMES_HOME_OVERRIDE) set per-request under the multiplexed gateway (multiple profiles served from one process, e.g. the desktop tui_gateway). Freezing the resolved path at import time pins every later HookRegistry.discover_and_load() call to whichever profile's HERMES_HOME happened to be active the first time this module was imported in the process.

This is more serious than the typical instance of this bug class (already fixed for cache dirs, skills_hub.py, rich_sent_store.py, and — from this same audit — the Anthropic OAuth file, Nous auth.json, sessions.json index, checkpoint store, and sticker cache): the hooks system loads and executes arbitrary Python (handler.py, dynamically imported via importlib.util) in response to live agent events. Under the multiplexed gateway, each profile owns its own Gateway/HookRegistry instance, but they all read the same frozen module constant — so a later-starting profile's HookRegistry discovers and executes the FIRST profile's hook handlers against its own live event context (session_id, chat_id, message, response text truncated to 500 chars, per the module's own docstring), not the profile's own hooks. This is both a cross-profile data leak and an isolation-boundary violation (profile B's process runs profile A's hook code).

Changes

  • gateway/hooks.py: added _resolve_hooks_dir(), called from discover_and_load() instead of reading HOOKS_DIR directly. Keeps the module constant for backward compat and to preserve the existing test seam (tests/gateway/test_hooks.py has many call sites that patch("gateway.hooks.HOOKS_DIR", tmp_path)) — the resolver honors a monkeypatched value away from its import-time default, otherwise re-resolves fresh, mirroring gateway/platforms/base.py::_resolve_cache_dir's established pattern.
  • tests/test_profile_isolation_runtime.py (the existing profile-isolation regression suite): added TestGatewayHooksDirResolution with three tests — the resolved path actually differs between two distinct profile overrides, an end-to-end test proving a hook that only exists under profile B's hooks dir is NOT discovered when profile A's override is active (and vice versa), and a "monkeypatched constant still wins" regression test for the test-seam-preserving resolver.

Test plan

  • pytest tests/gateway/test_hooks.py tests/gateway/test_background_command.py -q — 42 passed, 1 pre-existing unrelated failure (confirmed via git stash: a macOS /private/var vs /var symlink path-comparison quirk in test_media_files_routed_by_type, present identically before this change)
  • pytest tests/test_profile_isolation_runtime.py -q — 13 passed (10 pre-existing + 3 new)
  • ruff check on all changed files — clean

Mirror-of: NousResearch#56508
NousResearch#56508

@tenki-reviewer

tenki-reviewer Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Complete
No issues found!

Risk: 🟠 High (62/100) — no findings · 91 LOC across 2 files


Profile-aware hook directory resolution introduced in this PR creates a sys.modules namespace collision when two profiles register hooks with the same name, risking cross-profile type resolution errors.

Files Reviewed (2 files)
gateway/hooks.py
tests/test_profile_isolation_runtime.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant