Skip to content

fix(security): re-resolve profile-scoped credential/session paths per call - #56302

Open
srojk34 wants to merge 1 commit into
NousResearch:mainfrom
srojk34:fix/multiplex-profile-oauth-path-leak
Open

fix(security): re-resolve profile-scoped credential/session paths per call#56302
srojk34 wants to merge 1 commit into
NousResearch:mainfrom
srojk34:fix/multiplex-profile-oauth-path-leak

Conversation

@srojk34

@srojk34 srojk34 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Three modules resolve a profile-scoped path once at import time and cache it in a module-level constant:

  • agent/anthropic_adapter.py::_HERMES_OAUTH_FILE — the Hermes-managed Anthropic OAuth credential file
  • agent/auxiliary_client.py::_AUTH_JSON_PATH — the Nous Portal auth.json
  • gateway/mirror.py::_SESSIONS_INDEX — the delivery-mirror's sessions.json lookup index

All three go through get_hermes_home(), which reads a context-local ContextVar (_HERMES_HOME_OVERRIDE in hermes_constants.py) set per-request via set_hermes_home_override() for 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 request to whichever profile's HERMES_HOME happened to be active the first time the module was imported in the process — the same bug class already fixed for cache dirs (gateway/platforms/base.py), tools/skills_hub.py, and gateway/rich_sent_store.py.

For the Anthropic OAuth file specifically, this isn't just a read leak: hermes_cli/web_server.py's OAuth save/disconnect handlers (_save_anthropic_oauth_creds, disconnect_oauth_provider, _anthropic_oauth_status) all import and use the same frozen constant, so completing an OAuth login under one profile could write the freshly-obtained token into a different profile's credential file.

Changes

  • agent/anthropic_adapter.py: added _resolve_hermes_oauth_file(), called from read_hermes_oauth_credentials(). Keeps the module constant for backward compat and to preserve the existing test seam (tests/hermes_cli/test_web_server_oauth_write.py monkeypatches agent.anthropic_adapter._HERMES_OAUTH_FILE directly) — 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.
  • hermes_cli/web_server.py: updated all three call sites that imported the raw _HERMES_OAUTH_FILE constant to import and call _resolve_hermes_oauth_file() instead.
  • agent/auxiliary_client.py: added _resolve_auth_json_path(), called from _read_nous_auth(). No existing test monkeypatches the raw constant, so this is a direct per-call resolution without the test-seam indirection.
  • gateway/mirror.py: added _resolve_sessions_index(), called from _find_session_id(). Same test-seam-preserving pattern as the OAuth file, since tests/gateway/test_mirror.py has many call sites that monkeypatch gateway.mirror._SESSIONS_INDEX directly.
  • tests/test_profile_isolation_runtime.py (the existing profile-isolation regression suite): added TestAnthropicOauthFilePathResolution, TestAuxiliaryClientAuthJsonPathResolution, and TestMirrorSessionsIndexResolution, each proving the resolved path actually changes between two distinct profile overrides (not just that existing tests still pass), plus "monkeypatched constant still wins" regression tests for the two test-seam-preserving resolvers.

Test plan

  • pytest tests/test_profile_isolation_runtime.py -q — 15 passed (7 pre-existing + 8 new)
  • pytest tests/gateway/test_mirror.py tests/hermes_cli/test_web_server_oauth_write.py tests/agent/test_auxiliary_client.py tests/agent/test_credential_pool.py -q — 392 passed
  • ruff check on all changed files — clean
  • tests/agent/test_anthropic_adapter.py has 17 pre-existing failures in this sandbox unrelated to this change — confirmed identical (same tests, same error) on upstream/main with this diff stashed out. They stem from real local credential state leaking into TestResolveAnthropicToken/TestRefreshOauthToken/etc., not from anything this PR touches.

… call

agent/anthropic_adapter.py's Hermes OAuth file, agent/auxiliary_client.py's
Nous Portal auth.json, and gateway/mirror.py's sessions.json index are all
resolved once at import time via get_hermes_home(), which is a context-local
ContextVar under the multiplexed gateway (multiple profiles sharing one
process). Freezing the path at import time pins every later request to
whichever profile's HERMES_HOME was active when the module was first
imported, leaking OAuth/portal credentials and session lookups across
profiles (read AND write, since hermes_cli/web_server.py's OAuth
save/disconnect handlers use the same frozen constant) -- the same bug
class already fixed for cache dirs, skills_hub, and rich_sent_store.

Add a per-call resolver for each path, following the established
"respect an existing test monkeypatch of the constant, otherwise
re-resolve through get_hermes_home()" pattern so existing test seams in
tests/hermes_cli/test_web_server_oauth_write.py and
tests/gateway/test_mirror.py keep working unmodified.
@alt-glitch alt-glitch added type/security Security vulnerability or hardening P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/gateway Gateway runner, session dispatch, delivery area/auth Authentication, OAuth, credential pools sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data labels Jul 1, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for identifying the import-time profile-path issue. The auxiliary and mirror portions remain valid on current main: _AUTH_JSON_PATH is still frozen at agent/auxiliary_client.py:662 and read at :1606-1608; _SESSIONS_INDEX is still frozen at gateway/mirror.py:21-22 and used by _find_session_id() at :135-139.

Problems

  • The Anthropic OAuth changes are now superseded by 76979a086: current main uses _get_hermes_oauth_file() at agent/anthropic_adapter.py:1394, with dashboard consumers updated in hermes_cli/web_server.py:8021, :8474, and :8621. The submitted _HERMES_OAUTH_FILE / _resolve_hermes_oauth_file hunks will conflict.
  • The submitted OAuth test seam no longer matches current main, where tests/hermes_cli/test_web_server_oauth_write.py:22 patches _get_hermes_oauth_file.

Suggested changes

  • Retain and rebase only the auxiliary-client and mirror call-time resolution fixes and their focused profile-override coverage; drop the superseded OAuth portion.

Automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform area/sessions Session lifecycle, resume, persistence, history area/profiles Multi-profile isolation, HERMES_HOME scoping labels Jul 15, 2026

@GottZ GottZ left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Summary

Three PRs address the same import-time profile-path freezing class but touch distinct consumers: #56302 covers Nous credentials and the session mirror, with its Anthropic OAuth portion now superseded; #56508 changes hook-directory discovery but not multiplex lifecycle routing; #56523 dynamically resolves skills-sync paths but does not add per-profile gateway startup sync.

Related pull requests

  • #56302 related — (+162/-18) — keep open and rebase: The diff correctly re-resolves auth.json and sessions.json against the active profile, directly fixing the reported cross-profile reads. As the contributor keep_open review documents, the Anthropic OAuth implementation and test seam conflict with the current _get_hermes_oauth_file() implementation and must be dropped.
  • #56508 related — (+89/-2) — keep open for architectural completion: The diff makes hook discovery resolve the active profile's directory, but the contributor keep_open review correctly notes that the shared registry is loaded only at startup and still receives events from secondary profiles; profile-aware registry loading and dispatch plus a gateway-level multiplex regression are required.
  • #56523 related — (+123/-26) — keep open but narrow or complete: The diff removes frozen skills-sync path usage and therefore protects scoped in-process callers, but it does not establish the claimed per-profile gateway startup behavior. Consistent with the contributor keep_open review, either wire sync_skills() into each scoped profile startup and add an end-to-end destination test, or narrow the PR to generic dynamic-path protection.

Suggested consolidation

Merge #56302 after rebasing it to retain only the still-valid auxiliary-client and mirror fixes and their focused tests. Do not close #56508 or #56523 as duplicates: they cover separate hook and skills-sync paths, but both need the changes required by their contributor keep_open reviews before merge.

Cross-PR triage: Reviewed 3 pull requests and 0 issues in this complex. Each diff was read against this issue; Assessment working set: 33 kB of PR diffs, 10 kB of issue/PR text, 6 kB of discussion (4 comments), 0 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

@egilewski

Copy link
Copy Markdown
Contributor

suggesting changes

Security evidence:

  • trust boundary: multiplexed Hermes profiles share one process while each request carries an active profile home through the context-local override.
  • source/sink/invariant: profile-scoped credential and session-index paths must resolve at use time so one profile cannot read another profile's auth.json or legacy sessions.json fallback.
  • current-main reproduction: after importing the affected modules under profile A and switching to profile B, current main read profile A's Nous token and legacy mirror session when the credential-pool and state.db primary paths were disabled; current main already resolves Anthropic OAuth paths per call.
  • PR-head or patch-replay validation: a manager-owned replay of the auxiliary-client and mirror changes on current main read profile B's token and legacy session under profile B, with module paths asserted to come from the replay tree.
  • positive/negative cases: the replay retained the state.db lookup as primary, preserved the existing mirror constant monkeypatch seam, left the credential-pool branch unchanged, fixed the legacy auth.json fallback, and returned state-db-primary under the positive-control primary lookup.
  • residual bypass search: the affected auxiliary, mirror, web-server, and Anthropic paths were searched for frozen credential/session paths; remaining home resolutions are per-call or outside this fallback scope.
  • reviewer validation: the profile-isolation, mirror, web-server OAuth, auxiliary, and credential-pool tests passed (246), and ruff, Python compilation, and whitespace checks passed.

Keep the per-call auxiliary-client and legacy mirror fallback changes, but rebase or narrow the submitted branch onto current main and drop its superseded Anthropic OAuth hunks before merge.

Not checked:

  • Full test suite
  • CodeRabbit review

Signed: GPT-5.6-sol-xhigh in Codex

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

Labels

area/auth Authentication, OAuth, credential pools area/profiles Multi-profile isolation, HERMES_HOME scoping area/sessions Session lifecycle, resume, persistence, history comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants