fix(tui_gateway): scope secrets and MCP discovery to the active profile (#67605)

The dashboard/desktop profile switch was partial — switching to profile X
ran the launch profile's resources in two ways:

1. MCP discovery was gated on the launch profile's config having
   mcp_servers. If the launch profile had none, the background thread
   never started and zero MCP servers existed for every profile. Fix:
   always start discovery and let discover_mcp_tools() handle the
   empty-config case.

2. The profile secret scope (.env credentials) was never installed on the
   tui_gateway path. get_secret() fell through to os.environ, resolving
   secrets from the launch profile instead of the selected one. Fix:
   install set_secret_scope(build_profile_secret_scope(...)) alongside
   every set_hermes_home_override() call site:
   - compute_host.py:_ensure_server_session (build-time)
   - server.py:_build (lazy resume)
   - server.py:_handle_resume_session (_make_agent scope)
   - server.py:_handle_resume_session (_init_session scope)
   - server.py:_handle_submit_or_edit (per-turn handler)
This commit is contained in:
kyssta-exe 2026-07-19 18:10:50 +00:00 committed by Teknium
parent b93fd077c0
commit 2db6b8c85b
2 changed files with 42 additions and 0 deletions

View file

@ -436,12 +436,15 @@ class ComputeHost:
profile_home = str(frame.get("profile_home") or "")
session_db = None
home_token = None
secret_token = None
try:
if profile_home:
from hermes_constants import set_hermes_home_override
from agent.secret_scope import build_profile_secret_scope, set_secret_scope
from hermes_state import SessionDB
home_token = set_hermes_home_override(profile_home)
secret_token = set_secret_scope(build_profile_secret_scope(Path(profile_home)))
session_db = SessionDB(db_path=Path(profile_home) / "state.db")
agent = server._make_agent(
sid,
@ -457,8 +460,10 @@ class ComputeHost:
if home_token is not None:
try:
from hermes_constants import reset_hermes_home_override
from agent.secret_scope import reset_secret_scope
reset_hermes_home_override(home_token)
reset_secret_scope(secret_token)
except Exception:
pass
try:

View file

@ -18,6 +18,11 @@ from datetime import datetime
from pathlib import Path
from typing import Any, NamedTuple, Optional
from agent.secret_scope import (
build_profile_secret_scope,
reset_secret_scope,
set_secret_scope,
)
from hermes_constants import (
get_hermes_home,
get_hermes_home_override,
@ -1834,6 +1839,7 @@ def _start_agent_build(sid: str, session: dict) -> None:
worker = None
notify_registered = False
home_token = None
secret_token = None
profile_home = current.get("profile_home")
try:
tokens = _set_session_context(key)
@ -1843,6 +1849,12 @@ def _start_agent_build(sid: str, session: dict) -> None:
session_db = None
if profile_home:
home_token = set_hermes_home_override(profile_home)
try:
from agent.secret_scope import build_profile_secret_scope, set_secret_scope
secret_token = set_secret_scope(build_profile_secret_scope(Path(profile_home)))
except Exception:
pass
try:
from hermes_state import SessionDB
@ -1963,6 +1975,13 @@ def _start_agent_build(sid: str, session: dict) -> None:
finally:
if home_token is not None:
reset_hermes_home_override(home_token)
if secret_token is not None:
try:
from agent.secret_scope import reset_secret_scope
reset_secret_scope(secret_token)
except Exception:
pass
# _attach_worker already closed the worker if this session was
# reaped mid-build; only the late notify registration can still
# leak (session.close unregistered before _build registered it).
@ -7474,6 +7493,11 @@ def _(rid, params: dict) -> dict:
home_token = (
set_hermes_home_override(str(profile_home)) if profile_home is not None else None
)
secret_token = (
set_secret_scope(build_profile_secret_scope(Path(str(profile_home))))
if profile_home is not None
else None
)
try:
db.reopen_session(target)
# One lineage SELECT feeds both projections (see the interactive resume
@ -7515,6 +7539,8 @@ def _(rid, params: dict) -> dict:
finally:
if home_token is not None:
reset_hermes_home_override(home_token)
if secret_token is not None:
reset_secret_scope(secret_token)
# Double-checked locking: another concurrent resume may have created the
# live session while we were building. Re-check under the lock; if it won,
@ -7545,6 +7571,11 @@ def _(rid, params: dict) -> dict:
if profile_home is not None
else None
)
init_secret_token = (
set_secret_scope(build_profile_secret_scope(Path(str(profile_home))))
if profile_home is not None
else None
)
try:
_init_session(
sid,
@ -7559,6 +7590,8 @@ def _(rid, params: dict) -> dict:
finally:
if init_home_token is not None:
reset_hermes_home_override(init_home_token)
if init_secret_token is not None:
reset_secret_scope(init_secret_token)
if sid in _sessions:
if stored_runtime_overrides.get("model_override") is not None:
_sessions[sid]["model_override"] = stored_runtime_overrides[
@ -11609,6 +11642,7 @@ def _run_prompt_submit(
approval_token = None
session_tokens = []
home_token = None # per-turn HERMES_HOME override for a resumed remote profile
secret_token = None
goal_followup = None # set by the post-turn goal hook below
tts_queue = None # streaming-TTS feed for this turn (voice mode)
one_turn_restore = session.pop("one_turn_model_restore", None)
@ -11641,6 +11675,7 @@ def _run_prompt_submit(
_profile_home_str = session.get("profile_home")
if _profile_home_str:
home_token = set_hermes_home_override(_profile_home_str)
secret_token = set_secret_scope(build_profile_secret_scope(Path(_profile_home_str)))
# The sudo password callback is thread-local (tools.terminal_tool
# _callback_tls), so wiring it on the build thread doesn't reach this
# turn thread — terminal sudo prompts would fall through to /dev/tty
@ -12164,6 +12199,8 @@ def _run_prompt_submit(
pass
if home_token is not None:
reset_hermes_home_override(home_token)
if secret_token is not None:
reset_secret_scope(secret_token)
_clear_session_context(session_tokens)
# Clear the per-turn interim callback so a stale closure from
# this turn can't fire during a later turn on the same agent.