fix(env): scope terminal config re-bridge to the true process profile - #97014
Closed
Bergmann89 wants to merge 1 commit into
Closed
Bergmann89 wants to merge 1 commit into
Bergmann89 wants to merge 1 commit into
Conversation
monerostar
reviewed
Aug 28, 2026
monerostar
left a comment
There was a problem hiding this comment.
Ubuntu 26.04 on linux-5800x (kernel 7.0.0-30-generic). This box runs more than one Hermes profile in one process family, so the shared os.environ leak is the class we care about.
Copied the PR tests to /tmp so pytest would import each tree:
- main
8c098e9e8: both fail._process_hermes_home()followsset_hermes_home_override(get_hermes_home()). After a secondary-profile reload,TERMINAL_ENVgoesssh->local. - this PR
6eb4b5050: 2 passed in 0.03s. Process home stays the launch home.TERMINAL_ENVstaysssh.
Python 3.11.15. Looks good.
This was referenced Aug 30, 2026
muhifni
added a commit
to muhifni/hermes-agent
that referenced
this pull request
Sep 2, 2026
A multiplexed Hermes process (gateway.multiplex_profiles, unified dashboard/TUI, or cron) can serve several profiles at once. Terminal settings used to resolve through process-global TERMINAL_* env vars plus the one-shot _ensure_terminal_env_bridged() guard. The first profile to touch a terminal tool after startup therefore pinned its backend and other policy (mounts, SSH target, network, cwd, resources) onto every later profile until restart. This is a correctness and sandbox-boundary bug: a local profile can run inside another profile's docker sandbox, and a docker/ssh profile can be dropped onto the launch host. Repro lineages: canonical NousResearch#68559, gateway backend latch NousResearch#94200, dashboard/container symptoms NousResearch#98581/NousResearch#96992. Fix with an authoritative profile terminal policy seam, analogous to agent/secret_scope.py: - tools/terminal_scope.py adds a ContextVar that holds the active profile's complete effective terminal policy. Projection order is defined defaults + supplemental tool defaults <- profile .env TERMINAL_* values <- explicit terminal: config.yaml keys. Once a scope is bound, missing values never fall through to ambient os.environ. - install_profile_terminal_scope() fail-closes: unreadable/malformed policy installs a refusal scope; terminal_tool / execute_code refuse execution instead of inheriting launch-process policy. - tools/terminal_tool.py routes TERMINAL_* reads through the scope-aware _tenv() helper and suppresses the process-env bridge while scoped. - gateway/run.py, tui_gateway/server.py and cron/scheduler.py install the same profile terminal scope at their in-process profile boundaries. - Other scoped readers from the first patch (prompt/cwd/media/footer) remain routed through the same seam. Tests now cover the review-requested matrix: polluted launch profile A with sensitive mounts/SSH/CWD/network/resource policy; profile B with backend-only, empty terminal config, or .env-only selections cannot observe A's values. They also cover malformed/unreadable policy refusal, terminal_tool refusal, gateway cleanup including error paths, dashboard/TUI session scope, and cron install/reset lifecycles. Prior art / lineage: x7peeps NousResearch#68611 (original ContextVar direction), 100yenadmin NousResearch#79117/NousResearch#78030, liuhao1024 NousResearch#94206/NousResearch#98589, and complementary Bergmann89 NousResearch#97014 (env_loader re-bridge containment). Fixes NousResearch#68559 Refs NousResearch#94200, NousResearch#98581, NousResearch#96992
12 of 13 tasks
Bergmann89
force-pushed
the
fix/terminal-bridge-profile-scope
branch
from
September 4, 2026 18:53
6eb4b50 to
5c82f91
Compare
In a multiplex dashboard (one process serving every profile on the host),
a secondary profile's terminal.backend could leak into the shared
os.environ and silently override the launch profile's terminal backend.
A profile configured for terminal.backend: ssh would find itself running
every command locally because a sibling profile with backend: local had
polluted the shared environment.
The tell is an asymmetric env state on the launch session:
TERMINAL_ENV=local # leaked from a sibling profile
TERMINAL_SSH_HOST=10.10.0.103 # still the launch profile's, untouched
A restart does not help: as soon as the sibling profile is touched again
(a turn, a cron tick), the shared env is re-poisoned.
Root cause: env_loader._reapply_terminal_config_bridge() guards "only
re-bridge config into the shared os.environ when this load is for the
process's own profile" via _process_hermes_home(). That helper delegated
to get_hermes_home(), which follows the context-local
set_hermes_home_override a per-request task installs. When a per-turn
handler is scoped to a secondary profile and triggers a dotenv reload,
both sides of the comparison resolve to that secondary profile, the guard
passes, and the bridge writes the wrong profile's terminal.backend into
the shared environment. Because the secondary profile's config carries no
TERMINAL_SSH_* keys, only TERMINAL_ENV is overwritten, producing the
asymmetric state above.
The helper was introduced for the config-cache key (where following the
active home is correct) and later reused for the terminal bridge guard,
where the override-following semantics are wrong.
Fix: delegate _process_hermes_home() to get_process_hermes_home(), the
override-immune resolver built for exactly this. It reflects the scope the
process was launched under, ignoring per-task overrides. Both call sites
(the bridge guard and the secrets-cache reuse check) want the true process
home. Single-profile / CLI behaviour is unchanged: with no active
override the two resolvers are identical.
Complements the terminal_tool._active_environments session-key fix: that
scopes the per-session environment cache; this keeps the shared os.environ
the cache reads TERMINAL_ENV from uncontaminated in the first place.
Adds tests/hermes_cli/test_terminal_bridge_profile_scope.py covering both
the override-immune resolver and the no-leak reload behaviour.
Bergmann89
force-pushed
the
fix/terminal-bridge-profile-scope
branch
from
September 4, 2026 20:48
5c82f91 to
9e721cf
Compare
|
Landed on |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
In a multiplexed dashboard/gateway (one process serving every profile on the host), a secondary profile's
terminal.backendcould leak into the sharedos.environand silently override the launch profile's terminal backend. A profile configured forterminal.backend: sshwould end up running every command on the local host because a sibling profile withbackend: localhad polluted the shared environment.The tell is an asymmetric env state on the launch session:
A restart does not help: as soon as the sibling profile is touched again (a turn, a cron tick), the shared env is re-poisoned.
Root cause
env_loader._reapply_terminal_config_bridge()guards "only re-bridge config into the sharedos.environwhen this load is for the process's own profile":_process_hermes_home()delegated toget_hermes_home(), which follows the context-localset_hermes_home_overridea per-request task installs. When a per-turn handler is scoped to a secondary profile (via_config_profile_scope/set_hermes_home_override) and triggers aload_hermes_dotenv()reload, both sides of the comparison resolve to that secondary profile — the guard passes, and the bridge runsapply_terminal_config_to_env(env=None)against the sharedos.environ, writing the wrong profile'sterminal.backend.Because the secondary profile's
config.yamlcarries noTERMINAL_SSH_*keys, onlyTERMINAL_ENVis overwritten (explicit-keys-only bridge semantics), producing the asymmetric state above.The helper was originally introduced for the config-cache key (where following the active home is correct) and later reused for the terminal bridge guard, where the override-following semantics are wrong.
The fix
Delegate
_process_hermes_home()toget_process_hermes_home(), the override-immune resolver built for exactly this — it reflects the scope the process was launched under, ignoring per-task overrides. Both call sites (the bridge guard and the secrets-cache reuse check in_load_secrets_config) want the true process home.This is the correct boundary: in a multiplex process
os.environbelongs to the launch profile. A secondary profile's terminal config must reach its own sessions through session-scoped mechanisms (see Related), never by mutating the shared process environment.Related Issue
Same bug class (cross-profile terminal-backend leak under multiplexing), addressed here at a bridge site that no open PR touches:
HERMES_HOMEfor tool env — same symptom class at the process-env layer).terminal_tool._ensure_terminal_env_bridged()process-global one-shot latch.gateway/run.pyfor routed multiplex profiles.terminal.cwd(not backend) for multiplex sessions.Each of those fixes a different bridge site. This PR fixes a third, independent one —
env_loader._reapply_terminal_config_bridge()— which runs on everyload_hermes_dotenv()and is not covered by any of the above. Fixing only the others leaves this path as a live leak (and vice versa).Happy to fold this into #96992's tracking or coordinate with #94206's author if the maintainers prefer to consolidate the cluster.
Type of Change
Changes Made
hermes_cli/env_loader.py—_process_hermes_home()now delegates tohermes_constants.get_process_hermes_home()(override-immune) instead ofget_hermes_home()(which follows the per-task context override). Docstring expanded to explain why both callers require the true process home.tests/hermes_cli/test_terminal_bridge_profile_scope.py— new regression test:test_process_hermes_home_ignores_task_override— the resolver ignores an activeset_hermes_home_override.test_secondary_profile_reload_does_not_bridge_into_shared_env— a secondary-profile reload leaves the launch profile'sTERMINAL_ENVandTERMINAL_SSH_*inos.environuntouched.How to Test
Reproduce the leak and confirm the fix:
Configure two profiles:
laptop(terminal.backend: ssh, withssh_host/ssh_user) andtommy(terminal.backend: local, no ssh keys).Launch under
laptop(HERMES_HOME=…/laptop), so the shared env carriesTERMINAL_ENV=ssh+TERMINAL_SSH_*.Simulate a per-turn handler scoped to the secondary profile and drive a reload for its home:
Before:
os.environ["TERMINAL_ENV"] == "local"(tommy's backend leaked),TERMINAL_SSH_HOSTstill laptop's — the launch session silently runs locally. After:TERMINAL_ENVstays"ssh"; nothing leaks.Automated:
Checklist
Code
fix(env): …)pytest tests/ -qand all tests passDocumentation & Housekeeping
_process_hermes_homedocstring; no user-facing docs affectedcli-config.yaml.example— N/A (no config keys added/changed)CONTRIBUTING.md/AGENTS.md— N/A (no architecture/workflow change)