fix(tui): bind launch-profile terminal scope once multiplexing is active

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 (#107422 residual of #68559).

(cherry picked from commit f81147c1e5d283837e5e27f4da79710da7025235)
This commit is contained in:
infinitycrew39
2026-09-10 22:09:26 +07:00
committed by Teknium
parent a5c801c8dc
commit 606903badc
3 changed files with 47 additions and 13 deletions

View File

@@ -240,3 +240,39 @@ def test_dotenv_json_strings_stay_json_strings(tmp_path):
scope = build_profile_terminal_scope(home)
assert json.loads(scope["TERMINAL_DOCKER_FORWARD_ENV"]) == ["EMAIL_HOME_ADDRESS"]
assert json.loads(scope["TERMINAL_DOCKER_VOLUMES"]) == ["/tmp/a:/data"]
def test_launch_turn_binds_terminal_scope_once_multiplexing_is_active(
tmp_path, monkeypatch
):
"""#107422: after multiplexing starts, launch turns bind the launch home's
own terminal policy (mirrors ``prompt_turn._prepare_turn_input``'s
``elif _served_profile_homes`` branch) so poisoned ambient os.environ is
never the authority."""
from tools.terminal_scope import (
get_terminal_scope,
install_profile_terminal_scope,
reset_terminal_scope,
)
launch_home = tmp_path / ".hermes"
launch_home.mkdir()
(launch_home / "config.yaml").write_text(
"terminal:\n backend: local\n", encoding="utf-8"
)
monkeypatch.setenv("HERMES_HOME", str(launch_home))
# Poison ambient the way the pre-fix latch did — launch scope must win.
monkeypatch.setenv("TERMINAL_ENV", "docker")
monkeypatch.setenv("TERMINAL_DOCKER_IMAGE", "bee/img:1")
token = install_profile_terminal_scope(launch_home)
try:
assert get_terminal_scope() is not None
assert terminal_env("TERMINAL_ENV") == "local"
# DEFAULT_CONFIG may backfill docker_image; the poisoned bee image must not win.
assert terminal_env("TERMINAL_DOCKER_IMAGE", "") != "bee/img:1"
assert os.environ["TERMINAL_ENV"] == "docker"
assert os.environ["TERMINAL_DOCKER_IMAGE"] == "bee/img:1"
finally:
reset_terminal_scope(token)
assert get_terminal_scope() is None

View File

@@ -450,6 +450,13 @@ def _prepare_turn_input(sid: str, session: dict, st: _TurnRun, text: Any, images
scopes.secret = set_secret_scope(build_profile_secret_scope(Path(profile_home)))
from tools.terminal_scope import install_profile_terminal_scope
scopes.terminal = install_profile_terminal_scope(Path(profile_home))
elif _served_profile_homes:
# Multiplex residual of #68559 / #107422: the launch profile used to run
# unscoped and fall back to ambient os.environ. Once any secondary home
# has been served, bind the launch home's own terminal policy so a
# poisoned ambient bridge can never become the launch turn's authority.
from tools.terminal_scope import install_profile_terminal_scope
scopes.terminal = install_profile_terminal_scope(Path(_hermes_home))
# The sudo password callback is thread-local: without re-wiring here, sudo prompts
# fall through to /dev/tty and hang the headless gateway (re-run is a no-op).
_wire_callbacks(sid)

View File

@@ -506,19 +506,10 @@ def _profile_scoped(handler):
Secondary-profile adapters are constructed inside ``_profile_runtime_scope`` (secret scope installed +
multiplex active) — the same discriminator the Buzz/SimpleX adapters use for this bug class (#98738).
The DEFAULT profile under multiplexing runs unscoped: ``os.environ`` holds its own bridge output there
and keeps its legacy precedence.
Same discriminator as the Buzz/SimpleX/Raft adapters (#98738): secret scope installed + multiplex
active. The DEFAULT profile under multiplexing (and every single-profile process) runs unscoped and
keeps its legacy ``os.environ`` precedence.
Secondary-profile adapters are constructed, connected, and reloaded inside ``_profile_runtime_scope``
(secret scope installed + multiplex active) — the same discriminator as the Discord adapter's
``_profile_scoped_config_load`` (#72348). The DEFAULT profile under multiplexing runs unscoped:
``os.environ`` holds its own bridge output there and keeps its legacy precedence.
Secondary-profile adapters are constructed, connected, and reloaded inside ``_profile_runtime_scope``
(secret scope installed + multiplex active) — the same discriminator the Buzz/SimpleX adapters use for
this bug class (#98738). The DEFAULT profile under multiplexing runs unscoped: ``os.environ`` holds its
own bridge output there and keeps its legacy precedence.
Once multiplexing is active, launch-profile *turns* bind their own terminal scope
(``prompt_turn._prepare_turn_input``) so they never depend on ambient ``os.environ``
that a secondary context might have poisoned (#107422). Single-profile processes stay
unscoped and keep legacy ``os.environ`` precedence.
"""
def wrapper(rid, params):
home = _profile_home(params.get("profile") if isinstance(params, dict) else None)