Skip to content

fix(tools/tui): stop multiplex ambient TERMINAL_* latch from poisoning launch profile (#107422) - #107442

Closed
infinitycrew39 wants to merge 2 commits into
NousResearch:mainfrom
infinitycrew39:fix/107422-multiplex-terminal-bridge-latch
Closed

infinitycrew39 wants to merge 2 commits into
NousResearch:mainfrom
infinitycrew39:fix/107422-multiplex-terminal-bridge-latch

Conversation

@infinitycrew39

Copy link
Copy Markdown

Summary

  • Confirmed #107422 on current upstream/main: _ensure_terminal_env_bridged()'s one-shot latch can write a secondary profile's terminal.* into process-global os.environ when a home override is active without a terminal scope; unscoped launch-profile tool calls then spawn that profile's docker image/volumes.
  • Commit 1: skip the ambient bridge whenever a context-local HERMES_HOME override is set (ambient env is launch-profile authority only; routed profiles use terminal_scope — same rule as env_loader._reapply_terminal_config_bridge).
  • Commit 2: once any secondary home is in _served_profile_homes, bind the launch home's own terminal scope on launch turns so they never fall back to a poisoned ambient env.

Related open approach #94206 re-bridges per home into os.environ; this PR follows the #68559 fail-closed scope model instead (no cross-profile writes into shared process env).

Fixes #107422

Test plan

  • pytest tests/tools/test_terminal_env_bridge.py -q (includes new secondary-override latch regression)
  • pytest tests/tools/test_terminal_scope_multiplex.py::test_launch_turn_binds_terminal_scope_once_multiplexing_is_active -q
  • Manual: multiplexed dashboard (app-global remote); primary terminal.backend: local; secondary docker + volumes; open secondary session then run a primary file/terminal tool — must stay on host / primary policy, never create hermes-<primary> with secondary image/volumes

…ride

A multiplexed dashboard can call _ensure_terminal_env_bridged while a
secondary profile's HERMES_HOME override is active. The one-shot latch then
wrote that profile's docker policy into process-global os.environ and poisoned
later unscoped launch-profile tool calls (NousResearch#107422).

Skip the ambient bridge whenever a context-local home override is set —
ambient env is launch-profile authority only; routed profiles must use
terminal_scope (same rule as env_loader._reapply_terminal_config_bridge).
After any secondary profile home is served, launch-profile turns used to stay
unscoped and fall back to ambient os.environ. Bind the launch home's own
terminal policy in that case so a poisoned ambient bridge can never become
the launch turn's authority (NousResearch#107422 residual of NousResearch#68559).
@infinitycrew39
infinitycrew39 force-pushed the fix/107422-multiplex-terminal-bridge-latch branch from b1e659e to f81147c Compare September 10, 2026 15:10
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists tool/terminal Terminal execution and process management comp/tools Tool registry, model_tools, toolsets comp/tui Terminal UI (ui-tui/ + tui_gateway/) area/profiles Multi-profile isolation, HERMES_HOME scoping sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Sep 10, 2026

@ehz0ah ehz0ah left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I found four blocking issues at f81147c1e5d283837e5e27f4da79710da7025235. The profile-scoped direction is safer than mutating shared process environment state, but the current patch does not yet preserve a complete terminal policy for every profile.

  1. Secondary side-agent workers still use the launch profile's terminal policy. _spawn_side_agent() scopes only HERMES_HOME. With the new bridge guard, a secondary profile configured for Docker reads the launch process's ambient TERMINAL_ENV=local. This affects prompt.background, prompt.btw, and preview.restart. A behavioral countertest configured the secondary profile for Docker and observed local inside the worker. Please bind and reset that profile's terminal scope for the full worker lifetime.

  2. Secondary eager-resume and branch builds also omit terminal scope. _profile_build_scope() binds home and secrets only, and both _resume_eager() and _build_branch_agent() construct agents inside that scope. Tool availability discovery can read terminal configuration during construction, so these paths can freeze the launch profile's backend into a secondary agent's tool schema. Please include terminal scope in the common profile build scope.

  3. The launch-profile scope discards valid environment-only configuration once multiplexing starts. If the dashboard starts with TERMINAL_ENV=ssh and TERMINAL_SSH_HOST=example.test, but the launch home has no terminal config file, _prepare_turn_input() installs a file-backed scope that resolves to local with an empty host. Legacy single-profile behavior therefore changes after a second profile is served. The launch scope needs to preserve the launch profile's effective ambient policy.

  4. The new launch-profile test does not exercise the changed runtime branch and fails when run by itself. It calls install_profile_terminal_scope() directly instead of _prepare_turn_input() with multiplexing active. The autouse fixture already creates tmp_path / ".hermes", so launch_home.mkdir() raises FileExistsError before the assertions. The same direct scope behavior also passes on the base commit. Please replace it with a regression that enters the real launch-turn path and fails before the fix.

Validation on the exact head: the directly relevant suite passed 27 tests, the broader gateway set passed 58 tests, Ruff passed, compatibility and Windows-footgun checks completed, and the merge into current upstream main was conflict-free. Three focused behavioral countertests reproduced findings 1 through 3.

@teknium1

Copy link
Copy Markdown
Collaborator

Landed on main in #108440. Both commits were cherry-picked with authorship preserved: a5c801c (no ambient TERMINAL_* bridge under a profile home override) and 606903b (launch-profile turns bind their own terminal scope once multiplexing is active). Fixes #107422. Thanks, @infinitycrew39.

@teknium1 teknium1 closed this Sep 11, 2026
teknium1 added a commit that referenced this pull request Sep 13, 2026
…own terminal scope

Under multiplexing tools/terminal_tool.py no longer bridges a profile's
terminal.* into os.environ while a home override is bound (#108440), so any
secondary-profile entrypoint that binds only the home (+ secrets) leaves
terminal_tool on the launch process's ambient TERMINAL_*: a secondary with
`terminal.backend: docker` ran its prompt.background / prompt.btw /
preview.restart side agents, its eager resume and its session.branch build
on the launch `local` backend.

_spawn_side_agent and _profile_build_scope now enter
_session_profile_runtime_scope, the same home -> secrets -> terminal
composition a prompt turn binds (and gateway/run.py::_profile_runtime_scope
mirrors). The terminal scope is installed for the whole worker/build
lifetime and reset on success and on exception; a malformed secondary
config still yields the fail-closed refusal scope. No ambient os.environ
write is restored.

Refs #108440 review (andrexibiza), #107442 (ehz0ah).
teknium1 added a commit that referenced this pull request Sep 13, 2026
…olicy once multiplexing is active

606903b made launch-profile turns bind a file-backed terminal scope as
soon as any secondary home is served, so a poisoned ambient bridge can never
be the launch turn's authority. That scope is rebuilt from defaults +
<home>/.env + config.yaml only, which drops the launch process's legitimate
env-only policy: TERMINAL_ENV=ssh TERMINAL_SSH_HOST=example.test with a `{}`
config.yaml became backend=local, ssh_host='' the moment a second profile
was served.

tui_gateway/launch_terminal_policy.py freezes the process TERMINAL_* once,
in _profile_home right before the first secondary home is registered as
served — the last moment ambient env is provably the launch profile's own.
build_profile_terminal_scope takes that snapshot as a trusted env_overlay
sitting where the process env sits in the standalone bridge (explicit YAML
keys still win). Launch turns overlay the snapshot; ambient os.environ is
never re-read after activation, so a later secondary write is still
rejected, and the scope is reset after the turn as before.

Refs #108440 review (andrexibiza), #107442 (ehz0ah).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/profiles Multi-profile isolation, HERMES_HOME scoping comp/tools Tool registry, model_tools, toolsets comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades tool/terminal Terminal execution and process management type/bug Something isn't working

Projects

None yet

4 participants