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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user